refactor: 更新音频文件路径和UI样式调整

fix: 修正TTS提供商配置中的null值问题
chore: 清理无用文件和更新输入文本内容
This commit is contained in:
hex2077
2025-08-20 14:18:18 +08:00
parent a7ef2d6606
commit d3bd3fdff2
26 changed files with 125 additions and 207 deletions

138
CLAUDE.md
View File

@@ -1,138 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 常用命令
### Python 后端播客生成器
* **生成播客**:
```bash
python podcast_generator.py [可选参数]
```
可选参数包括:
* `--api-key <YOUR_OPENAI_API_KEY>`: OpenAI API 密钥。
* `--base-url <YOUR_OPENAI_BASE_URL>`: OpenAI API 代理地址。
* `--model <OPENAI_MODEL_NAME>`: 使用的 OpenAI 模型,默认为 `gpt-3.5-turbo`。
* `--threads <NUMBER_OF_THREADS>`: 生成音频的并行线程数,默认为 `1`。
**示例**:
```bash
python podcast_generator.py --api-key sk-xxxxxx --model gpt-4o --threads 4
```
* **启动 FastAPI Web 服务**:
```bash
python main.py
```
默认在 `http://localhost:8000` 启动,提供 REST API 接口。
* **检查 TTS 语音列表**:
```bash
python check/check_edgetts_voices.py
python check/check_indextts_voices.py
# 其他 TTS 服务检查脚本...
```
### Next.js Web 应用 (web/ 目录)
* **开发模式**:
```bash
cd web
npm run dev
```
在 `http://localhost:3000` 启动开发服务器。
* **构建生产版本**:
```bash
cd web
npm run build
```
* **启动生产服务器**:
```bash
cd web
npm run start
```
* **类型检查**:
```bash
cd web
npm run type-check
```
* **代码检查**:
```bash
cd web
npm run lint
```
* **安装依赖**:
```bash
cd web
npm install
```
## 高层代码架构
本项目是一个全栈播客生成器,包含 Python 后端和 Next.js Web 前端,核心功能是利用 AI 生成播客脚本并将其转换为音频。
### Python 后端架构
* **`podcast_generator.py`**: 主运行脚本,负责协调整个播客生成流程,包括:
* 读取配置文件 (`config/*.json`)。
* 读取输入文件 (`input.txt`) 和 AI 提示词文件 (`prompt/*.txt`)。
* 调用 OpenAI API 生成播客大纲和详细脚本。
* 调用配置的 TTS 服务生成音频。
* 使用 FFmpeg 合并生成的音频文件。
* 支持命令行参数配置 OpenAI API 和线程数。
* **`main.py`**: FastAPI Web 服务,提供 REST API 接口:
* `/generate-podcast`: 启动播客生成任务
* `/podcast-status`: 查询生成进度
* `/download-podcast/`: 下载生成的音频文件
* `/get-voices`: 获取可用的 TTS 语音列表
* **`tts_adapters.py`**: TTS 服务适配器,统一处理不同 TTS 服务的 API 调用。
* **`openai_cli.py`**: 负责与 OpenAI API 进行交互的模块。
* **`config/`**: 存放 TTS 服务和播客角色配置的 JSON 文件。例如 `edge-tts.json`。这些文件定义了 `podUsers` (播客角色)、`voices` (可用语音) 和 `apiUrl` (TTS 服务接口)。
* **`prompt/`**: 包含用于指导 AI 生成内容的提示词文件。
* `prompt-overview.txt`: 用于生成播客整体大纲。
* `prompt-podscript.txt`: 用于生成详细对话脚本,包含占位符 (`{{numSpeakers}}`, `{{turnPattern}}`)。
* **`check/`**: TTS 服务语音列表检查脚本,用于验证各种 TTS 服务的可用语音。
### Next.js Web 前端架构 (web/ 目录)
* **技术栈**: Next.js 14 (App Router), TypeScript, Tailwind CSS, Framer Motion
* **`src/app/`**: Next.js App Router 页面和 API 路由
* `api/generate-podcast/route.ts`: 播客生成 API与 Python 后端集成
* `api/audio/[filename]/route.ts`: 音频文件服务 API
* `api/config/route.ts`: 配置管理 API
* `api/tts-voices/route.ts`: TTS 语音列表 API
* **`src/components/`**: React 组件
* `PodcastCreator.tsx`: 播客创建器主组件
* `AudioPlayer.tsx`: 音频播放器组件
* `ProgressModal.tsx`: 生成进度显示模态框
* `ConfigSelector.tsx`: TTS 配置选择器
* `VoicesModal.tsx`: 语音选择模态框
* **`src/types/`**: TypeScript 类型定义,定义了播客生成请求/响应的数据结构
* **集成方式**: Web 应用通过 Node.js 子进程启动 Python 脚本,实时监控生成进度,并提供音频文件访问服务。
### TTS 服务集成
项目设计为高度灵活,支持多种 TTS 服务:
* **本地服务**: Index-TTS, Edge-TTS
* **网络服务**: 豆包 (Doubao), Minimax, Fish Audio, Gemini TTS
* **配置方式**: 通过 `config/*.json` 中的 `apiUrl` 进行配置
### 音频处理
使用 FFmpeg 工具将各个角色的语音片段拼接成一个完整的播客音频文件。FFmpeg 必须安装并配置在系统环境变量中。

