xwwww commited on
Commit
26b681b
·
1 Parent(s): d6fdd15
Files changed (3) hide show
  1. app.py +32 -0
  2. static/index.html +30 -4
  3. test_upload.html +250 -0
app.py CHANGED
@@ -34,6 +34,8 @@ except ImportError as e:
34
  pass
35
  class AutoProcessor:
36
  pass
 
 
37
  def process_vision_info(*args, **kwargs):
38
  raise ImportError("qwen_vl_utils not available")
39
 
@@ -538,7 +540,10 @@ async def analyze_simple(request: AnalyzeRequest):
538
  接收 JSON 格式的请求,包含 base64 图片和提示词
539
  返回标准化的分析结果
540
  """
 
 
541
  if not QWEN_VL_AVAILABLE:
 
542
  return AnalyzeResponse(
543
  success=False,
544
  prompt=request.prompt,
@@ -548,6 +553,7 @@ async def analyze_simple(request: AnalyzeRequest):
548
  )
549
 
550
  if model is None or processor is None:
 
551
  return AnalyzeResponse(
552
  success=False,
553
  prompt=request.prompt,
@@ -557,22 +563,31 @@ async def analyze_simple(request: AnalyzeRequest):
557
  )
558
 
559
  start_time = time.time()
 
560
 
561
  try:
562
  # 处理 base64 图片
 
563
  image_data = request.image
564
  if image_data.startswith('data:image'):
565
  # 移除 data:image/xxx;base64, 前缀
566
  image_data = image_data.split(',')[1]
 
567
 
 
568
  image_bytes = base64.b64decode(image_data)
 
 
569
  pil_image = Image.open(io.BytesIO(image_bytes))
 
570
 
571
  # 确保图片是 RGB 格式
572
  if pil_image.mode != 'RGB':
573
  pil_image = pil_image.convert('RGB')
 
574
 
575
  # 准备消息格式
 
576
  messages = [
577
  {
578
  "role": "user",
@@ -587,10 +602,13 @@ async def analyze_simple(request: AnalyzeRequest):
587
  ]
588
 
589
  # 处理输入
 
590
  text = processor.apply_chat_template(
591
  messages, tokenize=False, add_generation_prompt=True
592
  )
 
593
  image_inputs, video_inputs = process_vision_info(messages)
 
594
  inputs = processor(
595
  text=[text],
596
  images=image_inputs,
@@ -598,8 +616,10 @@ async def analyze_simple(request: AnalyzeRequest):
598
  padding=True,
599
  return_tensors="pt",
600
  )
 
601
 
602
  # 生成回答
 
603
  with torch.no_grad():
604
  # 使用 GenerationConfig 来避免警告并确保参数正确
605
  generation_config = GenerationConfig(
@@ -609,17 +629,24 @@ async def analyze_simple(request: AnalyzeRequest):
609
  eos_token_id=processor.tokenizer.eos_token_id,
610
  use_cache=True
611
  )
 
612
  generated_ids = model.generate(**inputs, generation_config=generation_config)
 
613
 
 
614
  generated_ids_trimmed = [
615
  out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
616
  ]
 
617
 
618
  output_text = processor.batch_decode(
619
  generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
620
  )[0]
621
 
622
  processing_time = time.time() - start_time
 
 
 
623
 
624
  return AnalyzeResponse(
625
  success=True,
@@ -703,3 +730,8 @@ def clear_cache():
703
  def web_interface():
704
  """返回 Web 界面"""
705
  return FileResponse("static/index.html")
 
 
 
 
 
 
34
  pass
35
  class AutoProcessor:
36
  pass
37
+ class GenerationConfig:
38
+ pass
39
  def process_vision_info(*args, **kwargs):
40
  raise ImportError("qwen_vl_utils not available")
41
 
 
540
  接收 JSON 格式的请求,包含 base64 图片和提示词
541
  返回标准化的分析结果
542
  """
543
+ logger.info(f"收到图片分析请求: prompt='{request.prompt}', image_length={len(request.image) if request.image else 0}")
544
+
545
  if not QWEN_VL_AVAILABLE:
546
+ logger.error(f"Qwen-VL 依赖不可用: {IMPORT_ERROR}")
547
  return AnalyzeResponse(
548
  success=False,
549
  prompt=request.prompt,
 
553
  )
554
 
555
  if model is None or processor is None:
556
+ logger.error("模型未加载")
557
  return AnalyzeResponse(
558
  success=False,
559
  prompt=request.prompt,
 
563
  )
564
 
565
  start_time = time.time()
566
+ logger.info("开始处理图片分析请求...")
567
 
568
  try:
569
  # 处理 base64 图片
