2025-03-07 00:00:11 +08:00
|
|
|
|
import os
|
2025-03-06 20:11:54 +08:00
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
2025-03-07 00:00:11 +08:00
|
|
|
|
import asyncio
|
|
|
|
|
|
from flask import Flask, request, jsonify, send_from_directory
|
|
|
|
|
|
from flask_cors import CORS
|
|
|
|
|
|
import openai
|
|
|
|
|
|
import fitz # PyMuPDF
|
|
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
|
|
|
|
|
|
# 加载环境变量
|
|
|
|
|
|
load_dotenv()
|
|
|
|
|
|
|
|
|
|
|
|
# 配置日志
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
|
|
|
|
handlers=[
|
|
|
|
|
|
logging.StreamHandler()
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
2025-03-06 20:11:54 +08:00
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
2025-03-07 00:00:11 +08:00
|
|
|
|
# 获取API密钥
|
|
|
|
|
|
#openai_api_key = os.getenv("OPENAI_API_KEY")
|
|
|
|
|
|
openai_api_key = "sk-95ab48a1e0754ad39c13e2987f73fe37"
|
|
|
|
|
|
openai_base_url = "https://api.deepseek.com"
|
|
|
|
|
|
|
|
|
|
|
|
if not openai_api_key:
|
|
|
|
|
|
logger.warning("OpenAI API key not found. AI explanation will use fallback mode.")
|
|
|
|
|
|
|
|
|
|
|
|
# 加载设置
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open('setting.json', 'r') as f:
|
|
|
|
|
|
settings = json.load(f)
|
2025-03-07 12:11:16 +08:00
|
|
|
|
port = settings.get('websocket_port', 6006)
|
2025-03-07 00:00:11 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error loading settings: {e}")
|
2025-03-07 12:11:16 +08:00
|
|
|
|
port = 6006
|
2025-03-06 20:11:54 +08:00
|
|
|
|
|
2025-03-07 00:00:11 +08:00
|
|
|
|
app = Flask(__name__, static_url_path='')
|
|
|
|
|
|
CORS(app)
|
2025-03-06 20:11:54 +08:00
|
|
|
|
|
2025-03-07 00:00:11 +08:00
|
|
|
|
# 存储PDF文档内容和生成的讲解
|
|
|
|
|
|
pdf_content = {
|
|
|
|
|
|
"full_text": "",
|
|
|
|
|
|
"pages": [],
|
|
|
|
|
|
"explanations": []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def extract_pdf_text(pdf_path):
|
|
|
|
|
|
"""提取PDF文档的全部文本内容和每一页的文本"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
doc = fitz.open(pdf_path)
|
|
|
|
|
|
full_text = ""
|
|
|
|
|
|
pages = []
|
|
|
|
|
|
|
|
|
|
|
|
for page_num in range(len(doc)):
|
|
|
|
|
|
page = doc.load_page(page_num)
|
|
|
|
|
|
page_text = page.get_text()
|
|
|
|
|
|
pages.append(page_text)
|
|
|
|
|
|
full_text += f"\n--- 第{page_num+1}页 ---\n{page_text}"
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": True,
|
|
|
|
|
|
"full_text": full_text,
|
|
|
|
|
|
"pages": pages,
|
|
|
|
|
|
"page_count": len(doc)
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error extracting PDF text: {e}")
|
|
|
|
|
|
return {
|
|
|
|
|
|
"success": False,
|
|
|
|
|
|
"error": str(e)
|
|
|
|
|
|
}
|
2025-03-06 20:11:54 +08:00
|
|
|
|
|
2025-03-07 00:00:11 +08:00
|
|
|
|
async def generate_explanations_for_all_pages(full_text, pages):
|
|
|
|
|
|
"""为所有页面生成讲解内容"""
|
|
|
|
|
|
explanations = []
|
|
|
|
|
|
client = openai.OpenAI(api_key=openai_api_key, base_url=openai_base_url)
|
|
|
|
|
|
|
|
|
|
|
|
# 首先让LLM理解整个文档
|
2025-03-06 20:11:54 +08:00
|
|
|
|
try:
|
2025-03-07 00:00:11 +08:00
|
|
|
|
logger.info("Generating context understanding from full document...")
|
|
|
|
|
|
context_response = client.chat.completions.create(
|
|
|
|
|
|
model="deepseek-chat",
|
|
|
|
|
|
messages=[
|
|
|
|
|
|
{"role": "system", "content": "你是一位专业的教师,需要理解整个PDF文档的内容,以便后续为每一页生成讲解。"},
|
|
|
|
|
|
{"role": "user", "content": f"请阅读并理解以下PDF文档的全部内容,不需要回复具体内容,只需要理解:\n\n{full_text}"}
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
context_understanding = context_response.choices[0].message.content.strip()
|
|
|
|
|
|
logger.info("Context understanding generated successfully")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error generating context understanding: {e}")
|
|
|
|
|
|
context_understanding = "无法生成文档理解,将基于单页内容生成讲解。"
|
|
|
|
|
|
|
|
|
|
|
|
# 为每一页生成讲解
|
|
|
|
|
|
for i, page_text in enumerate(pages):
|
|
|
|
|
|
try:
|
|
|
|
|
|
logger.info(f"Generating explanation for page {i+1}...")
|
|
|
|
|
|
response = client.chat.completions.create(
|
|
|
|
|
|
model="deepseek-chat",
|
|
|
|
|
|
messages=[
|
|
|
|
|
|
{"role": "system", "content": f"你是一位专业的教师,正在为学生讲解PDF文档内容。你已经理解了整个文档的内容,现在需要为第{i+1}页生成简洁的讲解。请提供清晰、简洁的解释,重点突出关键概念。你的讲解应该考虑到整个文档的上下文,而不仅仅是孤立地解释当前页面。"},
|
|
|
|
|
|
{"role": "user", "content": f"基于你对整个文档的理解,请为第{i+1}页生成简洁的讲解:\n\n{page_text}"}
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
explanation = response.choices[0].message.content.strip()
|
|
|
|
|
|
explanations.append(explanation)
|
|
|
|
|
|
logger.info(f"Explanation for page {i+1} generated successfully")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error generating explanation for page {i+1}: {e}")
|
|
|
|
|
|
explanations.append(f"生成第{i+1}页讲解时出错: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
return explanations
|
|
|
|
|
|
|
|
|
|
|
|
def generate_explanation(page_text, page_num=None):
|
|
|
|
|
|
"""为单个页面生成讲解内容"""
|
|
|
|
|
|
if not openai_api_key:
|
|
|
|
|
|
return "这是一个示例讲解。请设置OpenAI API密钥以获取真实的AI讲解内容。"
|
|
|
|
|
|
|
|
|
|
|
|
# 如果已经有预生成的讲解,直接返回
|
|
|
|
|
|
if pdf_content["explanations"] and page_num is not None and 0 <= page_num-1 < len(pdf_content["explanations"]):
|
|
|
|
|
|
return pdf_content["explanations"][page_num-1]
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
client = openai.OpenAI(api_key=openai_api_key, base_url=openai_base_url)
|
|
|
|
|
|
response = client.chat.completions.create(
|
|
|
|
|
|
model="deepseek-chat",
|
|
|
|
|
|
messages=[
|
|
|
|
|
|
{"role": "system", "content": "你是一位专业的教师,正在为学生讲解PDF文档内容。请提供清晰、简洁的解释,重点突出关键概念。"},
|
|
|
|
|
|
{"role": "user", "content": f"请讲解以下内容:\n\n{page_text}"}
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
return response.choices[0].message.content.strip()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Error generating explanation: {e}")
|
|
|
|
|
|
return f"生成讲解时出错: {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/')
|
|
|
|
|
|
def index():
|
|
|
|
|
|
return send_from_directory('', 'index.html')
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/<path:path>')
|
|
|
|
|
|
def serve_static(path):
|
|
|
|
|
|
return send_from_directory('', path)
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/explain', methods=['POST'])
|
|
|
|
|
|
def explain():
|
|
|
|
|
|
data = request.json
|
|
|
|
|
|
text = data.get('text', '')
|
|
|
|
|
|
page_num = data.get('page', None)
|
|
|
|
|
|
|
|
|
|
|
|
explanation = generate_explanation(text, page_num)
|
|
|
|
|
|
return jsonify({'explanation': explanation})
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/load_pdf', methods=['POST'])
|
|
|
|
|
|
def load_pdf():
|
|
|
|
|
|
data = request.json
|
2025-03-07 12:11:16 +08:00
|
|
|
|
pdf_path = data.get('path', './public/pdf/VLA4RM-仿生智能.pdf')
|
2025-03-07 00:00:11 +08:00
|
|
|
|
|
|
|
|
|
|
# 提取PDF文本
|
|
|
|
|
|
result = extract_pdf_text(pdf_path)
|
|
|
|
|
|
|
|
|
|
|
|
if result["success"]:
|
|
|
|
|
|
# 更新全局PDF内容
|
|
|
|
|
|
pdf_content["full_text"] = result["full_text"]
|
|
|
|
|
|
pdf_content["pages"] = result["pages"]
|
|
|
|
|
|
|
|
|
|
|
|
# 异步生成所有页面的讲解
|
|
|
|
|
|
async def process_explanations():
|
|
|
|
|
|
explanations = await generate_explanations_for_all_pages(
|
|
|
|
|
|
result["full_text"],
|
|
|
|
|
|
result["pages"]
|
|
|
|
|
|
)
|
|
|
|
|
|
pdf_content["explanations"] = explanations
|
|
|
|
|
|
logger.info(f"Generated explanations for all {len(explanations)} pages")
|
|
|
|
|
|
|
|
|
|
|
|
# 启动异步任务
|
|
|
|
|
|
asyncio.run(process_explanations())
|
|
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
'success': True,
|
|
|
|
|
|
'message': '已加载PDF并开始生成讲解',
|
|
|
|
|
|
'page_count': result["page_count"]
|
|
|
|
|
|
})
|
|
|
|
|
|
else:
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
'success': False,
|
|
|
|
|
|
'message': f'加载PDF失败: {result["error"]}'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/get_explanation/<int:page_num>', methods=['GET'])
|
|
|
|
|
|
def get_explanation(page_num):
|
|
|
|
|
|
if 0 <= page_num-1 < len(pdf_content["explanations"]):
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
'success': True,
|
|
|
|
|
|
'explanation': pdf_content["explanations"][page_num-1]
|
|
|
|
|
|
})
|
|
|
|
|
|
else:
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
'success': False,
|
|
|
|
|
|
'message': f'页码 {page_num} 的讲解不存在'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
@app.route('/api/explanation_status', methods=['GET'])
|
|
|
|
|
|
def explanation_status():
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
|
'total_pages': len(pdf_content["pages"]),
|
|
|
|
|
|
'explanations_generated': len(pdf_content["explanations"]),
|
|
|
|
|
|
'is_complete': len(pdf_content["pages"]) > 0 and len(pdf_content["pages"]) == len(pdf_content["explanations"])
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
# 在启动时预加载默认PDF
|
|
|
|
|
|
default_pdf_path = './VLA4RM-仿生智能.pdf'
|
|
|
|
|
|
if os.path.exists(default_pdf_path):
|
|
|
|
|
|
logger.info(f"Pre-loading default PDF: {default_pdf_path}")
|
|
|
|
|
|
result = extract_pdf_text(default_pdf_path)
|
|
|
|
|
|
if result["success"]:
|
|
|
|
|
|
pdf_content["full_text"] = result["full_text"]
|
|
|
|
|
|
pdf_content["pages"] = result["pages"]
|
|
|
|
|
|
|
|
|
|
|
|
# 异步生成所有页面的讲解
|
|
|
|
|
|
async def process_explanations():
|
|
|
|
|
|
explanations = await generate_explanations_for_all_pages(
|
|
|
|
|
|
result["full_text"],
|
|
|
|
|
|
result["pages"]
|
|
|
|
|
|
)
|
|
|
|
|
|
pdf_content["explanations"] = explanations
|
|
|
|
|
|
logger.info(f"Generated explanations for all {len(explanations)} pages")
|
|
|
|
|
|
|
|
|
|
|
|
# 启动异步任务
|
|
|
|
|
|
asyncio.run(process_explanations())
|
|
|
|
|
|
|
|
|
|
|
|
app.run(host='0.0.0.0', port=port, debug=True)
|