File size: 9,110 Bytes
edf5e48 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
from flask import Flask, request, jsonify, render_template, Response
import os
from dotenv import load_dotenv
import json
import requests
from PIL import Image
import base64
from io import BytesIO
import logging
import time
# 加载环境变量
load_dotenv()
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def create_headers():
"""创建API请求头"""
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"
}
def image_to_base64(image):
"""将PIL图像转换为base64"""
buffered = BytesIO()
image.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode()
def process_image_with_vision(image_base64):
"""使用Vision API处理图像"""
url = f"{os.getenv('OPENAI_API_BASE')}/v1/chat/completions"
messages = [
{
"role": "user",
"content": [
{
"type": "text",
"text": """请仔细分析这张图片中的题目,要求:
1. 识别内容类型:
- 判断是否为数学题、物理题、化学题等
- 识别是选择题、填空题还是解答题
- 判断是否包含图表或特殊符号
2. 内容提取:
- 提取所有文字内容,包括题干、选项(如果有)
- 识别所有数学公式、化学方程式或特殊符号
- 保留原有的排版格式(如换行、缩进等)
3. 公式处理:
- 将所有数学公式转换为LaTeX格式
- 使用[formula_n]作为占位符,其中n为公式编号
- 保持公式的完整性和准确性
请使用以下JSON格式返回:
{
"type": "题目类型(如:数学/物理/化学)",
"format": "题目格式(如:选择题/填空题/解答题)",
"text": "包含[formula_1], [formula_2]等占位符的完整文本",
"formulas": ["latex公式1", "latex公式2"],
"options": ["A. xxx", "B. xxx"] // 如果是选择题则包含此字段
"notes": ["可能存在的问题说明1", "问题说明2"]
}"""
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{image_base64}"
}
}
]
}
]
payload = {
"model": os.getenv('OPENAI_VISION_MODEL'),
"messages": messages,
"max_tokens": 1500,
"temperature": 0.2
}
try:
response = requests.post(url, headers=create_headers(), json=payload)
response.raise_for_status()
result = response.json()
content = result['choices'][0]['message']['content']
try:
return json.loads(content)
except json.JSONDecodeError:
return {
"type": "unknown",
"format": "unknown",
"text": content,
"formulas": [],
"notes": ["无法解析为JSON格式"]
}
except Exception as e:
logger.error(f"Vision API调用错误: {str(e)}")
return {"error": str(e)}
def handle_sse_response(raw_data):
"""处理SSE响应数据"""
if raw_data:
try:
data = json.loads(raw_data)
if len(data['choices']) > 0:
delta = data['choices'][0].get('delta', {})
content = delta.get('content', '')
print(f"Stream content: {content}", flush=True) # 直接打印流式内容
return content
except json.JSONDecodeError:
pass
return ''
def stream_solve(problem_text, formulas):
url = f"{os.getenv('OPENAI_API_BASE')}/v1/chat/completions"
full_problem = problem_text
for i, formula in enumerate(formulas, 1):
full_problem = full_problem.replace(f"[formula_{i}]", f"$${formula}$$")
print(f"\n问题内容: {full_problem}")
messages = [
{
"role": "system",
"content": """请按照以下格式回答问题:
[开始解答]
这是答案:(写出明确的答案,如"答案为:2.5米","答案选 A"等)
[解答结束]
[开始解析]
这是解析:(写出详细的解析过程,需严格遵循以下要求:
解题思路要求:
1. 开篇明确指出题目涉及的核心知识点和解题方向
2. 采用合理的解题策略,并说明策略选择的依据
3. 对于多解法题目,说明最优解法的选择理由
专业性要求:
1. 严格使用规范的学科专业术语,避免口语化表达
2. 数学公式、物理量、化学方程式等必须符合学科规范
3. 计算步骤要详实,重要的中间步骤不得省略
4. 注意数据的有效位数,物理量单位的规范性
逻辑性要求:
1. 解题步骤要层次分明,逻辑推导严谨
2. 关键结论要有充分的推导过程和理论依据
3. 复杂问题应合理拆分为子步骤,循序渐进
教学性要求:
1. 对重要概念和关键步骤要适当添加解释说明
2. 特别标注解题中的难点和易错点
3. 适时补充相关的知识点拓展
4. 针对典型错误进行分析和提醒
书写规范:
1. 专业符号和公式要规范,确保清晰美观
2. 合理使用缩进和分段,突出层次结构
3. 保持语言的严谨性和专业性)
[解析结束]"""
},
{
"role": "user",
"content": full_problem
}
]
payload = {
"model": os.getenv('OPENAI_CHAT_MODEL'),
"messages": messages,
"stream": True,
"temperature": 0.3
}
try:
response = requests.post(
url,
headers=create_headers(),
json=payload,
stream=True
)
response.raise_for_status()
complete_response = ""
for line in response.iter_lines():
if not line or not line.startswith(b'data: '):
continue
try:
data = json.loads(line[6:].decode('utf-8'))
content = data['choices'][0].get('delta', {}).get('content', '')
complete_response += content
except json.JSONDecodeError:
continue
except Exception as e:
print(f"\n错误: {str(e)}")
yield f"data: {json.dumps({'error': str(e)})}\n\n"
return
# 提取答案和解析
try:
if '[开始解答]' in complete_response and '[开始解析]' in complete_response:
parts = complete_response.split('[开始解答]')
answer_part = parts[1].split('[解答结束]')[0].strip()
parts = complete_response.split('[开始解析]')
analysis_part = parts[1].split('[解析结束]')[0].strip()
# 控制台打印
print("\n完整回答:")
print(f"答案: {answer_part}")
print(f"解析: {analysis_part}")
# 返回结果给前端
yield f"data: {json.dumps({'type': 'answer', 'content': answer_part})}\n\n"
yield f"data: {json.dumps({'type': 'analysis', 'content': analysis_part})}\n\n"
else:
print("\n响应格式不符合预期")
yield f"data: {json.dumps({'error': '响应格式不符合预期'})}\n\n"
except Exception as e:
print(f"\n解析错误: {str(e)}")
yield f"data: {json.dumps({'error': f'解析响应时出错: {str(e)}'})}\n\n"
@app.route('/')
def index():
return render_template('index.html')
@app.route('/process', methods=['POST'])
def process():
if 'file' not in request.files:
return jsonify({'error': '没有文件上传'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': '没有选择文件'}), 400
try:
image = Image.open(file)
image_base64 = image_to_base64(image)
result = process_image_with_vision(image_base64)
if 'error' in result:
return jsonify({'error': result['error']}), 500
return jsonify({
'original_image': image_base64,
'result': result
})
except Exception as e:
logger.error(f"处理图像错误: {str(e)}")
return jsonify({'error': str(e)}), 500
@app.route('/solve', methods=['POST'])
def solve():
data = request.json
if not data or 'text' not in data or 'formulas' not in data:
return jsonify({'error': '无效的请求数据'}), 400
return Response(
stream_solve(data['text'], data['formulas']),
content_type='text/event-stream'
)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860, debug=True) |