570
+ logger.info("开始处理 base64 图片数据...")
571
  image_data = request.image
572
  if image_data.startswith('data:image'):
573
  # 移除 data:image/xxx;base64, 前缀
574
  image_data = image_data.split(',')[1]
575
+ logger.info("移除了 data URL 前缀")
576
 
577
+ logger.info(f"解码 base64 数据,长度: {len(image_data)}")
578
  image_bytes = base64.b64decode(image_data)
579
+ logger.info(f"解码后字节数: {len(image_bytes)}")
580
+
581
  pil_image = Image.open(io.BytesIO(image_bytes))
582
+ logger.info(f"图片加载成功: {pil_image.size}, 模式: {pil_image.mode}")
583
 
584
  # 确保图片是 RGB 格式
585
  if pil_image.mode != 'RGB':
586
  pil_image = pil_image.convert('RGB')
587
+ logger.info("图片已转换为 RGB 模式")
588
 
589
  # 准备消息格式
590
+ logger.info("准备模型输入消息...")
591
  messages = [
592
  {
593
  "role": "user",
 
602
  ]
603
 
604
  # 处理输入
605
+ logger.info("应用聊天模板...")
606
  text = processor.apply_chat_template(
607
  messages, tokenize=False, add_generation_prompt=True
608
  )
609
+ logger.info("处理视觉信息...")
610
  image_inputs, video_inputs = process_vision_info(messages)
611
+ logger.info("处理器编码输入...")
612
  inputs = processor(
613
  text=[text],
614
  images=image_inputs,
 
616
  padding=True,
617
  return_tensors="pt",
618
  )
619
+ logger.info(f"输入处理完成,input_ids shape: {inputs.input_ids.shape if hasattr(inputs, 'input_ids') else 'N/A'}")
620
 
621
  # 生成回答
622
+ logger.info("开始模型生成...")
623
  with torch.no_grad():
624
  # 使用 GenerationConfig 来避免警告并确保参数正确
625
  generation_config = GenerationConfig(
 
629
  eos_token_id=processor.tokenizer.eos_token_id,
630
  use_cache=True
631
  )
632
+ logger.info(f"生成配置: max_new_tokens=512, do_sample=False")
633
  generated_ids = model.generate(**inputs, generation_config=generation_config)
634
+ logger.info(f"生成完成,输出 shape: {generated_ids.shape}")
635
 
636
+ logger.info("开始解码生成的文本...")
637
  generated_ids_trimmed = [
638
  out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids)
639
  ]
640
+ logger.info(f"修剪后的 token 数量: {[len(ids) for ids in generated_ids_trimmed]}")
641
 
642
  output_text = processor.batch_decode(
643
  generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
644
  )[0]
645
 
646
  processing_time = time.time() - start_time
647
+ logger.info(f"分析完成,处理时间: {processing_time:.2f}秒")
648
+ logger.info(f"生成的文本长度: {len(output_text)} 字符")
649
+ logger.info(f"生成的文本预览: {output_text[:100]}...")
650
 
