feat: 添加Docker支持并优化SEO和用户认证

refactor: 重构页面元数据以支持SEO规范链接
feat(web): 实现用户积分系统和登录验证
docs: 添加Docker使用指南和更新README
build: 添加Docker相关配置文件和脚本
chore: 更新依赖项并添加初始化SQL文件
This commit is contained in:
hex2077
2025-08-21 17:59:17 +08:00
parent d3bd3fdff2
commit 043b0e39f8
20 changed files with 862 additions and 26 deletions

125
DOCKER_USAGE.md Normal file
View File

@@ -0,0 +1,125 @@
# Docker 使用指南
本指南将介绍如何使用 `Dockerfile-Web``Dockerfile-Server` 来构建 Docker 镜像并运行整个应用程序,以及如何使用 Docker Compose 简化部署流程。
## 前提条件
* 已安装 Docker 和 Docker Compose。
## 方法一:分别构建和运行 Docker 镜像
`Simple-Podcast-Script` 项目的根目录下执行以下命令来构建 Docker 镜像。
### 构建 Docker 镜像
#### 构建 Web 应用镜像
```bash
docker build -t simple-podcast-web -f Dockerfile-Web .
```
* `-t simple-podcast-web`:为镜像指定一个名称和标签。
* `-f Dockerfile-Web`:指定 Web 应用的 Dockerfile 路径。
* `.`:指定构建上下文的路径,这里是项目的根目录。
#### 构建 Server 应用镜像
```bash
docker build -t simple-podcast-server -f Dockerfile-Server .
```
* `-t simple-podcast-server`:为镜像指定一个名称和标签。
* `-f Dockerfile-Server`:指定 Server 应用的 Dockerfile 路径。
* `.`:指定构建上下文的路径,这里是项目的根目录。
构建过程可能需要一些时间,具体取决于您的网络速度和系统性能。
### 运行 Docker 容器
#### 运行 Web 应用容器
```bash
docker run -d -p 3200:3000 -v /opt/audio:/app/server/output --restart always --name podcast-web simple-podcast-web
```
#### 命令说明:
* `-d`在分离模式detached mode下运行容器即在后台运行。
* `-p 3200:3000`:将宿主机的 3200 端口映射到容器的 3000 端口。Next.js 应用程序在容器内部的 3000 端口上运行。
* `-v /opt/audio:/app/server/output`:将宿主机的 `/opt/audio` 目录挂载到容器内的 `/app/server/output` 目录,用于音频文件的持久化存储。
* `--restart always`:设置容器的重启策略,确保容器在意外停止或系统重启后能自动重启。
* `--name podcast-web`:为运行中的容器指定一个名称,方便后续管理。
* `simple-podcast-web`:指定要运行的 Docker 镜像名称。
#### 运行 Server 应用容器
```bash
docker run -d -p 3100:8000 -v /opt/audio:/app/server/output --restart always --name podcast-server simple-podcast-server
```
或者,如果您的应用程序需要配置环境变量(例如 `PODCAST_API_SECRET_KEY`),您可以使用 `-e` 参数进行设置:
```bash
docker run -d -p 3100:8000 -v /opt/audio:/app/server/output --restart always --name podcast-server -e PODCAST_API_SECRET_KEY="your-production-api-secret-key" simple-podcast-server
```
#### 命令说明:
* `-d`在分离模式detached mode下运行容器即在后台运行。
* `-p 3100:8000`:将宿主机的 3100 端口映射到容器的 8000 端口。Server 应用程序在容器内部的 8000 端口上运行。
* `-v /opt/audio:/app/server/output`:将宿主机的 `/opt/audio` 目录挂载到容器内的 `/app/server/output` 目录,用于音频文件的持久化存储。
* `--restart always`:设置容器的重启策略,确保容器在意外停止或系统重启后能自动重启。
* `--name podcast-server`:为运行中的容器指定一个名称,方便后续管理。
* `-e PODCAST_API_SECRET_KEY="your-production-api-secret-key"`:设置环境变量,将 `"your-production-api-secret-key"` 替换为您的实际密钥。
* `simple-podcast-server`:指定要运行的 Docker 镜像名称。
## 方法二:使用 Docker Compose推荐
项目提供了 `docker-compose.yml` 文件,可以更方便地管理和部署整个应用。
### 启动服务
在项目根目录下执行以下命令启动所有服务:
```bash
docker-compose up -d
```
* `-d`:在分离模式下运行容器,即在后台运行。
### 停止服务
```bash
docker-compose down
```
### 查看服务状态
```bash
docker-compose ps
```
### 查看服务日志
```bash
# 查看所有服务日志
docker-compose logs
# 查看特定服务日志
docker-compose logs web
docker-compose logs server
```
## 验证应用程序是否运行
容器启动后,您可以通过以下地址来验证应用程序是否正常运行:
* Web 应用: `http://localhost:3200`
* Server 应用: `http://localhost:3100`
## 注意事项
1. 请确保宿主机上的端口 3100 和 3200 未被其他应用程序占用。
2. 请确保宿主机上的 `/opt/audio` 目录存在且具有适当的读写权限,或者根据实际情况修改挂载路径。
3. 在生产环境中,请使用安全的密钥替换示例中的 `PODCAST_API_SECRET_KEY`
4. 使用 Docker Compose 时服务间通过服务名称进行通信Web 应用通过 `http://server:8000` 访问 Server 应用。

