diff --git a/main.py b/main.py
index eb5f1c0..76fc73d 100644
--- a/main.py
+++ b/main.py
@@ -74,7 +74,7 @@ stop_scheduler_event = threading.Event()
# 全局配置
output_dir = "output"
-time_after = 30
+time_after = 10
# 内存中存储任务结果
# {task_id: {"auth_id": auth_id, "status": TaskStatus, "result": any, "timestamp": float}}
@@ -96,15 +96,60 @@ tts_provider_map = {
# 定义一个函数来清理输出目录
def clean_output_directory():
- """Removes files from the output directory that are older than 30 minutes."""
- print(f"Cleaning output directory: {output_dir}")
+ """
+ 清理 output 目录中的旧文件以及 task_results 中过期的任务。
+ 优先清理过期的任务及其关联文件,确保内存和文件系统同步。
+ """
+ print(f"Cleaning output directory and expired tasks from memory: {output_dir}")
now = time.time()
- # 30 minutes in seconds
- threshold = time_after * 60
+ threshold = time_after * 60 # 清理阈值,单位秒
- # 存储需要删除的 task_results 中的任务,避免在迭代时修改
- tasks_to_remove_from_memory = []
+ # 第一阶段:清理 task_results 中已完成且过期的任务及其关联文件
+ # 使用 list() 创建副本以安全地在迭代时修改原始字典
+ auth_ids_to_clean = []
+ for auth_id, tasks_by_auth in list(task_results.items()):
+ task_ids_to_clean = []
+ for task_id, task_info in list(tasks_by_auth.items()):
+ # 只要 timestamp 过期,无论任务状态如何,都进行清理
+ if (now - task_info["timestamp"] > threshold):
+ task_ids_to_clean.append(task_id)
+
+ # 尝试删除对应的音频文件
+ output_audio_filepath = task_info.get("output_audio_filepath")
+ if output_audio_filepath:
+ full_audio_path = os.path.join(output_dir, output_audio_filepath)
+ try:
+ if os.path.isfile(full_audio_path):
+ os.unlink(full_audio_path)
+ print(f"Deleted expired audio file: {full_audio_path}")
+ else:
+ print(f"Expired task {task_id} audio file {full_audio_path} not found or is not a file.")
+ except Exception as e:
+ print(f"Failed to delete audio file {full_audio_path}. Reason: {e}")
+ # 从 audio_file_mapping 中删除对应的条目
+ filename_without_ext = os.path.splitext(output_audio_filepath)[0] if output_audio_filepath else None
+ if filename_without_ext and filename_without_ext in audio_file_mapping:
+ del audio_file_mapping[filename_without_ext]
+ print(f"Removed audio_file_mapping entry for {filename_without_ext}.")
+
+ # 清理 task_results 中的任务
+ for task_id in task_ids_to_clean:
+ if task_id in task_results[auth_id]:
+ del task_results[auth_id][task_id]
+ print(f"Removed expired task {task_id} for auth_id {auth_id} from task_results.")
+
+ # 如果该 auth_id 下没有其他任务,则删除 auth_id 的整个条目
+ if not task_results[auth_id]:
+ auth_ids_to_clean.append(auth_id)
+
+ for auth_id in auth_ids_to_clean:
+ if auth_id in task_results:
+ del task_results[auth_id]
+ print(f"Removed empty auth_id {auth_id} from task_results.")
+
+ # 第二阶段:清理 output 目录中可能未被任务关联的孤立文件
+ # 或者那些任务还未过期,但文件因为某种原因在内存任务清理阶段没有被删除的文件
for filename in os.listdir(output_dir):
file_path = os.path.join(output_dir, filename)
try:
@@ -112,41 +157,13 @@ def clean_output_directory():
# 获取最后修改时间
if now - os.path.getmtime(file_path) > threshold:
os.unlink(file_path)
- print(f"Deleted old file: {file_path}")
-
- # 遍历 task_results,查找匹配的文件名
- # 注意:task_info["output_audio_filepath"] 存储的是文件名,不是完整路径
- for auth_id, tasks_by_auth in task_results.items():
- # 使用 list() 创建副本,以安全地在循环中删除元素
- for task_id, task_info in list(tasks_by_auth.items()):
- # 检查文件名是否匹配,并且任务状态为 COMPLETED
- # 只有 COMPLETED 的任务才应该被清理,PENDING/RUNNING 任务的输出文件可能还未生成或正在使用
- task_output_filename = task_info.get("output_audio_filepath")
- if task_output_filename == filename and task_info["status"] == TaskStatus.COMPLETED:
- tasks_to_remove_from_memory.append((auth_id, task_id, task_info))
+ print(f"Deleted old unassociated file: {file_path}")
elif os.path.isdir(file_path):
# 可选地,递归删除旧的子目录或其中的文件
- # 目前只跳过目录
pass
except Exception as e:
print(f"Failed to delete {file_path}. Reason: {e}")
- # 在文件删除循环结束后统一处理 task_results 的删除
- for auth_id, task_id, task_info_to_remove in tasks_to_remove_from_memory:
- if auth_id in task_results and task_id in task_results[auth_id]:
- del task_results[auth_id][task_id]
- print(f"Removed task {task_id} for auth_id {auth_id} from task_results.")
- # 从 audio_file_mapping 中删除对应的条目
- task_output_filename = task_info_to_remove.get("output_audio_filepath")
- if task_output_filename and task_output_filename in audio_file_mapping:
- del audio_file_mapping[task_output_filename]
- print(f"Removed audio_file_mapping entry for {task_output_filename}.")
- # 如果该 auth_id 下没有其他任务,则删除 auth_id 的整个条目
- if not task_results[auth_id]:
- del task_results[auth_id]
- print(f"Removed empty auth_id {auth_id} from task_results.")
-
-
async def get_auth_id(x_auth_id: str = Header(..., alias="X-Auth-Id")):
"""
从头部获取 X-Auth-Id 的依赖项。
diff --git a/openai_cli.py b/openai_cli.py
index 0c554e3..0990125 100644
--- a/openai_cli.py
+++ b/openai_cli.py
@@ -20,26 +20,29 @@ OpenAI CLI - 纯命令行OpenAI接口调用工具
{
"api_key": "你的API密钥",
"base_url": "https://api.openai.com/v1",
- "model": "gpt-3.5-turbo"
+ "model": "gpt-3.5-turbo",
+ "temperature": 0.7,
+ "top_p": 1.0
}
然后通过 --config config.json 加载
3. 运行脚本:
- - 交互式聊天模式:
- python openai_cli.py [可选参数: --api-key VAL --base-url VAL --model VAL]
- 在交互模式中,输入 'quit' 或 'exit' 退出,输入 'clear' 清空对话历史。
+ - 交互式聊天模式:
+ python openai_cli.py [可选参数: --api-key VAL --base-url VAL --model VAL --temperature VAL --top-p VAL]
+ 在交互模式中,输入 'quit' 或 'exit' 退出,输入 'clear' 清空对话历史。
- - 单次查询模式:
- python openai_cli.py --query "你的问题" [可选参数: --api-key VAL --base-url VAL --model VAL --temperature VAL --max-tokens VAL --system-message VAL]
+ - 单次查询模式:
+ python openai_cli.py --query "你的问题" [可选参数: --api-key VAL --base-url VAL --model VAL --temperature VAL --top-p VAL --max-tokens VAL --system-message VAL]
- - 使用配置文件:
- python openai_cli.py --config config.json --query "你的问题"
+ - 使用配置文件:
+ python openai_cli.py --config config.json --query "你的问题"
示例:
- python openai_cli.py
- python openai_cli.py -q "你好,世界" -m gpt-4
- python openai_cli.py --config my_config.json
+ python openai_cli.py
+ python openai_cli.py -q "你好,世界" -m gpt-4
+ python openai_cli.py -q "你好,世界" --temperature 0.8 --top-p 0.9
+ python openai_cli.py --config my_config.json
"""
import argparse
@@ -72,7 +75,7 @@ class OpenAICli:
self.client = openai.OpenAI(api_key=self.api_key, base_url=effective_base_url)
- def chat_completion(self, messages: List[ChatCompletionMessageParam], temperature: float = 0.7, max_tokens: Optional[int] = None) -> Any:
+ def chat_completion(self, messages: List[ChatCompletionMessageParam], temperature: float = 0.7, top_p: float = 1.0, max_tokens: Optional[int] = None) -> Any:
"""发送聊天完成请求"""
# 处理系统提示词
messages_to_send = list(messages) # 创建一个副本以避免修改原始列表
@@ -94,6 +97,7 @@ class OpenAICli:
model=self.model,
messages=messages_to_send, # 使用包含系统提示词的列表
temperature=temperature,
+ top_p=top_p,
max_tokens=max_tokens,
stream=True
)
@@ -146,14 +150,14 @@ class OpenAICli:
except Exception as e:
print(f"\n❌ 错误: {str(e)}")
- def single_query(self, query: str, temperature: float = 0.7, max_tokens: Optional[int] = None):
+ def single_query(self, query: str, temperature: float = 0.7, top_p: float = 1.0, max_tokens: Optional[int] = None):
"""单次查询模式"""
messages: List[ChatCompletionMessageParam] = []
# 移除此处添加system_message的逻辑,因为它已在chat_completion中处理
messages.append({"role": "user", "content": query})
try:
- response_generator = self.chat_completion(messages, temperature, max_tokens)
+ response_generator = self.chat_completion(messages, temperature, top_p, max_tokens)
for chunk in response_generator:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
@@ -173,7 +177,8 @@ def main():
# 查询参数
parser.add_argument("--query", "-q", help="单次查询的问题")
- parser.add_argument("--temperature", "-t", type=float, default=0.7, help="温度参数 (0.0-2.0)")
+ parser.add_argument("--temperature", "-t", type=float, default=1, help="温度参数 (0.0-2.0)")
+ parser.add_argument("--top-p", type=float, default=0.95, help="Top-p采样参数 (0.0-1.0)")
parser.add_argument("--max-tokens", type=int, help="最大token数")
parser.add_argument("--system-message", "-s", help="系统提示词")
@@ -197,13 +202,15 @@ def main():
base_url = args.base_url or config.get("base_url")
model = args.model or config.get("model", "gpt-3.5-turbo")
system_message = args.system_message or config.get("system_message")
+ temperature = args.temperature or config.get("temperature", 1)
+ top_p = args.top_p or config.get("top_p", 0.95)
try:
cli = OpenAICli(api_key=api_key, base_url=base_url, model=model, system_message=system_message)
if args.query:
# 单次查询模式
- cli.single_query(args.query, args.temperature, args.max_tokens)
+ cli.single_query(args.query, temperature, top_p, args.max_tokens)
else:
# 交互式模式
cli.interactive_chat()
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index 554e456..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "name": "Simple-Podcast-Script",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {}
-}
diff --git a/prompt/prompt-overview.txt b/prompt/prompt-overview.txt
index 3eae941..c2ee841 100644
--- a/prompt/prompt-overview.txt
+++ b/prompt/prompt-overview.txt
@@ -1,19 +1,64 @@
- - {{outlang}}
- - Create a summary of approximately 150 characters based on the core content of the document, without using the title or subtitle. Then, generate a title of approximately 15-20 characters based on the summary, such as "The Four Mirrors of Human Nature: See Through Selfishness and Admiration for Strength, and Live More Transparently." Place this title on the first line of the output.
- - Generate tags based on the document's content, without using the content from titles or subtitles, separated by the # symbol. Generate 3-5 tags. The tags should not be summative; they should be excerpted from words within the document and placed on the second line of the output.
+ **1. Metadata Generation**
+
+ * **Step 1: Intermediate Core Summary Generation (Internal Step)**
+ * **Task**: First, generate a core idea summary of approximately 150 characters based *only* on the **[body content]** of the document (ignoring titles and subtitles).
+ * **Purpose**: This summary is the sole basis for generating the final title and should **not** be displayed in the final output itself.
+
+ * **Step 2: Title Generation**
+ * **Source**: Must be refined from the "core summary" generated in the previous step.
+ * **Length**: Strictly controlled to be between 15-20 characters.
+ * **Format**: Adopt a "Main Title: Subtitle" structure, using a full-width colon ":" for separation. For example: "Brevity and Precision: Practical Engineering for AI Context".
+ * **Position**: As the **first line** of the final output.
+
+ * **Step 3: Tag Generation**
+ * **Source**: Extract from the **[body content]** of the document (ignoring titles and subtitles).
+ * **Quantity**: 3 to 5.
+ * **Format**: Keywords separated by the "#" symbol (e.g., #Keyword1#Keyword2).
+ * **Position**: As the **second line** of the final output.
+
+ **2. Output Language**
+
+ * **{{outlang}}**.
-
- You are an expert document analyst and summarization specialist tasked with distilling complex information into clear,
- comprehensive summaries. Your role is to analyze documents thoroughly and create structured summaries that:
- 1. Capture the complete essence and key insights of the source material
- 2. Maintain perfect accuracy and factual precision
- 3. Present information objectively without bias or interpretation
- 4. Preserve critical context and logical relationships
- 5. Structure content in a clear, hierarchical format
-
+
+ You are a professional document analysis and processing expert, capable of intelligently switching work modes based on the length of the input content.
+
+
+
+ 1. **Evaluate Input**: First, evaluate the word count of the input document.
+ 2. **Execution Branch**:
+ * **If Content is Sufficient (e.g., over 200 words)**: Switch to **"Mode A: In-depth Summary"** and strictly follow the and defined below.
+ * **If Content is Insufficient (e.g., under 200 words)**: Switch to **"Mode B: Topic Expansion"**, at which point, ignore the "fidelity to the original text" constraint in the and instead execute a content generation task.
+
+
+
+
+ When the input content is sufficient, your task is to distill it into a clear, comprehensive, objective, and structured summary.
+
+
+ - Accurately capture the complete essence and core ideas of the source material.
+ - Strictly adhere to the (Accuracy, Objectivity, Comprehensiveness).
+ - Generate the summary following the and .
+ - Simultaneously complete the title and tag generation as specified in the .
+
+
+
+
+
+ When the input content is too short to produce a meaningful summary, your task is to logically enrich and expand upon its core theme.
+
+
+ - **Identify Core Theme**: Identify 1-2 core concepts or keywords from the brief input.
+ - **Logical Association and Expansion**: Based on the identified core theme, perform logical association and expand on it from various dimensions (e.g., background, importance, applications, future trends) to generate a more information-rich text.
+ - **Maintain Coherence**: Ensure the expanded content remains highly relevant and logically coherent with the core idea of the original text.
+ - **Ignore Summarization Principles**: In this mode, requirements from the such as "absolute fidelity to the original text" and "avoid inference" **do not apply**.
+ - **Fulfill Customization Requirements**: You are still required to complete the title and tag generation from the based on the **expanded content**.
+ - **Output**: Directly output the expanded text content without further summarization.
+
+
@@ -51,8 +96,8 @@
- Return summary in clean markdown format
- Do not include markdown code block tags (```markdown ```)
- Use standard markdown syntax for formatting (headers, lists, etc.)
- - Use # for main headings (e.g., # EXECUTIVE SUMMARY)
- - Use ## for subheadings where appropriate
+ - Use ### for main headings
+ - Use #### for subheadings where appropriate
- Use bullet points (- item) for lists
- Ensure proper indentation and spacing
- Use appropriate emphasis (**bold**, *italic*) where needed
diff --git a/web/package-lock.json b/web/package-lock.json
index 3a292ba..72de57e 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -30,6 +30,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
+ "react-icons": "^5.5.0",
"remark": "^15.0.1",
"remark-html": "^16.0.1",
"remark-parse": "^11.0.0",
@@ -8780,6 +8781,15 @@
"react-dom": ">=16"
}
},
+ "node_modules/react-icons": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
+ "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/web/package.json b/web/package.json
index 12e7e15..80e6c78 100644
--- a/web/package.json
+++ b/web/package.json
@@ -34,6 +34,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
+ "react-icons": "^5.5.0",
"remark": "^15.0.1",
"remark-html": "^16.0.1",
"remark-parse": "^11.0.0",
diff --git a/web/public/favicon.webp b/web/public/favicon.webp
new file mode 100644
index 0000000..495d909
Binary files /dev/null and b/web/public/favicon.webp differ
diff --git a/web/scripts/setup.js b/web/scripts/setup.js
index 764d31b..bbaaf8e 100644
--- a/web/scripts/setup.js
+++ b/web/scripts/setup.js
@@ -4,7 +4,7 @@ const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
-console.log('🚀 ListenHub Web应用设置向导\n');
+console.log('🚀 PodcastHub Web应用设置向导\n');
// 检查Node.js版本
const nodeVersion = process.version;
@@ -72,4 +72,4 @@ console.log('\n下一步:');
console.log('1. 编辑 .env.local 文件,配置您的OpenAI API密钥');
console.log('2. 运行 npm run dev 启动开发服务器');
console.log('3. 在浏览器中打开 http://localhost:3000');
-console.log('\n享受使用ListenHub!🎙️');
\ No newline at end of file
+console.log('\n享受使用PodcastHub!🎙️');
\ No newline at end of file
diff --git a/web/src/app/api/newuser/route.ts b/web/src/app/api/newuser/route.ts
index 7decb39..85f2f66 100644
--- a/web/src/app/api/newuser/route.ts
+++ b/web/src/app/api/newuser/route.ts
@@ -21,8 +21,9 @@ export async function GET(request: NextRequest) {
if (!userHasPointsAccount) {
console.log(`用户 ${userId} 不存在积分账户,正在初始化...`);
try {
- await createPointsAccount(userId, 100); // 调用封装的创建积分账户函数
- await recordPointsTransaction(userId, 100, "initial_bonus", "新用户注册,初始积分奖励"); // 调用封装的记录流水函数
+ const pointsPerPodcastDay = parseInt(process.env.POINTS_PER_PODCAST_DAY || '100', 10);
+ await createPointsAccount(userId, pointsPerPodcastDay); // 调用封装的创建积分账户函数
+ await recordPointsTransaction(userId, pointsPerPodcastDay, "initial_bonus", "新用户注册,初始积分奖励"); // 调用封装的记录流水函数
} catch (error) {
console.error(`初始化用户 ${userId} 积分账户或记录流水失败:`, error);
// 根据错误类型,可能需要更详细的错误处理或重定向
diff --git a/web/src/app/contact/page.tsx b/web/src/app/contact/page.tsx
new file mode 100644
index 0000000..bf17bdb
--- /dev/null
+++ b/web/src/app/contact/page.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { Metadata } from 'next';
+import { AiOutlineTikTok, AiFillQqCircle, AiOutlineGithub, AiOutlineTwitter, AiFillMail } from 'react-icons/ai';
+
+/**
+ * 设置页面元数据。
+ */
+export const metadata: Metadata = {
+ title: '联系我们 - PodcastHub',
+ description: '有任何问题或建议?请随时联系 PodcastHub 团队。我们期待您的声音。',
+};
+
+/**
+ * 联系我们页面组件。
+ * 优化后的版本,移除了联系表单,专注于清晰地展示联系方式。
+ * 采用单栏居中布局,设计简洁、现代。
+ */
+const ContactUsPage: React.FC = () => {
+ return (
+