651
  return AnalyzeResponse(
652
  success=True,
 
730
  def web_interface():
731
  """返回 Web 界面"""
732
  return FileResponse("static/index.html")
733
+
734
+ @app.get("/test-upload")
735
+ def test_upload_interface():
736
+ """返回图片上传测试界面"""
737
+ return FileResponse("test_upload.html")
static/index.html CHANGED
@@ -623,22 +623,48 @@
623
  result.style.display = 'none';
624
 
625
  try {
 
 
 
 
 
 
 
626
  // 将图片转换为 base64
 
627
  const base64Image = await fileToBase64(file);
 
628
 
629
  // 优先使用 JSON API
 
 
 
 
 
 
 
 
 
 
 
630
  const response = await fetch('/analyze', {
631
  method: 'POST',
632
  headers: {
633
  'Content-Type': 'application/json'
634
  },
635
- body: JSON.stringify({
636
- image: base64Image,
637
- prompt: question
638
- })
639
  });
640
 
 
 
 
 
 
 
 
 
641
  const data = await response.json();
 
642
 
643
  if (data.success) {
644
  result.innerHTML = `
 
623
  result.style.display = 'none';
624
 
625
  try {
626
+ console.log('开始图片分析...');
627
+ console.log('文件信息:', {
628
+ name: file.name,
629
+ size: file.size,
630
+ type: file.type
631
+ });
632
+
633
  // 将图片转换为 base64
634
+ console.log('正在转换图片为 base64...');
635
  const base64Image = await fileToBase64(file);
636
+ console.log('Base64 转换完成,长度:', base64Image.length);
637
 
638
  // 优先使用 JSON API
639
+ console.log('发送分析请求到 /analyze...');
640
+ const requestData = {
641
+ image: base64Image,
642
+ prompt: question
643
+ };
644
+ console.log('请求数据:', {
645
+ prompt: question,
646
+ imageLength: base64Image.length,
647
+ imagePrefix: base64Image.substring(0, 50) + '...'
648
+ });
649
+
650
  const response = await fetch('/analyze', {
651
  method: 'POST',
652
  headers: {
653
  'Content-Type': 'application/json'
654
  },
655
+ body: JSON.stringify(requestData)
 
 
 
656
  });
657
 
658
+ console.log('收到响应:', response.status, response.statusText);
659
+
660
+ if (!response.ok) {
661
+ const errorText = await response.text();
662
+ console.error('响应错误:', errorText);
663
+ throw new Error(`HTTP ${response.status}: ${response.statusText}\n${errorText}`);
664
+ }
665
+
666
  const data = await response.json();
667
+ console.log('解析响应数据:', data);
668
 
669
  if (data.success) {
670
  result.innerHTML = `
test_upload.html ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>图片上传测试</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ max-width: 800px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ }
14
+ .test-area {
15
+ border: 2px solid #ddd;
16
+ padding: 20px;
17
+ margin: 20px 0;
18
+ border-radius: 8px;
19
+ }
20
+ button {
21
+ background: #007bff;
22
+ color: white;
23
+ border: none;
24
+ padding: 10px 20px;
25
+ border-radius: 5px;
26
+ cursor: pointer;
27
+ margin: 5px;
28
+ }
29
+ button:hover {
30
+ background: #0056b3;
31
+ }
32
+ .result {
33
+ background: #f8f9fa;
34
+ padding: 15px;
35
+ margin: 10px 0;
36
+ border-radius: 5px;
37
+ border-left: 4px solid #007bff;
38
+ }
39
+ .error {
40
+ border-left-color: #dc3545;
41
+ background: #f8d7da;
42
+ }
43
+ .success {
44
+ border-left-color: #28a745;
45
+ background: #d4edda;
46
+ }
47
+ input[type="file"] {
48
+ margin: 10px 0;
49
+ }
50
+ textarea {
51
+ width: 100%;
52
+ height: 80px;
53
+ margin: 10px 0;
54
+ padding: 10px;
55
+ }
56
+ .log {
57
+ background: #f1f1f1;
58
+ padding: 10px;
59
+ margin: 10px 0;
60
+ border-radius: 5px;
61
+ font-family: monospace;
62
+ font-size: 12px;
63
+ max-height: 200px;
64
+ overflow-y: auto;
65
+ }
66
+ </style>
67
+ </head>
68
+ <body>
69
+ <h1>🧪 图片上传分析测试</h1>
70
+
71
+ <div class="test-area">
72
+ <h3>1. 创建测试图片</h3>
73
+ <button onclick="createTestImage()">创建测试图片</button>
74
+ <canvas id="testCanvas" width="200" height="150" style="border: 1px solid #ddd; margin: 10px;"></canvas>
75
+ </div>
76
+
77
+ <div class="test-area">
78
+ <h3>2. 上传图片测试</h3>
79
+ <input type="file" id="imageFile" accept="image/*">
80
+ <textarea id="prompt" placeholder="输入问题">请详细描述这张图片的内容</textarea>
81
+ <br>
82
+ <button onclick="testUpload()">测试上传分析</button>
83
+ <button onclick="clearLog()">清空日志</button>
84
+ </div>
85
+
86
+ <div class="test-area">
87
+ <h3>3. 测试结果</h3>
88
+ <div id="result"></div>
89
+ </div>
90
+
91
+ <div class="test-area">
92
+ <h3>4. 调试日志</h3>
93
+ <div id="log" class="log"></div>
94
+ </div>
95
+
96
+ <script>
97
+ let logMessages = [];
98
+
99
+ function log(message) {
100
+ const timestamp = new Date().toLocaleTimeString();
101
+ const logMessage = `[${timestamp}] ${message}`;
102
+ logMessages.push(logMessage);
103
+ console.log(logMessage);
104
+ updateLogDisplay();
105
+ }
106
+
107
+ function updateLogDisplay() {
108
+ const logDiv = document.getElementById('log');
109
+ logDiv.innerHTML = logMessages.join('<br>');
110
+ logDiv.scrollTop = logDiv.scrollHeight;
111
+ }
112
+
113
+ function clearLog() {
114
+ logMessages = [];
115
+ updateLogDisplay();
116
+ }
117
+
118
+ function createTestImage() {
119
+ log('创建测试图片...');
120
+ const canvas = document.getElementById('testCanvas');
121
+ const ctx = canvas.getContext('2d');
122
+
123
+ // 清空画布
124
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
125
+
126
+ // 绘制背景
127
+ ctx.fillStyle = '#lightblue';
128
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
129
+
130
+ // 绘制红色矩形
131
+ ctx.fillStyle = '#red';
132
+ ctx.fillRect(50, 50, 100, 50);
133
+
134
+ // 绘制黄色圆形
135
+ ctx.fillStyle = '#yellow';
136
+ ctx.beginPath();
137
+ ctx.arc(100, 75, 20, 0, 2 * Math.PI);
138
+ ctx.fill();
139
+
140
+ // 绘制文字
141
+ ctx.fillStyle = '#black';
142
+ ctx.font = '16px Arial';
143
+ ctx.fillText('Test Image', 60, 120);
144
+
145
+ // 转换为 blob 并设置到文件输入
146
+ canvas.toBlob(blob => {
147
+ const file = new File([blob], 'test-image.png', { type: 'image/png' });
148
+ const dataTransfer = new DataTransfer();
149
+ dataTransfer.items.add(file);
150
+ document.getElementById('imageFile').files = dataTransfer.files;
151
+ log('测试图片创建完成并已选中');
152
+ });
153
+ }
154
+
155
+ function fileToBase64(file) {
156
+ return new Promise((resolve, reject) => {
157
+ const reader = new FileReader();
158
+ reader.readAsDataURL(file);
159
+ reader.onload = () => resolve(reader.result);
160
+ reader.onerror = error => reject(error);
161
+ });
162
+ }
163
+
164
+ async function testUpload() {
165
+ const fileInput = document.getElementById('imageFile');
166
+ const prompt = document.getElementById('prompt').value;
167
+ const resultDiv = document.getElementById('result');
168
+
169
+ if (!fileInput.files[0]) {
170
+ log('❌ 请先选择或创建测试图片');
171
+ return;
172
+ }
173
+
174
+ const file = fileInput.files[0];
175
+ log(`开始测试上传: ${file.name} (${file.size} bytes, ${file.type})`);
176
+
177
+ resultDiv.innerHTML = '<div class="result">🔄 正在分析...</div>';
178
+
179
+ try {
180
+ // 转换为 base64
181
+ log('转换图片为 base64...');
182
+ const base64 = await fileToBase64(file);
183
+ log(`Base64 转换完成,长度: ${base64.length}`);
184
+
185
+ // 发送请求
186
+ log('发送分析请求...');
187
+ const requestData = {
188
+ image: base64,
189
+ prompt: prompt
190
+ };
191
+
192
+ const response = await fetch('/analyze', {
193
+ method: 'POST',
194
+ headers: {
195
+ 'Content-Type': 'application/json'
196
+ },
197
+ body: JSON.stringify(requestData)
198
+ });
199
+
200
+ log(`收到响应: ${response.status} ${response.statusText}`);
201
+
202
+ if (!response.ok) {
203
+ const errorText = await response.text();
204
+ log(`❌ 响应错误: ${errorText}`);
205
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
206
+ }
207
+
208
+ const data = await response.json();
209
+ log(`响应数据: ${JSON.stringify(data, null, 2)}`);
210
+
211
+ if (data.success) {
212
+ resultDiv.innerHTML = `
213
+ <div class="result success">
214
+ <h4>✅ 分析成功</h4>
215
+ <p><strong>问题:</strong> ${data.prompt}</p>
216
+ <p><strong>回答:</strong> ${data.response}</p>
217
+ <p><strong>处理时间:</strong> ${data.processing_time.toFixed(2)}秒</p>
218
+ <p><strong>图片信息:</strong> ${data.image_info.size} (${data.image_info.mode})</p>
219
+ </div>
220
+ `;
221
+ log('✅ 分析成功完成');
222
+ } else {
223
+ resultDiv.innerHTML = `
224
+ <div class="result error">
225
+ <h4>❌ 分析失败</h4>
226
+ <p><strong>错误:</strong> ${data.error}</p>
227
+ </div>
228
+ `;
229
+ log(`❌ 分析失败: ${data.error}`);
230
+ }
231
+
232
+ } catch (error) {
233
+ log(`❌ 请求异常: ${error.message}`);
234
+ resultDiv.innerHTML = `
235
+ <div class="result error">
236
+ <h4>❌ 请求失败</h4>
237
+ <p><strong>错误:</strong> ${error.message}</p>
238
+ </div>
239
+ `;
240
+ }
241
+ }
242
+
243
+ // 页面加载时的初始化
244
+ window.addEventListener('load', () => {
245
+ log('页面加载完成');
246
+ log('可以创建测试图片或上传自己的图片进行测试');
247
+ });
248
+ </script>
249
+ </body>
250
+ </html>