30
Dockerfile-Server Normal file
View File

@@ -0,0 +1,30 @@
# 使用官方 Python 运行时作为父镜像
FROM python:3.11-alpine
# 安装 FFmpeg 和 FFprobe
# FFmpeg 用于音频处理,包括合并和转换
RUN apk add --no-cache ffmpeg && rm -rf /var/cache/apk/*
# 复制 requirements.txt 以便利用 Docker 缓存
COPY server/requirements.txt /tmp/requirements.txt
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r /tmp/requirements.txt && rm /tmp/requirements.txt
# 复制 server 目录的内容到容器的 /app/server
COPY server/ /app/server/
# 复制父目录的 config 文件夹到容器的 /app/config
COPY config/ /app/config/
# 设置工作目录为 server 目录
WORKDIR /app/server
# 暴露应用程序运行的端口
EXPOSE 8000
# 定义环境变量,如果你的应用需要
# ENV PODCAST_API_SECRET_KEY="your-production-api-secret-key"
# 运行应用程序的命令
# 使用 uvicorn 直接运行 FastAPI 应用
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

51
Dockerfile-Web Normal file
View File

@@ -0,0 +1,51 @@
# Stage 1: Dependency Installation and Build
FROM node:20-alpine AS builder
# Set working directory
WORKDIR /app/web
# Copy package.json and package-lock.json to leverage Docker cache
COPY web/package.json web/package-lock.json ./
# Install dependencies
RUN npm install --frozen-lockfile \
&& npm install --cpu=x64 --os=linux --libc=musl @libsql/client@latest --foreground-scripts
# Copy the rest of the application code (web directory content)
COPY web .
# Copy parent config directory
COPY config ../config
# Build the Next.js application
# The `standalone` output mode creates a self-contained application
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Stage 2: Production Image
FROM node:20-alpine AS runner
WORKDIR /app
# Create output directory for web/server.js to use
RUN mkdir -p server/output
RUN npm install @libsql/linux-x64-musl
# Set production environment
ENV NODE_ENV production
COPY web/.env ./web/.env
COPY web/sqlite.db ./web/sqlite.db
# Copy standalone application and public assets from the builder stage
COPY --from=builder /app/web/.next/standalone ./web/
COPY --from=builder /app/web/.next/static ./web/.next/static
COPY --from=builder /app/web/public ./web/public
# Copy parent config directory from builder stage to runner stage
COPY --from=builder /app/config ./config
# Expose port (Next.js default port)
EXPOSE 3000
# Start the Next.js application
CMD ["node", "web/server.js"]

View File

@@ -1,4 +1,4 @@
# 🎙️ 简易播客生成器 (Simple Podcast Generator)
# 🎙️ 播客生成器 (Podcast Generator)
> 轻松将您的想法,一键生成为生动有趣的多人对话播客!
> [English Version](README_EN.md)
@@ -73,6 +73,8 @@ python podcast_generator.py [可选参数]
* `--base-url <YOUR_OPENAI_BASE_URL>`: OpenAI API 的代理地址。若不提供,将从配置文件或 `OPENAI_BASE_URL` 环境变量中读取。
* `--model <OPENAI_MODEL_NAME>`: 指定使用的 OpenAI 模型(如 `gpt-4o`, `gpt-4-turbo`)。默认值为 `gpt-3.5-turbo`
* `--threads <NUMBER_OF_THREADS>`: 指定生成音频的并行线程数(默认为 `1`),提高处理速度。
* `--output-language <LANGUAGE_CODE>`: 指定播客脚本的输出语言(默认为 `Chinese`)。
* `--usetime <TIME_DURATION>`: 指定播客脚本的时间长度(默认为 `10 minutes`)。
#### **运行示例**
@@ -150,6 +152,48 @@ curl -X POST "http://localhost:8000/generate-podcast" \
---
## 🌐 Web 应用 (Next.js)
除了命令行脚本和 FastAPI 服务,本项目还提供了一个功能完善的 Web 用户界面。这个界面旨在提供更直观、便捷的播客生成与管理体验,将后端复杂的功能通过友好的前端操作暴露给用户。
### ✨ 核心功能亮点
* **web操作界面**: 直观友好的web界面让播客生成过程一目了然。
* **微用户体系集成**: 支持用户登录、注册、积分与计费功能,构建完善的用户生态。
* **播客创建与配置**: 允许用户通过表单输入主题,配置 TTS 角色、音量和语速等参数。
* **实时进度跟踪**: 显示播客生成的状态和进度。
* **播客播放与管理**: 集成音频播放器,方便用户收听已生成的播客,并可能提供管理历史播客的功能。
* **API 交互**: 通过 API 与后端 Python 服务无缝通信,包括播客生成、状态查询和音频流。
### 🚀 快速开始 (Web)
1. **安装 Node.js**: 请确保您的系统中已安装 Node.js (推荐 LTS 版本)。
2. **安装依赖**: 进入 `web/` 目录,安装所有前端依赖。
```bash
cd web/
npm install
# 或者 yarn install
```
3. **启动开发服务器**:
```bash
npm run dev
# 或者 yarn dev
```
Web 应用将在 `http://localhost:3000` (默认) 启动。
4. **构建生产环境**:
```bash
npm run build
# 或者 yarn build
npm run start
# 或者 yarn start
```
### 🐳 Docker 部署
本项目支持通过 Docker 进行部署,详细信息请参考 [Docker 使用指南](DOCKER_USAGE.md)。
---
## ⚙️ 配置文件详解
### `config/[tts-provider].json` (TTS 角色与语音配置)
@@ -253,15 +297,29 @@ curl -X POST "http://localhost:8000/generate-podcast" \
├── config/ # ⚙️ 配置文件目录
│ ├── doubao-tts.json # ... (各 TTS 服务商的配置)
│ └── tts_providers.json # 统一的 TTS 认证文件
├── server/ # 🐍 后端服务目录
│ ├── main.py # FastAPI Web API 入口:提供播客生成、状态查询、音频下载等 RESTful API管理任务生命周期并进行数据清理。
│ ├── podcast_generator.py # 核心播客生成逻辑:负责与 OpenAI API 交互生成播客脚本,调用 TTS 适配器将文本转语音,并使用 FFmpeg 合并音频文件。
│ ├── tts_adapters.py # TTS 适配器:封装了与不同 TTS 服务(如 Index-TTS, Edge-TTS, Doubao, Minimax, Fish Audio, Gemini的交互逻辑。
│ ├── openai_cli.py # OpenAI 命令行工具
│ └── ... # 其他后端文件
├── web/ # 🌐 前端 Web 应用目录 (Next.js)
│ ├── public/ # 静态资源
│ ├── src/ # 源码
│ │ ├── app/ # Next.js 路由页面
│ │ ├── components/ # React 组件
│ │ ├── hooks/ # React Hooks
│ │ ├── lib/ # 库文件 (认证、数据库、API等)
│ │ └── types/ # TypeScript 类型定义
│ ├── package.json # 前端依赖
│ ├── next.config.js # Next.js 配置
│ └── ... # 其他前端文件
├── prompt/ # 🧠 AI 提示词目录
│ ├── prompt-overview.txt
│ └── prompt-podscript.txt
├── example/ # 🎧 示例音频目录
├── output/ # 🎉 输出音频目录
├── input.txt # 🎙️ 播客主题输入文件
├── openai_cli.py # OpenAI 命令行工具
├── podcast_generator.py # 🚀 主运行脚本
├── tts_adapters.py # TTS 适配器文件
├── README.md # 📄 项目说明文档 (中文)
└── README_EN.md # 📄 项目说明文档 (英文)
```

View File

@@ -73,6 +73,8 @@ python podcast_generator.py [optional parameters]
* `--base-url <YOUR_OPENAI_BASE_URL>`: Proxy address of the OpenAI API. If not provided, it will be read from the configuration file or `OPENAI_BASE_URL` environment variable.
* `--model <OPENAI_MODEL_NAME>`: Specify the OpenAI model to use (such as `gpt-4o`, `gpt-4-turbo`). Default value is `gpt-3.5-turbo`.
* `--threads <NUMBER_OF_THREADS>`: Specify the number of parallel threads for audio generation (default is `1`) to improve processing speed.
* `--output-language <LANGUAGE_CODE>`: Specify the output language of the podcast script (default is `Chinese`).
* `--usetime <TIME_DURATION>`: Specify the time length of the podcast script (default is `10 minutes`).
#### **Running Example**
@@ -150,6 +152,48 @@ Additional instructions or context you want to provide to the AI, for example:
---
## 🌐 Web Application (Next.js)
In addition to the command-line script and FastAPI service, this project also provides a fully functional web user interface. This interface aims to provide a more intuitive and convenient podcast generation and management experience, exposing complex backend functions through friendly frontend operations to users.
### ✨ Core Features
* **Web Operation Interface**: Intuitive and friendly web interface that makes the podcast generation process clear at a glance.
* **Micro User System Integration**: Supports user login, registration, points and billing functions, building a complete user ecosystem.
* **Podcast Creation and Configuration**: Allows users to enter topics through forms and configure TTS characters, volume and speed parameters.
* **Real-time Progress Tracking**: Displays the status and progress of podcast generation.
* **Podcast Playback and Management**: Integrates an audio player for users to listen to generated podcasts and may provide functions for managing historical podcasts.
* **API Interaction**: Seamless communication with the backend Python service through APIs, including podcast generation, status queries, and audio streaming.
### 🚀 Quick Start (Web)
1. **Install Node.js**: Please ensure Node.js is installed on your system (LTS version recommended).
2. **Install Dependencies**: Enter the `web/` directory and install all frontend dependencies.
```bash
cd web/
npm install
# or yarn install
```
3. **Start Development Server**:
```bash
npm run dev
# or yarn dev
```
The web application will start at `http://localhost:3000` (default).
4. **Build Production Environment**:
```bash
npm run build
# or yarn build
npm run start
# or yarn start
```
### 🐳 Docker Deployment
This project supports deployment via Docker. For detailed information, please refer to [Docker Usage Guide](DOCKER_USAGE.md).
---
## ⚙️ Configuration File Details
### `config/[tts-provider].json` (TTS Character and Voice Configuration)
@@ -253,15 +297,29 @@ You can find sample podcast audio generated using different TTS services in the
├── config/ # ⚙️ Configuration directory
│ ├── doubao-tts.json # ... (configuration for each TTS provider)
│ └── tts_providers.json # Unified TTS authentication file
├── server/ # 🐍 Backend service directory
│ ├── main.py # FastAPI Web API entry: Provides RESTful APIs for podcast generation, status query, audio download, manages task lifecycle, and performs data cleanup.
│ ├── podcast_generator.py # Core podcast generation logic: Responsible for interacting with OpenAI API to generate podcast scripts, calling TTS adapters to convert text to speech, and using FFmpeg to merge audio files.
│ ├── tts_adapters.py # TTS adapter: Encapsulates interaction logic with different TTS services (such as Index-TTS, Edge-TTS, Doubao, Minimax, Fish Audio, Gemini).
│ ├── openai_cli.py # OpenAI command-line tool
│ └── ... # Other backend files
├── web/ # 🌐 Frontend Web Application Directory (Next.js)
│ ├── public/ # Static resources
│ ├── src/ # Source code
│ │ ├── app/ # Next.js route pages
│ │ ├── components/ # React components
│ │ ├── hooks/ # React Hooks
│ │ ├── lib/ # Library files (authentication, database, API, etc.)
│ │ └── types/ # TypeScript type definitions
│ ├── package.json # Frontend dependencies
│ ├── next.config.js # Next.js configuration
│ └── ... # Other frontend files
├── prompt/ # 🧠 AI prompt directory
│ ├── prompt-overview.txt
│ └── prompt-podscript.txt
├── example/ # 🎧 Sample audio directory
├── output/ # 🎉 Output audio directory
├── input.txt # 🎙️ Podcast topic input file
├── openai_cli.py # OpenAI command-line tool
├── podcast_generator.py # 🚀 Main script
├── tts_adapters.py # TTS adapter file
├── README.md # 📄 Project documentation (Chinese)
└── README_EN.md # 📄 Project documentation (English)
```

33
docker-compose.yml Normal file
View File

@@ -0,0 +1,33 @@
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile-Web
image: simple-podcast-web
ports:
- "3200:3000"
volumes:
- audio-data:/app/server/output
restart: always
depends_on:
- server
environment:
- NEXT_PUBLIC_API_URL=http://server:8000
server:
build:
context: .
dockerfile: Dockerfile-Server
image: simple-podcast-server
ports:
- "3100:8000"
volumes:
- audio-data:/app/server/output
restart: always
environment:
- PODCAST_API_SECRET_KEY=your-production-api-secret-key
volumes:
audio-data:

View File

@@ -0,0 +1,150 @@
import os
import re
import sys
from collections import defaultdict
# 假设的项目根目录,用于判断是否为内部模块
PROJECT_ROOT = os.getcwd()
# Python 内置库的简单列表 (可以根据需要扩展)
# 这是一个不完全列表,但包含了常见的内置库
BUILTIN_LIBS = {
"os", "sys", "json", "time", "argparse", "uuid", "hashlib", "hmac",
"enum", "shutil", "random", "threading", "contextlib", "io", "base64",
"datetime", "glob", "subprocess", "urllib", "re", "abc", "typing",
"concurrent", "collections", "wave"
}
def is_builtin_or_standard_library(module_name):
"""
判断模块是否为 Python 内置库或标准库。
这里仅做一个简单的基于名称的判断。
更精确的判断需要检查 sys.builtin_module_names 或 sys.stdlib_module_names
但会使脚本复杂化,对于当前任务,基于列表判断已足够。
"""
return module_name.lower() in BUILTIN_LIBS
def is_project_internal_module(module_name, project_root_path, py_files_paths):
"""
判断模块是否为项目内部模块。
通过检查模块名是否对应项目中的某个Python文件或包。
"""
# 转换为可能的相对路径形式
module_path_parts = module_name.replace('.', os.sep)
for py_file_path in py_files_paths:
# 检查是否为直接导入的文件 (例如: from podcast_generator import ...)
if py_file_path.endswith(f"{module_path_parts}.py"):
return True
# 检查是否为包导入 (例如: from check.check_doubao_voices import ...)
if os.path.isdir(os.path.join(project_root_path, module_path_parts)) and \
os.path.exists(os.path.join(project_root_path, module_path_parts, "__init__.py")):
return True
return False
def extract_dependencies(file_path, project_root_path, all_py_files):
"""从Python文件中提取第三方依赖"""
dependencies = set()
internal_modules = set()
# 将所有Python文件的路径转换为集合方便查找
all_py_files_set = set(all_py_files)
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 匹配 import module 和 from package import module
# 这里我们只关心顶级模块名
for match in re.finditer(r"^(?:import|from)\s+([a-zA-Z0-9_.]+)", content, re.MULTILINE):
full_module_name = match.group(1).split('.')[0] # 获取顶级模块名
if is_builtin_or_standard_library(full_module_name):
continue # 跳过内置库
# 检查是否为项目内部模块
# 为了提高准确性这里需要将py_files_paths传递给 is_project_internal_module
if is_project_internal_module(full_module_name, project_root_path, all_py_files_set):
internal_modules.add(full_module_name)
continue
dependencies.add(full_module_name)
return dependencies, internal_modules
def find_all_py_files(directory):
"""递归查找指定目录下所有.py文件"""
py_files = []
for root, _, files in os.walk(directory):
for file in files:
if file.endswith(".py"):
py_files.append(os.path.relpath(os.path.join(root, file), start=directory))
return py_files
def main():
print("开始提取 Python 项目依赖...")
all_py_files = find_all_py_files(PROJECT_ROOT)
all_external_dependencies = set()
all_internal_modules = set()
for py_file in all_py_files:
print(f"处理文件: {py_file}")
try:
current_dependencies, current_internal_modules = extract_dependencies(py_file, PROJECT_ROOT, all_py_files)
all_external_dependencies.update(current_dependencies)
all_internal_modules.update(current_internal_modules)
except Exception as e:
print(f"处理文件 {py_file} 时出错: {e}", file=sys.stderr)
# 某些库名可能需要映射到 pip 包名
# 例如PIL 导入为 PIL 但包名是 Pillow
# httpx 导入为 httpx, 包名也是 httpx
# starlette 导入为 starlette包名也是 starlette
# fastapi 导入为 fastapi包名也是 fastapi
# uvicorn 导入为 uvicorn包名也是 uvicorn
# openai 导入为 openai包名也是 openai
# msgpack 导入为 msgpack包名是 msgpack
# pydub 导入为 pydub包名是 pydub
# requests 导入为 requests, 包名也是 requests
# schedule 导入为 schedule包名是 schedule
dependency_mapping = {
"PIL": "Pillow",
"fastapi": "fastapi",
"starlette": "starlette",
"httpx": "httpx",
"schedule": "schedule",
"uvicorn": "uvicorn",
"openai": "openai",
"msgpack": "msgpack",
"pydub": "pydub",
"requests": "requests",
}
final_dependencies = set()
for dep in all_external_dependencies:
final_dependencies.add(dependency_mapping.get(dep, dep))
# 手动添加一些可能未通过 import 语句捕获的依赖,或者需要特定版本的依赖
# 这部分通常需要根据项目实际情况调整
# 例如:
# final_dependencies.add("uvicorn[standard]") # 如果使用了 uvicorn 的标准安装
# final_dependencies.add("fastapi[all]") # 如果使用了 FastAPI 的所有可选依赖
output_file = "requirements.txt"
with open(output_file, 'w', encoding='utf-8') as f:
for dep in sorted(list(final_dependencies)):
f.write(f"{dep}\n")
print(f"\n提取完成。所有第三方依赖已写入 {output_file}")
print("\n检测到的第三方依赖:")
for dep in sorted(list(final_dependencies)):
print(f"- {dep}")
print("\n检测到的项目内部模块 (供参考):")
for mod in sorted(list(all_internal_modules)):
print(f"- {mod}")
if __name__ == "__main__":
main()

10
server/requirements.txt Normal file
View File

@@ -0,0 +1,10 @@
Pillow==11.2.1
fastapi==0.104.1
httpx==0.25.2
msgpack==1.1.0
openai==1.79.0
requests==2.32.3
schedule==1.2.2
starlette==0.27.0
uvicorn==0.24.0
python-multipart==0.0.20

47
web/.dockerignore Normal file
View File

@@ -0,0 +1,47 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Node.js
node_modules
.next/cache
dist
.pnp
.pnp.js
# Testing
/coverage
# Production
/build
# Misc
.DS_Store
*.pem
# Vercel
.vercel
# TypeScript
*.tsbuildinfo
next-env.d.ts
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
Thumbs.db
drizzle
.claude/
/.public/
# Docker specific
Dockerfile
.dockerignore

142
web/init.sql Normal file
View File

@@ -0,0 +1,142 @@
--
-- SQLiteStudio v3.4.17 生成的文件,周三 8月 20 17:58:34 2025
--
-- 所用的文本编码System
--
PRAGMA foreign_keys = off;
BEGIN TRANSACTION;
-- 表__drizzle_migrations
CREATE TABLE IF NOT EXISTS __drizzle_migrations (
id SERIAL PRIMARY KEY,
hash TEXT NOT NULL,
created_at NUMERIC
);
-- 表account
CREATE TABLE IF NOT EXISTS account (
id TEXT PRIMARY KEY
NOT NULL,
account_id TEXT NOT NULL,
provider_id TEXT NOT NULL,
user_id TEXT NOT NULL,
access_token TEXT,
refresh_token TEXT,
id_token TEXT,
access_token_expires_at INTEGER,
refresh_token_expires_at INTEGER,
scope TEXT,
password TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (
user_id
)
REFERENCES user (id) ON UPDATE NO ACTION
ON DELETE CASCADE
);
-- 表points_accounts
CREATE TABLE IF NOT EXISTS points_accounts (-- 账户ID使用自增整数作为主键效率最高。
account_id INTEGER PRIMARY KEY AUTOINCREMENT,-- 关联的用户ID设置为 TEXT 类型以匹配您的设计。
/* 添加 UNIQUE 约束确保一个用户只有一个积分账户。 */user_id TEXT NOT NULL
UNIQUE,-- 当前总积分,非负,默认为 0。
total_points INTEGER NOT NULL
DEFAULT 0
CHECK (total_points >= 0),-- 最后更新时间,使用 TEXT 存储 ISO8601 格式的日期时间。
/* 在记录更新时,应由应用程序逻辑来更新此字段。 */updated_at TEXT NOT NULL
);
-- 表points_transactions
CREATE TABLE IF NOT EXISTS points_transactions (-- 流水ID使用自增整数主键。
transaction_id INTEGER PRIMARY KEY AUTOINCREMENT,-- 关联的用户ID。
user_id TEXT NOT NULL,-- 本次变动的积分数,正数代表增加,负数代表减少。
points_change INTEGER NOT NULL,-- 变动原因代码,方便程序进行逻辑判断。
reason_code TEXT NOT NULL,-- 变动原因的文字描述,可为空。
description TEXT,-- 记录创建时间,默认为当前时间戳。
created_at TEXT NOT NULL
DEFAULT CURRENT_TIMESTAMP
);
-- 表session
CREATE TABLE IF NOT EXISTS session (
id TEXT PRIMARY KEY
NOT NULL,
expires_at INTEGER NOT NULL,
token TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
ip_address TEXT,
user_agent TEXT,
user_id TEXT NOT NULL,
FOREIGN KEY (
user_id
)
REFERENCES user (id) ON UPDATE NO ACTION
ON DELETE CASCADE
);
-- 表user
CREATE TABLE IF NOT EXISTS user (
id TEXT PRIMARY KEY
NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
email_verified INTEGER NOT NULL,
image TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
username TEXT,
display_username TEXT
);
-- 表verification
CREATE TABLE IF NOT EXISTS verification (
id TEXT PRIMARY KEY
NOT NULL,
identifier TEXT NOT NULL,
value TEXT NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER,
updated_at INTEGER
);
-- 索引idx_points_accounts_user_id
CREATE INDEX IF NOT EXISTS idx_points_accounts_user_id ON points_accounts (
user_id
);
-- 索引idx_points_transactions_user_id
CREATE INDEX IF NOT EXISTS idx_points_transactions_user_id ON points_transactions (
user_id
);
-- 索引session_token_unique
CREATE UNIQUE INDEX IF NOT EXISTS session_token_unique ON session (
token
);
-- 索引user_email_unique
CREATE UNIQUE INDEX IF NOT EXISTS user_email_unique ON user (
email
);
-- 索引user_username_unique
CREATE UNIQUE INDEX IF NOT EXISTS user_username_unique ON user (
username
);
COMMIT TRANSACTION;
PRAGMA foreign_keys = on;

