From 043b0e39f86a13e0e28fbf4e00bda9ccbdf9a316 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Thu, 21 Aug 2025 17:59:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Docker=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=B9=B6=E4=BC=98=E5=8C=96SEO=E5=92=8C=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 重构页面元数据以支持SEO规范链接 feat(web): 实现用户积分系统和登录验证 docs: 添加Docker使用指南和更新README build: 添加Docker相关配置文件和脚本 chore: 更新依赖项并添加初始化SQL文件 --- DOCKER_USAGE.md | 125 ++++++++++++++++++++ Dockerfile-Server | 30 +++++ Dockerfile-Web | 51 ++++++++ README.md | 66 ++++++++++- README_EN.md | 64 +++++++++- docker-compose.yml | 33 ++++++ server/extract_dependencies.py | 150 ++++++++++++++++++++++++ server/requirements.txt | 10 ++ web/.dockerignore | 47 ++++++++ web/init.sql | 142 ++++++++++++++++++++++ web/next.config.js | 1 + web/package-lock.json | 78 ++++++++++++ web/sqlite.db | Bin 0 -> 90112 bytes web/src/app/contact/page.tsx | 3 + web/src/app/layout.tsx | 16 ++- web/src/app/podcast/[fileName]/page.tsx | 15 +++ web/src/app/pricing/page.tsx | 30 +++-- web/src/app/privacy/page.tsx | 3 + web/src/app/terms/page.tsx | 3 + web/src/components/PodcastCreator.tsx | 21 +++- 20 files changed, 862 insertions(+), 26 deletions(-) create mode 100644 DOCKER_USAGE.md create mode 100644 Dockerfile-Server create mode 100644 Dockerfile-Web create mode 100644 docker-compose.yml create mode 100644 server/extract_dependencies.py create mode 100644 server/requirements.txt create mode 100644 web/.dockerignore create mode 100644 web/init.sql create mode 100644 web/sqlite.db diff --git a/DOCKER_USAGE.md b/DOCKER_USAGE.md new file mode 100644 index 0000000..f16b513 --- /dev/null +++ b/DOCKER_USAGE.md @@ -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 应用。 \ No newline at end of file diff --git a/Dockerfile-Server b/Dockerfile-Server new file mode 100644 index 0000000..4f4f20e --- /dev/null +++ b/Dockerfile-Server @@ -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"] \ No newline at end of file diff --git a/Dockerfile-Web b/Dockerfile-Web new file mode 100644 index 0000000..8e99b0d --- /dev/null +++ b/Dockerfile-Web @@ -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"] \ No newline at end of file diff --git a/README.md b/README.md index 35db70d..7744ff2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🎙️ 简易播客生成器 (Simple Podcast Generator) +# 🎙️ 播客生成器 (Podcast Generator) > 轻松将您的想法,一键生成为生动有趣的多人对话播客! > [English Version](README_EN.md) @@ -73,6 +73,8 @@ python podcast_generator.py [可选参数] * `--base-url `: OpenAI API 的代理地址。若不提供,将从配置文件或 `OPENAI_BASE_URL` 环境变量中读取。 * `--model `: 指定使用的 OpenAI 模型(如 `gpt-4o`, `gpt-4-turbo`)。默认值为 `gpt-3.5-turbo`。 * `--threads `: 指定生成音频的并行线程数(默认为 `1`),提高处理速度。 +* `--output-language `: 指定播客脚本的输出语言(默认为 `Chinese`)。 +* `--usetime `: 指定播客脚本的时间长度(默认为 `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 # 📄 项目说明文档 (英文) ``` diff --git a/README_EN.md b/README_EN.md index 02eb8bf..c513630 100644 --- a/README_EN.md +++ b/README_EN.md @@ -73,6 +73,8 @@ python podcast_generator.py [optional parameters] * `--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 `: Specify the OpenAI model to use (such as `gpt-4o`, `gpt-4-turbo`). Default value is `gpt-3.5-turbo`. * `--threads `: Specify the number of parallel threads for audio generation (default is `1`) to improve processing speed. +* `--output-language `: Specify the output language of the podcast script (default is `Chinese`). +* `--usetime `: 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) ``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d0ba904 --- /dev/null +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/server/extract_dependencies.py b/server/extract_dependencies.py new file mode 100644 index 0000000..7d27ab9 --- /dev/null +++ b/server/extract_dependencies.py @@ -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() \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..7bfdcae --- /dev/null +++ b/server/requirements.txt @@ -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 \ No newline at end of file diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..9f06dbc --- /dev/null +++ b/web/.dockerignore @@ -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 \ No newline at end of file diff --git a/web/init.sql b/web/init.sql new file mode 100644 index 0000000..f5bbd05 --- /dev/null +++ b/web/init.sql @@ -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; diff --git a/web/next.config.js b/web/next.config.js index fa5f6d3..f1ce147 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -8,6 +8,7 @@ const nextConfig = { compiler: { removeConsole: process.env.NODE_ENV === 'production', }, + output: 'standalone', }; module.exports = nextConfig; \ No newline at end of file diff --git a/web/package-lock.json b/web/package-lock.json index 72de57e..c496ddb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/sqlite.db b/web/sqlite.db new file mode 100644 index 0000000000000000000000000000000000000000..910002841dcf2a1018ba0c80dff5e8bcf516ea44 GIT binary patch literal 90112 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 AtN;K2 literal 0 HcmV?d00001 diff --git a/web/src/app/contact/page.tsx b/web/src/app/contact/page.tsx index bf17bdb..479dece 100644 --- a/web/src/app/contact/page.tsx +++ b/web/src/app/contact/page.tsx @@ -8,6 +8,9 @@ import { AiOutlineTikTok, AiFillQqCircle, AiOutlineGithub, AiOutlineTwitter, AiF export const metadata: Metadata = { title: '联系我们 - PodcastHub', description: '有任何问题或建议?请随时联系 PodcastHub 团队。我们期待您的声音。', + alternates: { + canonical: '/contact', + }, }; /** diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index bbbed9f..2197329 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -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 = { diff --git a/web/src/app/podcast/[fileName]/page.tsx b/web/src/app/podcast/[fileName]/page.tsx index 3f77a3a..ecc0a12 100644 --- a/web/src/app/podcast/[fileName]/page.tsx +++ b/web/src/app/podcast/[fileName]/page.tsx @@ -1,5 +1,20 @@ +import { Metadata } from 'next'; import PodcastContent from '@/components/PodcastContent'; +export async function generateMetadata({ params }: PodcastDetailPageProps): Promise { + const fileName = decodeURIComponent(params.fileName); + const title = `播客详情 - ${fileName}`; + const description = `收听 ${fileName} 的播客。`; + + return { + title, + description, + alternates: { + canonical: `/podcast/${fileName}`, + }, + }; +} + interface PodcastDetailPageProps { params: { fileName: string; diff --git a/web/src/app/pricing/page.tsx b/web/src/app/pricing/page.tsx index a15dbeb..460f16e 100644 --- a/web/src/app/pricing/page.tsx +++ b/web/src/app/pricing/page.tsx @@ -1,12 +1,20 @@ -'use client'; - -import React from 'react'; -import PricingSection from '@/components/PricingSection'; // 导入 PricingSection 组件 - -const PricingPage: React.FC = () => { - return ( - - ); +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; \ No newline at end of file + + const PricingPage: React.FC = () => { + return ( + + ); + }; + + export default PricingPage; \ No newline at end of file diff --git a/web/src/app/privacy/page.tsx b/web/src/app/privacy/page.tsx index ad8f26f..591a877 100644 --- a/web/src/app/privacy/page.tsx +++ b/web/src/app/privacy/page.tsx @@ -7,6 +7,9 @@ import { Metadata } from 'next'; export const metadata: Metadata = { title: '隐私政策 - PodcastHub', description: '了解 PodcastHub 如何保护您的隐私。我们致力于透明化地处理您的数据。', + alternates: { + canonical: '/privacy', + }, }; /** diff --git a/web/src/app/terms/page.tsx b/web/src/app/terms/page.tsx index 6eda44b..028d540 100644 --- a/web/src/app/terms/page.tsx +++ b/web/src/app/terms/page.tsx @@ -7,6 +7,9 @@ import { Metadata } from 'next'; export const metadata: Metadata = { title: '使用条款 - PodcastHub', description: '欢迎了解 PodcastHub 的使用条款。本条款旨在保护用户与平台的共同利益。', + alternates: { + canonical: '/terms', + }, }; /** diff --git a/web/src/components/PodcastCreator.tsx b/web/src/components/PodcastCreator.tsx index 867f5d6..f4e0d03 100644 --- a/web/src/components/PodcastCreator.tsx +++ b/web/src/components/PodcastCreator.tsx @@ -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 = ({ 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([]); // 从 ConfigSelector 获取 voices const [selectedPodcastVoices, setSelectedPodcastVoices] = useState<{[key: string]: Voice[]}>(() => { // 从 localStorage 读取缓存的说话人配置 @@ -83,8 +86,13 @@ const PodcastCreator: React.FC = ({ const fileInputRef = useRef(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 = ({ -

+

给创意一个真实的声音 -

+ {/* 模式切换按钮 todo */} {/*
@@ -387,10 +395,10 @@ const PodcastCreator: React.FC = ({ {/* 创作按钮 */}