data-learner commited on
Commit
b819d98
·
verified ·
1 Parent(s): ba43ff8

Upload unified_server.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. unified_server.py +208 -0
unified_server.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 统一服务器 - 合并H5页面 + OCR + 导出服务于一个端口
3
+ 手机端通过内网穿透访问
4
+ """
5
+ import json
6
+ import io
7
+ import os
8
+ import sys
9
+ import re
10
+ import base64
11
+ import tempfile
12
+ from http.server import HTTPServer, BaseHTTPRequestHandler
13
+ from urllib.parse import urlparse
14
+
15
+ # 确保能导入同目录模块
16
+ WORK_DIR = os.path.dirname(os.path.abspath(__file__))
17
+ sys.path.insert(0, WORK_DIR)
18
+
19
+ # 导入导出服务核心函数
20
+ from export_server import generate_docx
21
+
22
+ # 导入OCR核心函数
23
+ import pytesseract
24
+ import platform
25
+ import shutil
26
+ from PIL import Image
27
+
28
+ # 跨平台 Tesseract 路径配置
29
+ if platform.system() == 'Windows':
30
+ # Windows 本地开发环境
31
+ pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
32
+ os.environ['TESSDATA_PREFIX'] = os.path.expandvars(r'%USERPROFILE%\.tesseract\tessdata')
33
+ else:
34
+ # Linux/Docker 环境:tesseract 已通过 apt 安装到 PATH
35
+ tesseract_bin = shutil.which('tesseract')
36
+ if tesseract_bin:
37
+ pytesseract.pytesseract.tesseract_cmd = tesseract_bin
38
+ # 自动检测 tessdata 路径(兼容 Tesseract 4.x/5.x)
39
+ for _tessdata in ['/usr/share/tesseract-ocr/5/tessdata',
40
+ '/usr/share/tesseract-ocr/4.00/tessdata',
41
+ '/usr/share/tessdata']:
42
+ if os.path.isdir(_tessdata):
43
+ os.environ.setdefault('TESSDATA_PREFIX', _tessdata)
44
+ break
45
+ print(f'[Tesseract] cmd={pytesseract.pytesseract.tesseract_cmd}, '
46
+ f'TESSDATA_PREFIX={os.environ.get("TESSDATA_PREFIX", "(default)")}')
47
+
48
+ # H5页面路径
49
+ H5_FILE = os.path.join(WORK_DIR, 'test-h5.html')
50
+
51
+ PORT = int(os.environ.get('PORT', 7860))
52
+
53
+
54
+ def ocr_image(image_data):
55
+ """对图片进行OCR识别"""
56
+ img = Image.open(io.BytesIO(image_data))
57
+ w, h = img.size
58
+ max_dim = 2000
59
+ if w > max_dim or h > max_dim:
60
+ ratio = min(max_dim / w, max_dim / h)
61
+ img = img.resize((int(w * ratio), int(h * ratio)), Image.LANCZOS)
62
+ text = pytesseract.image_to_string(img, lang='chi_sim', config='--psm 6')
63
+ return text.strip()
64
+
65
+
66
+ class UnifiedHandler(BaseHTTPRequestHandler):
67
+
68
+ def _add_cors_headers(self):
69
+ self.send_header('Access-Control-Allow-Origin', '*')
70
+ self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
71
+ self.send_header('Access-Control-Allow-Headers', 'Content-Type')
72
+
73
+ def do_OPTIONS(self):
74
+ self.send_response(200)
75
+ self._add_cors_headers()
76
+ self.end_headers()
77
+
78
+ def do_GET(self):
79
+ """处理GET请求 - 返回H5页面"""
80
+ path = urlparse(self.path).path
81
+
82
+ if path == '/' or path == '/test-h5.html' or path == '/index.html':
83
+ try:
84
+ with open(H5_FILE, 'r', encoding='utf-8') as f:
85
+ content = f.read()
86
+ self.send_response(200)
87
+ self.send_header('Content-Type', 'text/html; charset=utf-8')
88
+ self._add_cors_headers()
89
+ self.end_headers()
90
+ self.wfile.write(content.encode('utf-8'))
91
+ except Exception as e:
92
+ self.send_error(500, f'读取页面失败: {e}')
93
+ elif path == '/health':
94
+ # 健康检查
95
+ self.send_response(200)
96
+ self.send_header('Content-Type', 'application/json')
97
+ self._add_cors_headers()
98
+ self.end_headers()
99
+ self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8'))
100
+ else:
101
+ self.send_error(404, '页面不存在')
102
+
103
+ def do_POST(self):
104
+ """处理POST请求 - OCR和导出"""
105
+ path = urlparse(self.path).path
106
+
107
+ if path == '/ocr':
108
+ self._handle_ocr()
109
+ elif path == '/export':
110
+ self._handle_export()
111
+ else:
112
+ self.send_error(404, '接口不存在')
113
+
114
+ def _handle_ocr(self):
115
+ """处理OCR识别请求"""
116
+ content_length = int(self.headers.get('Content-Length', 0))
117
+ body = self.rfile.read(content_length)
118
+
119
+ content_type = self.headers.get('Content-Type', '')
120
+ if 'multipart/form-data' not in content_type:
121
+ self._send_json({'error': '需要 multipart/form-data'}, 400)
122
+ return
123
+
124
+ # 提取 boundary
125
+ boundary = content_type.split('boundary=')[1].strip()
126
+ if boundary.startswith('"') and boundary.endswith('"'):
127
+ boundary = boundary[1:-1]
128
+
129
+ # 手动解析 multipart
130
+ parts = body.split(('--' + boundary).encode())
131
+ image_data = None
132
+ for part in parts:
133
+ if b'Content-Disposition' in part and b'filename=' in part:
134
+ header_end = part.find(b'\r\n\r\n')
135
+ if header_end == -1:
136
+ continue
137
+ file_data = part[header_end + 4:]
138
+ if file_data.endswith(b'\r\n'):
139
+ file_data = file_data[:-2]
140
+ image_data = file_data
141
+ break
142
+
143
+ if not image_data:
144
+ self._send_json({'error': '未找到图片数据'}, 400)
145
+ return
146
+
147
+ try:
148
+ text = ocr_image(image_data)
149
+ self._send_json({'text': text, 'length': len(text)})
150
+ print(f'[OCR] 识别成功,提取 {len(text)} 个字符')
151
+ except Exception as e:
152
+ self._send_json({'error': f'OCR识别失败: {str(e)}'}, 500)
153
+ print(f'[OCR] 识别失败: {e}')
154
+
155
+ def _handle_export(self):
156
+ """处理Word文档导出请求"""
157
+ content_length = int(self.headers.get('Content-Length', 0))
158
+ body = self.rfile.read(content_length)
159
+
160
+ try:
161
+ visit_data = json.loads(body)
162
+ buffer = generate_docx(visit_data)
163
+
164
+ self.send_response(200)
165
+ self.send_header('Content-Type',
166
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')
167
+ self._add_cors_headers()
168
+ filename = 'visit_%s_%sstores.docx' % (
169
+ visit_data.get('visitDateDisplay', '').replace('.', '-'),
170
+ visit_data.get('totalCount', 0)
171
+ )
172
+ self.send_header('Content-Disposition', f'attachment; filename="{filename}"')
173
+ self.end_headers()
174
+ self.wfile.write(buffer.getvalue())
175
+ print(f'[Export] 导出成功: {filename}')
176
+ except Exception as e:
177
+ self.send_response(500)
178
+ self.send_header('Content-Type', 'application/json')
179
+ self._add_cors_headers()
180
+ self.end_headers()
181
+ self.wfile.write(json.dumps({'error': str(e)}).encode('utf-8'))
182
+ print(f'[Export] 导出失败: {e}')
183
+
184
+ def _send_json(self, data, status=200):
185
+ self.send_response(status)
186
+ self.send_header('Content-Type', 'application/json; charset=utf-8')
187
+ self._add_cors_headers()
188
+ self.end_headers()
189
+ self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
190
+
191
+ def log_message(self, format, *args):
192
+ print(f'[{self.log_date_time_string()}] {args[0]}')
193
+
194
+
195
+ if __name__ == '__main__':
196
+ print('=' * 50)
197
+ print(' 药店拜访助手 - 统一服务器')
198
+ print(f' 本机访问: http://127.0.0.1:{PORT}')
199
+ print(f' 局域网访问: http://<本机IP>:{PORT}')
200
+ print('=' * 50)
201
+
202
+ # 绑定 0.0.0.0 让局域网设备也能访问
203
+ server = HTTPServer(('0.0.0.0', PORT), UnifiedHandler)
204
+ try:
205
+ server.serve_forever()
206
+ except KeyboardInterrupt:
207
+ print('\n服务器已停止')
208
+ server.shutdown()