View File

@@ -8,6 +8,7 @@ const nextConfig = {
compiler: {
removeConsole: process.env.NODE_ENV === 'production',
},
output: 'standalone',
};
module.exports = nextConfig;

78
web/package-lock.json generated
View File

@@ -24,6 +24,7 @@
"dotenv": "^17.2.1",
"drizzle-orm": "^0.44.4",
"framer-motion": "^11.3.8",
"globby": "^14.1.0",
"lucide-react": "^0.424.0",
"next": "^14.2.5",
"postcss": "^8.4.40",
@@ -2520,6 +2521,18 @@
"node": ">=20.0.0"
}
},
"node_modules/@sindresorhus/merge-streams": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
"integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@@ -6000,6 +6013,35 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/globby": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz",
"integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==",
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^2.1.0",
"fast-glob": "^3.3.3",
"ignore": "^7.0.3",
"path-type": "^6.0.0",
"slash": "^5.1.0",
"unicorn-magic": "^0.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globby/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/goober": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
@@ -8234,6 +8276,18 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-type": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz",
"integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -9432,6 +9486,18 @@
"simple-concat": "^1.0.0"
}
},
"node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
@@ -10336,6 +10402,18 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unified": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",

BIN
web/sqlite.db Normal file

Binary file not shown.

