lexlepty commited on
Commit
edf5e48
·
verified ·
1 Parent(s): 1d8ebdd

Update app.py

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