View File

@@ -9,17 +9,17 @@
"api_url": null "api_url": null
}, },
"doubao": { "doubao": {
"X-Api-App-Id": "null", "X-Api-App-Id": null,
"X-Api-Access-Key": "null" "X-Api-Access-Key": null
}, },
"fish": { "fish": {
"api_key": "null" "api_key": null
}, },
"minimax": { "minimax": {
"group_id": "null", "group_id": null,
"api_key": "null" "api_key": null
}, },
"gemini": { "gemini": {
"api_key": "null" "api_key": null
} }
} }

View File

@@ -1,14 +0,0 @@
```custom-begin
Start your podcast with “欢迎收听来生小酒馆客官不进来喝点吗End with “感谢收听,欢迎下次再来”
```custom-end
### AI产品与功能更新
1. B站最近推出了一项堪称"黑科技”的**AI原声翻译功能**它能在翻译视频内容的同时奇迹般地保留UP主独特的声线、音色和语气习惯 (o´ω'o)ノ。这项技术不仅解决了跨语言交流的生硬感,更通过[深度研究技术AI资讯](https://www.aibase.com/zh/news/20183)精准拿捏了游戏、二次元等领域的"行话”与"梗”,让文化出海之路变得既地道又充满人情味儿 🔥。这简直是为全球粉丝献上的一份原汁原味的大礼,确保了情感连接不会在翻译中"迷路”。
<br/>![AI资讯B站AI翻译保留UP主音色](https://cdn.jsdmirror.com/gh/justlovemaki/imagehub@main/images/2025/08/news_01k1tshmx8frtvqqp1f9wwnrmn.avif)
2. Figma开发者模式迎来史诗级更新正式向设计师与开发者之间的"沟通地狱”宣战 (✧∀✧)!全新的**彩色交互式批注系统**,让交互逻辑、样式规范和无障碍需求一目了然,彻底告别了无休止的猜谜游戏。更具革命性的是,升级后的**MCP协议**能将设计系统的结构化数据直接喂给AI编码工具这意味着AI生成的代码将前所未有地贴合设计稿让[设计转代码的效率AI资讯](https://www.aibase.com/zh/news/20211)实现指数级暴增 🚀。
<br/>![AI资讯Figma彩色批注系统](https://cdn.jsdmirror.com/gh/justlovemaki/imagehub@main/images/2025/08/news_01k1tshqr4f2wt2pwqfv42m12s.avif)<br/>![AI资讯Figma开发者模式更新](https://cdn.jsdmirror.com/gh/justlovemaki/imagehub@main/images/2025/08/news_01k1tshv8geg5at2md3331gqfc.avif)
3. 米哈游联合创始人蔡浩宇亲自操刀的AI互动游戏**《星之低语》**即将在Steam平台开启一场前所未有的情感实验 🌌。玩家将通过麦克风与坠落在异星的宇航员Stella进行完全由AI驱动的开放式对话你的每一句话都将直接影响她的命运。这款游戏彻底抛弃了传统对话树旨在探索人机之间建立深层情感连接的可能性正如[这份游戏前瞻AI资讯](https://www.aibase.com/zh/news/20184)所说,未来每个人都可能拥有一个数字灵魂伴侣 💡。
<br/>![AI资讯米哈游AI游戏《星之低语》](https://cdn.jsdmirror.com/gh/justlovemaki/imagehub@main/images/2025/08/news_01k1tshxk8e2hb8cnx8e5gytwy.avif)

View File

@@ -6,8 +6,8 @@ import os
import json import json
def check_doubao_tts_voices(): def check_doubao_tts_voices():
config_file_path = "config/doubao-tts.json" config_file_path = "../config/doubao-tts.json"
tts_providers_path = "config/tts_providers.json" tts_providers_path = "../config/tts_providers.json"
test_text = "你好" # 测试文本 test_text = "你好" # 测试文本
try: try:

View File

@@ -5,8 +5,8 @@ import msgpack
import json import json
def check_fishaudio_voices(): def check_fishaudio_voices():
config_file_path = "config/fish-audio.json" config_file_path = "../config/fish-audio.json"
tts_providers_path = "config/tts_providers.json" tts_providers_path = "../config/tts_providers.json"
test_text = "你好" # 测试文本 test_text = "你好" # 测试文本
try: try:

View File

@@ -7,8 +7,8 @@ import base64
import json import json
def check_gemini_voices(): def check_gemini_voices():
config_file_path = "config/gemini-tts.json" config_file_path = "../config/gemini-tts.json"
tts_providers_path = "config/tts_providers.json" tts_providers_path = "../config/tts_providers.json"
test_text = "你好" # 测试文本 test_text = "你好" # 测试文本
try: try:

View File

@@ -5,8 +5,8 @@ import os
import json import json
def check_minimax_voices(): def check_minimax_voices():
config_file_path = "config/minimax.json" config_file_path = "../config/minimax.json"
tts_providers_path = "config/tts_providers.json" tts_providers_path = "../config/tts_providers.json"
test_text = "你好" # 测试文本 test_text = "你好" # 测试文本
try: try:

59
server/input.txt Normal file
View File

@@ -0,0 +1,59 @@
```custom-begin
start with '欢迎收听来生小酒馆,客官不进来喝点吗?' end with '感谢收听,下期再见'
不要自称主理人,馆长。说话符合人物角色设定。
```custom-end
### 产品与功能更新
1. DeepSeek V3.1 版本悄然上线,**上下文长度直接飙升至 128K**,处理十几万字的文档或整个代码库都变得轻而易举 (o´ω'o)ノ。本次升级不仅推理能力提升43%、幻觉减少38%多语言支持也更上一层楼唯一的美中不足是大家翘首以盼的R2模型仍是"犹抱琵琶半遮面”。现在就去[官网体验一下 - AI资讯](https://chat.deepseek.com/),感受超长文本的威力吧!
2. 还在为复杂的图文视频生成流程头疼吗Higgsfield AI 推出的 **Draw-to-Video** 功能让你彻底告别繁琐的文本提示词只需在图片上画个箭头或圈圈AI就能心领神会地生成电影级动态视频 🔥。这种"指哪打哪”的直观创作方式在外网迅速爆火,让视频创作的门槛又降低了一大截。快来[这里体验这份快乐 - AI资讯](https://higgsfield.ai/),让你的图片动起来!<br/>![AI资讯Higgsfield AI 的 Draw-to-Video 功能](https://image.jiqizhixin.com/uploads/editor/0416df91-9e5c-4677-ba53-a415ebe84ed1/640.gif)
3. 小红书AIGC团队祭出大招正式发布了名为 **DynamicFace 的可控人脸生成技术**,致力于解决图像和视频换脸中的老大难问题 🤔。这项技术的核心亮点在于"可控”与"高度一致性”,旨在消除视频换脸时常见的闪烁和不连贯感,为用户提供更精准、更个性的创作工具。正如[这篇AI资讯报道](https://www.aibase.com/zh/news/20613)所说这是小红书在AI内容生成领域迈出的重要一步让创意表达拥有了更多可能。
4. 英伟达发布了在排行榜上名列前茅的 **Nemotron Nano 2** 模型,这个仅 **9B 参数**的多语言推理小钢炮正在重新定义AI的效率边界 🚀。它采用了独特的 **Transformer-Mamba 混合架构**实现了比同类8B模型快6倍的吞吐量同时通过"思考预算”机制将成本削减高达60%。想了解更多[技术细节可看这篇AI资讯](https://nvda.ws/3JfcKST),或者直接去[排行榜围观AI资讯](https://nvda.ws/47B7iUh),见证它的强大!<br/><video src="https://video.twimg.com/amplify_video/1957573291566063621/vid/avc1/720x1280/goPWf6djGgXEiqL5.mp4?tag=14" controls="controls" width="100%"></video>
5. Gemini API 迎来了一项超实用的更新,现在**直接支持对URL进行内容抓取**无论是网页、PDF还是图片链接统统可以一网打尽这意味着开发者可以省去调用第三方抓取API的麻烦和费用直接让模型处理网络上的实时内容堪称是降本增效的一大利器 (✧∀✧)。快来[看看这篇AI资讯解读](https://x.com/dotey/status/1957579164363481114),了解如何用好这个新功能吧!<br/>![AI资讯Gemini API 抓取示例](https://pbs.twimg.com/media/Gyqd8opWIAMjgEU?format=jpg&name=orig)
### 前沿研究
1. AI模型在理解图像时会不会因为思维定式而"一叶障目”一篇来自arXiv的[最新研究AI资讯](https://arxiv.org/abs/2404.10357)提出了**CoKnow框架**,通过引入多知识表征来优化提示学习,极大地丰富了模型的"视野”💡。简单说,它不再让模型只走一条路,而是给它提供了多种"知识视角”来分析问题从而在11个公开数据集上超越了既有方法让模型预测更准确。
2. 如何让AI不仅会说话更能"共情”?一篇名为 E3RG 的[前沿论文AI资讯](https://arxiv.org/abs/2508.12854)提出了一种全新的多模态共情响应生成系统,将任务分解为**理解、记忆和生成**三部曲。该系统无需额外训练,就能生成包含丰富情感且身份一致的虚拟人形象,仿佛拥有了真正的"同理心”❤。这项研究在ACM MM 25挑战赛中斩获头名为构建更具人情味的人机交互开辟了新道路。
### 行业展望与社会影响
1. AI投资热潮之下现实却有些骨感麻省理工学院的一项研究发现高达 **95% 的企业未能从其AI投入中获得任何回报**总计约400亿美元的投资几乎打了水漂 💸。报告指出,"生成式AI鸿沟”的根源并非人才或资源匮乏而是AI系统普遍缺乏记忆和适应能力无法深度融入关键工作流程。正如[宝玉的这篇AI资讯分享](https://x.com/dotey/status/1957648622851428689)所言成功的AI部署更像是建立深度合作关系而非简单购买产品。
### 开源TOP项目
1. 腾讯为多模态和强化学习领域送上了一份大礼,正式开源了名为 **WeChat-YATT** 的大模型训练库,旨在解决两大核心瓶颈 🔥。通过创新的**并行控制器**机制和**异步交互**策略它有效解决了多模态训练的可扩展性难题和动态采样下的效率短板显著提升了GPU的利用率。想了解这一[开源利器的AI资讯详情](https://www.aibase.com/zh/news/20620),不妨深入看看官方发布的内容。<br/>![AI资讯腾讯开源WeChat-YATT训练库](https://upload.chinaz.com/2025/0819/6389120959924199577995616.png)
2. 谷歌的Genie 3还在闭源国产开源版世界模型 **Matrix-Game 2.0** 已经横空出世,在社区引发热议!这个仅 **1.8B 参数**的模型能在单块GPU上以 **25FPS** 的帧率实时生成可交互的虚拟世界,你只需上传一张图片,就能在其中自由探索 (✧∀✧)。昆仑万维的这一开源力作,以其惊人的轻量化和高性能,为游戏开发和智能体训练开启了无限想象,快去[GitHub主页 - AI资讯](https://github.com/SkyworkAI/Matrix-Game)一探究竟吧。<br/>![AI资讯Matrix-Game 2.0 实时生成虚拟世界](https://image.jiqizhixin.com/uploads/editor/d7bfad6c-e613-40cf-8ec8-4bd9770615c8/640.gif)<br/>![AI资讯在Matrix-Game 2.0中探索GTA风格地图](https://image.jiqizhixin.com/uploads/editor/a3035e1c-ddbb-4f4b-ac4a-fc5e3f356816/640.gif)
3. 想摆脱商业邮件服务商的月费"绑架”吗?**BillionMail** 这个在 [GitHub 上 ⭐8.9k 星的AI资讯项目](https://github.com/aaPanel/BillionMail) 为你提供了一站式开源解决方案,集邮件服务器、新闻通讯和邮件营销于一身。它完全支持自托管,对开发者极其友好,让你能以零月费的方式掌控自己的邮件系统,实现真正的数字独立 🚀。
4. 如果你是追求极致简约的音乐爱好者,那么在 [GitHub 上拥有 ⭐4.7k 星的 SPlayerAI资讯](https://github.com/imsyy/SPlayer) 绝对值得一试。这款播放器不仅界面清爽,还支持**逐字歌词、歌曲下载、音乐云盘管理**等强大功能,甚至还有酷炫的音乐频谱,堪称简约而不简单 (o´ω'o)ノ。它完美诠释了如何在小巧的体积中,容纳一个完整的音乐世界。
5. 对于那些对数字踪迹充满好奇的技术爱好者,[GitHub 上的 GhostTrackAI资讯](https://github.com/HunxByts/GhostTrack) 项目提供了一个用于追踪位置或手机号码的实用工具,已收获 ⭐1.9k 星。它就像一个数字世界的侦探工具,虽然用途广泛,但也提醒着我们在探索技术边界的同时,必须时刻关注隐私与伦理 🤔。
6. 让你的电脑拥有一个AI管家是怎样的体验在 [GitHub 上收获 ⭐1.9k 星的 bytebotAI资讯](https://github.com/bytebot-ai/bytebot) 就是这样一个自托管的AI桌面代理它能通过自然语言命令自动化执行电脑任务。它在安全的**容器化Linux环境**中运行,让你只需动动嘴,就能完成复杂操作,真正实现"君子动口不动手”的智能生活 🔥。
### 社媒分享
1. 进入AI领域不只需要懂代码和数学软技能同样关键吴恩达发布了一本免费的[职业指导电子书AI资讯](https://hubs.la/Q03DgNQ50)堪称是为AI求职者量身打造的"通关秘籍”💡。书中涵盖了**简历制作、面试技巧**,甚至还包括如何克服"冒名顶替综合症”,帮助你规划清晰的职业路线图,向心仪的工作迈进。<br/>![AI资讯吴恩达发布的免费电子书](https://pbs.twimg.com/media/Gyqx7K_W8AI4o8Z?format=jpg&name=orig)
2. 在AI绘画中提示词是不是越长越好一位Reddit用户发出了灵魂拷问他发现自己用二三十个词的短提示词生成效果和别人几百词的长篇大论相差无几甚至模型还会忽略大部分细节 🤔。这篇引发热议的[帖子 - AI资讯](https://old.reddit.com/r/FluxAI/comments/1mtyikj/whats_the_point_of_overly_long_prompts/)探讨了"长提示词”的实际意义,或许有时候,简洁才是通往好作品的捷径。
3. DeepSeek V3.1 的前端代码能力似乎又在"闷声发大财”了,有用户惊喜地发现,以前搞不定的一个复杂提示词,新版模型居然轻松拿捏,而且没有出现其他模型的字体大小问题 (✧∀✧)。这个在[社交媒体上的AI资讯发现](https://x.com/op7418/status/1957784895952155089),再次印证了官方宣布的 **128k 上下文**升级背后,是实打实的性能提升。<br/>![AI资讯Deepseek V3.1 官方更新通知](https://pbs.twimg.com/media/GytgxDKacAMVWfO?format=jpg&name=orig)
4. 提示词工程也能成为一门艺术!用户李继刚分享了一段极具诗意的"视觉编织场”Prompt用**光、张力、流**等充满美学的隐喻指导AI将播客链接转化为设计感十足的可视化卡片 🎨。这种将设计哲学融入提示词的[高级玩法AI资讯](https://x.com/lijigang_com/status/1957756215653724324)展示了与AI沟通的全新境界堪称一场人与机器的灵感共舞。<br/>![AI资讯李继刚的视觉编织场Prompt](https://pbs.twimg.com/media/GytY4-XacAQjMsJ?format=jpg&name=orig)
5. 千问最新开源的图像编辑模型与FLUX Kontext的对决结果出炉根据[博主的AI资讯评测](https://weibo.com/6182606334/Q0yOekb6d),千问模型的最大亮点在于其**独一无二的中文生成和编辑能力**但图像美学和细节处理上则稍逊于FLUXAI感较重。总的来说它为中文内容创作提供了新利器但想达到顶级效果可能还需社区的LoRA模型来"画龙点睛”✨。
6. OpenAI正在让顶级AI变得更亲民**ChatGPT Go** 计划已在印度率先启动每月订阅费仅需约4.55美元 🇮🇳!根据[Greg Brockman的AI资讯分享](https://x.com/gdb/status/1957650320923979996),该计划提供了比免费版**高10倍的消息量和图像生成量**以及更长的记忆力。此举被视为AI普惠的重要一步让更多人能以低成本享受强大AI工具带来的便利。
7. 想和孩子一起创作一本独一无二的故事书吗Google Gemini 的 **Storybook** 功能让这一切变得简单有趣,正如[这篇AI资讯教程](https://x.com/shao__meng/status/1957605772017430917)所分享的,你可以上传照片作为灵感,指定**漫画或黏土动画**等艺术风格。这不仅是一个AI工具更是一个激发家庭创造力、记录温馨回忆的互动平台 (o´ω'o)ノ。<br/>![AI资讯Google Gemini Storybook 使用技巧](https://pbs.twimg.com/media/GyrQEOLagAAz6OA?format=jpg&name=orig)

View File

@@ -74,7 +74,7 @@ stop_scheduler_event = threading.Event()
# 全局配置 # 全局配置
output_dir = "output" output_dir = "output"
time_after = 10 time_after = 30
# 内存中存储任务结果 # 内存中存储任务结果
# {task_id: {"auth_id": auth_id, "status": TaskStatus, "result": any, "timestamp": float}} # {task_id: {"auth_id": auth_id, "status": TaskStatus, "result": any, "timestamp": float}}
@@ -86,12 +86,12 @@ audio_file_mapping: Dict[str, Dict] = {}
SECRET_KEY = os.getenv("PODCAST_API_SECRET_KEY", "your-super-secret-key") # 在生产环境中请务必修改! SECRET_KEY = os.getenv("PODCAST_API_SECRET_KEY", "your-super-secret-key") # 在生产环境中请务必修改!
# 定义从 tts_provider 名称到其配置文件路径的映射 # 定义从 tts_provider 名称到其配置文件路径的映射
tts_provider_map = { tts_provider_map = {
"index-tts": "config/index-tts.json", "index-tts": "../config/index-tts.json",
"doubao-tts": "config/doubao-tts.json", "doubao-tts": "../config/doubao-tts.json",
"edge-tts": "config/edge-tts.json", "edge-tts": "../config/edge-tts.json",
"fish-audio": "config/fish-audio.json", "fish-audio": "../config/fish-audio.json",
"gemini-tts": "config/gemini-tts.json", "gemini-tts": "../config/gemini-tts.json",
"minimax": "config/minimax.json", "minimax": "../config/minimax.json",
} }
# 定义一个函数来清理输出目录 # 定义一个函数来清理输出目录

View File

@@ -19,7 +19,7 @@ from tts_adapters import TTSAdapter, IndexTTSAdapter, EdgeTTSAdapter, FishAudioA
# Global configuration # Global configuration
output_dir = "output" output_dir = "output"
file_list_path = os.path.join(output_dir, "file_list.txt") file_list_path = os.path.join(output_dir, "file_list.txt")
tts_providers_config_path = 'config/tts_providers.json' tts_providers_config_path = '../config/tts_providers.json'
def read_file_content(filepath): def read_file_content(filepath):
"""Reads content from a given file path.""" """Reads content from a given file path."""
@@ -39,7 +39,7 @@ def _load_json_config(file_path: str) -> dict:
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise ValueError(f"Error decoding JSON from {file_path}: {e}") raise ValueError(f"Error decoding JSON from {file_path}: {e}")
def select_json_config(config_dir='config', return_file_path=False): def select_json_config(config_dir='../config', return_file_path=False):
""" """
Reads JSON files from the specified directory and allows the user to select one. Reads JSON files from the specified directory and allows the user to select one.
Returns the content of the selected JSON file. Returns the content of the selected JSON file.
@@ -114,6 +114,7 @@ def generate_speaker_id_text(pod_users, voices_list):
def merge_audio_files(): def merge_audio_files():
# 生成一个唯一的UUID # 生成一个唯一的UUID
unique_id = str(uuid.uuid4()) unique_id = str(uuid.uuid4())
unique_id = unique_id.replace("-", "")
# 获取当前时间戳 # 获取当前时间戳
timestamp = int(time.time()) timestamp = int(time.time())
# 组合UUID和时间戳作为文件名去掉 'podcast_' 前缀 # 组合UUID和时间戳作为文件名去掉 'podcast_' 前缀

View File

@@ -14,7 +14,7 @@ export async function GET(request: NextRequest) {
} }
// 构建文件路径 // 构建文件路径
const outputDir = path.join(process.cwd(), '..', 'output'); const outputDir = path.join(process.cwd(), '..', 'server', 'output');
const filePath = path.join(outputDir, filename); const filePath = path.join(outputDir, filename);
try { try {

View File

@@ -22,7 +22,11 @@ export async function GET() {
} }
// 缓存无效或不存在,读取文件并更新缓存 // 缓存无效或不存在,读取文件并更新缓存
const configPath = path.join(process.cwd(), '..', 'config', 'tts_providers.json'); const ttsProvidersName = process.env.TTS_PROVIDERS_NAME;
if (!ttsProvidersName) {
throw new Error('TTS_PROVIDERS_NAME 环境变量未设置');
}
const configPath = path.join(process.cwd(), '..', 'config', ttsProvidersName);
const configContent = await fs.readFile(configPath, 'utf-8'); const configContent = await fs.readFile(configPath, 'utf-8');
const config = JSON.parse(configContent); const config = JSON.parse(configContent);

View File

@@ -14,20 +14,24 @@ export const metadata: Metadata = {
description: '使用AI技术将您的想法和内容转换为高质量的播客音频支持多种语音和风格选择。', description: '使用AI技术将您的想法和内容转换为高质量的播客音频支持多种语音和风格选择。',
keywords: ['播客', 'AI', '语音合成', 'TTS', '音频生成'], keywords: ['播客', 'AI', '语音合成', 'TTS', '音频生成'],
authors: [{ name: 'PodcastHub Team' }], authors: [{ name: 'PodcastHub Team' }],
viewport: 'width=device-width, initial-scale=1',
themeColor: '#000000',
icons: { icons: {
icon: '/favicon.webp', icon: '/favicon.webp',
apple: '/favicon.webp', apple: '/favicon.webp',
}, },
openGraph: { openGraph: {
title: 'PodcastHub - 给创意一个真实的声音', title: 'PodcastHub - 给创意一个真实的声音',
description: '使用AI技术将您的想法和内容转换为高质量的播客音频', description: '使用AI技术将您的想法和内容转换为高质量的播客音频,支持多种语音和风格选择。',
type: 'website', type: 'website',
locale: 'zh_CN', locale: 'zh_CN',
}, },
}; };
export const viewport = {
themeColor: '#000000',
width: 'device-width',
initialScale: 1,
};
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {

View File

@@ -100,7 +100,7 @@ export default async function PodcastContent({ fileName }: PodcastContentProps)
</div> </div>
{/* 标题 */} {/* 标题 */}
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 leading-tight break-words"> <h1 className="text-3xl md:text-3xl font-bold text-gray-900 leading-tight break-words">
{audioInfo.title} {audioInfo.title}
</h1> </h1>

View File

@@ -157,17 +157,34 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
<div className="flex items-center justify-center gap-3 mb-4"> <div className="flex items-center justify-center gap-3 mb-4">
<svg className="h-[80px] w-[300px] sm:h-[100px] sm:w-[600px]" viewBox="0 0 600 150" xmlns="http://www.w3.org/2000/svg"> <svg className="h-[80px] w-[300px] sm:h-[100px] sm:w-[600px]" viewBox="0 0 600 150" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<linearGradient id="waveGradient" x1="49" y1="98" x2="140" y2="98" gradientUnits="userSpaceOnUse"> <linearGradient id="waveGradient" x1="0" y1="0" x2="140" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#8E54E9"/> <stop stopColor="#D869E5">
<stop offset="1" stop-color="#C26AE6"/> <animate attributeName="stop-color" values="#D069E6;#FB866C;#FA6F7E;#E968E2;" dur="5s" repeatCount="indefinite"/>
</stop>
<stop offset="1" stopColor="#D069E6">
<animate attributeName="stop-color" values="#FB866C;#FA6F7E;#E968E2;#D869E5;" dur="5s" repeatCount="indefinite"/>
</stop>
</linearGradient> </linearGradient>
<linearGradient id="textGradient" x1="175" y1="0" x2="810" y2="0" gradientUnits="userSpaceOnUse"> <linearGradient id="textGradient" x1="600" y1="0" x2="150" y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0.05" stop-color="#D069E6"/> <stop offset="0" stopColor="#C75AD4">
<stop offset="0.35" stop-color="#FB866C"/> <animate attributeName="stop-color" values="#C75AD4;#D85AD1;#F85F6F;#F9765B;#C15ED5;#C75AD4" dur="10s" repeatCount="indefinite" />
<stop offset="0.55" stop-color="#FA6F7E"/> </stop>
<stop offset="0.85" stop-color="#E968E2"/> <stop offset="0.1818" stopColor="#D85AD1">
<stop offset="1" stop-color="#D869E5"/> <animate attributeName="stop-color" values="#D85AD1;#F85F6F;#F9765B;#C15ED5;#C75AD4;#D85AD1" dur="10s" repeatCount="indefinite" />
</stop>
<stop offset="0.3636" stopColor="#F85F6F">
<animate attributeName="stop-color" values="#F85F6F;#F9765B;#C15ED5;#C75AD4;#D85AD1;#F85F6F" dur="10s" repeatCount="indefinite" />
</stop>
<stop offset="0.5455" stopColor="#F9765B">
<animate attributeName="stop-color" values="#F9765B;#C15ED5;#C75AD4;#D85AD1;#F85F6F;#F9765B" dur="10s" repeatCount="indefinite" />
</stop>
<stop offset="0.7273" stopColor="#C15ED5">
<animate attributeName="stop-color" values="#C15ED5;#C75AD4;#D85AD1;#F85F6F;#F9765B;#C15ED5" dur="10s" repeatCount="indefinite" />
</stop>
<stop offset="0.9091" stopColor="#C75AD4">
<animate attributeName="stop-color" values="#C75AD4;#D85AD1;#F85F6F;#F9765B;#C15ED5;#C75AD4" dur="10s" repeatCount="indefinite" />
</stop>
</linearGradient> </linearGradient>
</defs> </defs>

View File

@@ -139,21 +139,6 @@ const Sidebar: React.FC<SidebarProps> = ({
{/* 品牌名称容器 - 慢慢收缩动画 */} {/* 品牌名称容器 - 慢慢收缩动画 */}
<div className="overflow-hidden transition-all duration-500 ease-in-out w-auto "> <div className="overflow-hidden transition-all duration-500 ease-in-out w-auto ">
<svg className="h-[30px] w-[180px] sm:h-[30px] sm:w-[180px]" viewBox="0 0 800 150" xmlns="http://www.w3.org/2000/svg"> <svg className="h-[30px] w-[180px] sm:h-[30px] sm:w-[180px]" viewBox="0 0 800 150" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="waveGradient" x1="49" y1="98" x2="140" y2="98" gradientUnits="userSpaceOnUse">
<stop stop-color="#8E54E9"/>
<stop offset="1" stop-color="#C26AE6"/>
</linearGradient>
<linearGradient id="textGradient" x1="175" y1="0" x2="810" y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0.05" stop-color="#D069E6"/>
<stop offset="0.35" stop-color="#FB866C"/>
<stop offset="0.55" stop-color="#FA6F7E"/>
<stop offset="0.85" stop-color="#E968E2"/>
<stop offset="1" stop-color="#D869E5"/>
</linearGradient>
</defs>
<g> <g>
<path <path
d="M49 98.5 C 56 56.5, 65 56.5, 73 90.5 C 79 120.5, 85 125.5, 91 100.5 C 96 80.5, 100 75.5, 106 95.5 C 112 115.5, 118 108.5, 125 98.5" d="M49 98.5 C 56 56.5, 65 56.5, 73 90.5 C 79 120.5, 85 125.5, 91 100.5 C 96 80.5, 100 75.5, 106 95.5 C 112 115.5, 118 108.5, 125 98.5"

View File

@@ -82,7 +82,7 @@ const Toast: React.FC<ToastProps> = ({
return ( return (
<div <div
className={cn( className={cn(
"flex items-start gap-3 p-4 rounded-lg shadow-lg bg-white border border-gray-200 backdrop-blur-md max-w-sm w-full transition-all duration-300 ease-in-out", "flex items-start gap-3 p-4 rounded-lg shadow-lg bg-white border border-gray-200 backdrop-blur-md max-w-sm w-full transition-all duration-300 ease-in-out pointer-events-auto",
getAccentColor(), // 添加左侧强调色边框 getAccentColor(), // 添加左侧强调色边框
isVisible && !isLeaving ? "translate-y-0 opacity-100" : "-translate-y-4 opacity-0" // 向上弹出动画 isVisible && !isLeaving ? "translate-y-0 opacity-100" : "-translate-y-4 opacity-0" // 向上弹出动画
)} )}
@@ -121,7 +121,7 @@ const Toast: React.FC<ToastProps> = ({
onRemove, onRemove,
}) => { }) => {
return ( return (
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 w-full max-w-md pointer-events-none p-4 flex flex-col items-center space-y-3"> {/* 定位到顶部水平居中并限制宽度使用flex布局垂直居中增加间距 */} <div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 w-full max-w-md p-4 flex flex-col items-center space-y-3"> {/* 定位到顶部水平居中并限制宽度使用flex布局垂直居中增加间距 */}
{toasts.map((toast) => ( {toasts.map((toast) => (
<Toast <Toast
key={toast.id} key={toast.id}