View File

@@ -8,6 +8,9 @@ import { AiOutlineTikTok, AiFillQqCircle, AiOutlineGithub, AiOutlineTwitter, AiF
export const metadata: Metadata = {
title: '联系我们 - PodcastHub',
description: '有任何问题或建议?请随时联系 PodcastHub 团队。我们期待您的声音。',
alternates: {
canonical: '/contact',
},
};
/**

View File

@@ -10,8 +10,9 @@ const inter = Inter({
});
export const metadata: Metadata = {
title: 'PodcastHub - 给创意一个真实的声音',
description: '使用AI技术将您的想法和内容转换为高质量播客音频,支持多种语音和风格选择。',
metadataBase: new URL('https://www.podcasthub.com'),
title: 'PodcastHub: 您的AI播客创作平台 - 轻松将文字转化为高质量播客音频,支持多种语音和风格,让创意触手可及',
description: 'PodcastHub 利用尖端AI技术为您的创意提供无限可能。轻松将文字和想法转化为专业品质的播客音频支持多种个性化语音和风格选择。立即体验高效创作让您的声音在全球范围内传播吸引更多听众并简化您的播客制作流程。',
keywords: ['播客', 'AI', '语音合成', 'TTS', '音频生成'],
authors: [{ name: 'PodcastHub Team' }],
icons: {
@@ -19,11 +20,18 @@ export const metadata: Metadata = {
apple: '/favicon.webp',
},
openGraph: {
title: 'PodcastHub - 给创意一个真实的声音',
description: '使用AI技术将您的想法和内容转换为高质量的播客音频,支持多种语音和风格选择。',
title: 'PodcastHub: 您的AI播客创作平台 - 轻松将文字转化为高质量播客音频,支持多种语音和风格,让创意触手可及',
description: 'PodcastHub 利用尖端AI技术为您的创意提供无限可能。轻松将文字和想法转化为专业品质的播客音频,支持多种个性化语音和风格选择。立即体验高效创作,让您的声音在全球范围内传播,吸引更多听众,并简化您的播客制作流程。',
type: 'website',
locale: 'zh_CN',
},
twitter: {
card: 'summary_large_image',
title: 'PodcastHub: 您的AI播客创作平台 - 轻松将文字转化为高质量播客音频,支持多种语音和风格,让创意触手可及',
},
alternates: {
canonical: '/',
},
};
export const viewport = {

View File

@@ -1,5 +1,20 @@
import { Metadata } from 'next';
import PodcastContent from '@/components/PodcastContent';
export async function generateMetadata({ params }: PodcastDetailPageProps): Promise<Metadata> {
const fileName = decodeURIComponent(params.fileName);
const title = `播客详情 - ${fileName}`;
const description = `收听 ${fileName} 的播客。`;
return {
title,
description,
alternates: {
canonical: `/podcast/${fileName}`,
},
};
}
interface PodcastDetailPageProps {
params: {
fileName: string;

View File

@@ -1,12 +1,20 @@
'use client';
import React from 'react';
import PricingSection from '@/components/PricingSection'; // 导入 PricingSection 组件
const PricingPage: React.FC = () => {
return (
<PricingSection />
);
import { Metadata } from 'next';
import React from 'react';
import PricingSection from '@/components/PricingSection'; // 导入 PricingSection 组件
export const metadata: Metadata = {
title: '定价 - PodcastHub',
description: '查看 PodcastHub 的灵活定价方案,找到最适合您的播客创作计划。',
alternates: {
canonical: '/pricing',
},
};
export default PricingPage;
const PricingPage: React.FC = () => {
return (
<PricingSection />
);
};
export default PricingPage;

View File

@@ -7,6 +7,9 @@ import { Metadata } from 'next';
export const metadata: Metadata = {
title: '隐私政策 - PodcastHub',
description: '了解 PodcastHub 如何保护您的隐私。我们致力于透明化地处理您的数据。',
alternates: {
canonical: '/privacy',
},
};
/**

View File

@@ -7,6 +7,9 @@ import { Metadata } from 'next';
export const metadata: Metadata = {
title: '使用条款 - PodcastHub',
description: '欢迎了解 PodcastHub 的使用条款。本条款旨在保护用户与平台的共同利益。',
alternates: {
canonical: '/terms',
},
};
/**

View File

@@ -16,8 +16,10 @@ import {
import { cn } from '@/lib/utils';
import ConfigSelector from './ConfigSelector';
import VoicesModal from './VoicesModal'; // 引入 VoicesModal
import LoginModal from './LoginModal'; // 引入 LoginModal
import { useToast, ToastContainer } from './Toast'; // 引入 Toast Hook 和 Container
import { setItem, getItem } from '@/lib/storage'; // 引入 localStorage 工具
import { useSession } from '@/lib/auth-client'; // 引入 useSession
import type { PodcastGenerationRequest, TTSConfig, Voice, SettingsFormData } from '@/types';
import { Satisfy } from 'next/font/google'; // 导入艺术字体 Satisfy
@@ -72,6 +74,7 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
const [language, setLanguage] = useState(languageOptions[0].value);
const [duration, setDuration] = useState(durationOptions[0].value);
const [showVoicesModal, setShowVoicesModal] = useState(false); // 新增状态
const [showLoginModal, setShowLoginModal] = useState(false); // 控制登录模态框的显示
const [voices, setVoices] = useState<Voice[]>([]); // 从 ConfigSelector 获取 voices
const [selectedPodcastVoices, setSelectedPodcastVoices] = useState<{[key: string]: Voice[]}>(() => {
// 从 localStorage 读取缓存的说话人配置
@@ -83,8 +86,13 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
const fileInputRef = useRef<HTMLInputElement>(null);
const { toasts, error } = useToast(); // 使用 useToast hook, 引入 success
const { data: session } = useSession(); // 获取 session
const handleSubmit = async () => { // 修改为 async 函数
if (!session?.user) { // 判断是否登录
setShowLoginModal(true); // 未登录则显示登录模态框
return;
}
if (!topic.trim()) {
error("主题不能为空", "请输入播客主题。"); // 使用 toast.error
return;
@@ -206,9 +214,9 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
</g>
</svg>
</div>
<h2 className="text-2xl sm:text-3xl text-black mb-6 break-words">
<h1 className="text-2xl sm:text-3xl text-black mb-6 break-words">
</h2>
</h1>
{/* 模式切换按钮 todo */}
{/* <div className="flex items-center justify-center gap-2 sm:gap-4 mb-8 flex-wrap">
@@ -387,10 +395,10 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
{/* 创作按钮 */}
<button
onClick={handleSubmit}
disabled={!topic.trim() || isGenerating || credits <= 0}
disabled={!topic.trim() || isGenerating}
className={cn(
"btn-primary flex items-center gap-1 text-sm sm:text-base px-3 py-2 sm:px-4 sm:py-2",
(!topic.trim() || isGenerating || credits <= 0) && "opacity-50 cursor-not-allowed"
(!topic.trim() || isGenerating) && "opacity-50 cursor-not-allowed"
)}
>
{isGenerating ? (
@@ -440,6 +448,11 @@ const PodcastCreator: React.FC<PodcastCreatorProps> = ({
}}
/>
)}
{/* Login Modal */}
<LoginModal
isOpen={showLoginModal}
onClose={() => setShowLoginModal(false)}
/>
<ToastContainer toasts={toasts} onRemove={() => {}} /> {/* 添加 ToastContainer */}
</div>