openfree commited on
Commit
5cf6f94
·
verified ·
1 Parent(s): 70cb9de

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +23 -1200
app.py CHANGED
@@ -1,1212 +1,35 @@
1
- #!/usr/bin/env python
2
-
3
  import os
4
- import re
5
- import tempfile
6
- import gc # garbage collector
7
- from collections.abc import Iterator
8
- from threading import Thread
9
- import json
10
- import requests
11
- import cv2
12
- import gradio as gr
13
- import spaces
14
- import torch
15
- from loguru import logger
16
- from PIL import Image
17
- from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextIteratorStreamer
18
-
19
- # CSV/TXT analysis
20
- import pandas as pd
21
- # PDF text extraction
22
- import PyPDF2
23
-
24
- ##############################################################################
25
- # Memory cleanup function
26
- ##############################################################################
27
- def clear_cuda_cache():
28
- """Clear CUDA cache explicitly."""
29
- if torch.cuda.is_available():
30
- torch.cuda.empty_cache()
31
- gc.collect()
32
-
33
- ##############################################################################
34
- # SERPHouse API key from environment variable
35
- ##############################################################################
36
- SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
37
-
38
- ##############################################################################
39
- # Simple keyword extraction function
40
- ##############################################################################
41
- def extract_keywords(text: str, top_k: int = 5) -> str:
42
- """
43
- Extract keywords from text
44
- """
45
- text = re.sub(r"[^a-zA-Z0-9가-힣\s]", "", text)
46
- tokens = text.split()
47
- key_tokens = tokens[:top_k]
48
- return " ".join(key_tokens)
49
-
50
- ##############################################################################
51
- # SerpHouse Live endpoint call
52
- ##############################################################################
53
- def do_web_search(query: str) -> str:
54
- """
55
- Return top 20 'organic' results as JSON string
56
- """
57
- try:
58
- url = "https://api.serphouse.com/serp/live"
59
-
60
- # 기본 GET 방식으로 파라미터 간소화하고 결과 수를 20개로 제한
61
- params = {
62
- "q": query,
63
- "domain": "google.com",
64
- "serp_type": "web", # Basic web search
65
- "device": "desktop",
66
- "lang": "en",
67
- "num": "20" # Request max 20 results
68
- }
69
-
70
- headers = {
71
- "Authorization": f"Bearer {SERPHOUSE_API_KEY}"
72
- }
73
-
74
- logger.info(f"SerpHouse API call... query: {query}")
75
- logger.info(f"Request URL: {url} - params: {params}")
76
-
77
- # GET request
78
- response = requests.get(url, headers=headers, params=params, timeout=60)
79
- response.raise_for_status()
80
-
81
- logger.info(f"SerpHouse API response status: {response.status_code}")
82
- data = response.json()
83
-
84
- # Handle various response structures
85
- results = data.get("results", {})
86
- organic = None
87
-
88
- # Possible response structure 1
89
- if isinstance(results, dict) and "organic" in results:
90
- organic = results["organic"]
91
-
92
- # Possible response structure 2 (nested results)
93
- elif isinstance(results, dict) and "results" in results:
94
- if isinstance(results["results"], dict) and "organic" in results["results"]:
95
- organic = results["results"]["organic"]
96
-
97
- # Possible response structure 3 (top-level organic)
98
- elif "organic" in data:
99
- organic = data["organic"]
100
-
101
- if not organic:
102
- logger.warning("No organic results found in response.")
103
- logger.debug(f"Response structure: {list(data.keys())}")
104
- if isinstance(results, dict):
105
- logger.debug(f"results structure: {list(results.keys())}")
106
- return "No web search results found or unexpected API response structure."
107
-
108
- # Limit results and optimize context length
109
- max_results = min(20, len(organic))
110
- limited_organic = organic[:max_results]
111
-
112
- # Format results for better readability
113
- summary_lines = []
114
- for idx, item in enumerate(limited_organic, start=1):
115
- title = item.get("title", "No title")
116
- link = item.get("link", "#")
117
- snippet = item.get("snippet", "No description")
118
- displayed_link = item.get("displayed_link", link)
119
-
120
- # Markdown format
121
- summary_lines.append(
122
- f"### Result {idx}: {title}\n\n"
123
- f"{snippet}\n\n"
124
- f"**Source**: [{displayed_link}]({link})\n\n"
125
- f"---\n"
126
- )
127
-
128
- # Add simple instructions for model
129
- instructions = """
130
- # X-RAY Security Scanning Reference Results
131
- Use this information to enhance your analysis.
132
- """
133
-
134
- search_results = instructions + "\n".join(summary_lines)
135
- logger.info(f"Processed {len(limited_organic)} search results")
136
- return search_results
137
-
138
- except Exception as e:
139
- logger.error(f"Web search failed: {e}")
140
- return f"Web search failed: {str(e)}"
141
-
142
-
143
- ##############################################################################
144
- # Model/Processor loading
145
- ##############################################################################
146
- MAX_CONTENT_CHARS = 2000
147
- MAX_INPUT_LENGTH = 2096 # Max input token limit
148
- model_id = os.getenv("MODEL_ID", "VIDraft/Gemma-3-R1984-4B")
149
-
150
- processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
151
- model = Gemma3ForConditionalGeneration.from_pretrained(
152
- model_id,
153
- device_map="auto",
154
- torch_dtype=torch.bfloat16,
155
- attn_implementation="eager" # Change to "flash_attention_2" if available
156
- )
157
- MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
158
-
159
-
160
- ##############################################################################
161
- # CSV, TXT, PDF analysis functions
162
- ##############################################################################
163
- def analyze_csv_file(path: str) -> str:
164
- """
165
- Convert CSV file to string. Truncate if too long.
166
- """
167
- try:
168
- df = pd.read_csv(path)
169
- if df.shape[0] > 50 or df.shape[1] > 10:
170
- df = df.iloc[:50, :10]
171
- df_str = df.to_string()
172
- if len(df_str) > MAX_CONTENT_CHARS:
173
- df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
174
- return f"**[CSV File: {os.path.basename(path)}]**\n\n{df_str}"
175
- except Exception as e:
176
- return f"Failed to read CSV ({os.path.basename(path)}): {str(e)}"
177
-
178
-
179
- def analyze_txt_file(path: str) -> str:
180
- """
181
- Read TXT file. Truncate if too long.
182
- """
183
- try:
184
- with open(path, "r", encoding="utf-8") as f:
185
- text = f.read()
186
- if len(text) > MAX_CONTENT_CHARS:
187
- text = text[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
188
- return f"**[TXT File: {os.path.basename(path)}]**\n\n{text}"
189
- except Exception as e:
190
- return f"Failed to read TXT ({os.path.basename(path)}): {str(e)}"
191
-
192
-
193
- def pdf_to_markdown(pdf_path: str) -> str:
194
- """
195
- Convert PDF text to Markdown. Extract text by pages.
196
- """
197
- text_chunks = []
198
- try:
199
- with open(pdf_path, "rb") as f:
200
- reader = PyPDF2.PdfReader(f)
201
- max_pages = min(5, len(reader.pages))
202
- for page_num in range(max_pages):
203
- page = reader.pages[page_num]
204
- page_text = page.extract_text() or ""
205
- page_text = page_text.strip()
206
- if page_text:
207
- if len(page_text) > MAX_CONTENT_CHARS // max_pages:
208
- page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(truncated)"
209
- text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n")
210
- if len(reader.pages) > max_pages:
211
- text_chunks.append(f"\n...(Showing {max_pages} of {len(reader.pages)} pages)...")
212
- except Exception as e:
213
- return f"Failed to read PDF ({os.path.basename(pdf_path)}): {str(e)}"
214
-
215
- full_text = "\n".join(text_chunks)
216
- if len(full_text) > MAX_CONTENT_CHARS:
217
- full_text = full_text[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
218
-
219
- return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
220
-
221
-
222
- ##############################################################################
223
- # Image/Video upload limit check
224
- ##############################################################################
225
- def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
226
- image_count = 0
227
- video_count = 0
228
- for path in paths:
229
- if path.endswith(".mp4"):
230
- video_count += 1
231
- elif re.search(r"\.(png|jpg|jpeg|gif|webp)$", path, re.IGNORECASE):
232
- image_count += 1
233
- return image_count, video_count
234
-
235
-
236
- def count_files_in_history(history: list[dict]) -> tuple[int, int]:
237
- image_count = 0
238
- video_count = 0
239
- for item in history:
240
- if item["role"] != "user" or isinstance(item["content"], str):
241
- continue
242
- if isinstance(item["content"], list) and len(item["content"]) > 0:
243
- file_path = item["content"][0]
244
- if isinstance(file_path, str):
245
- if file_path.endswith(".mp4"):
246
- video_count += 1
247
- elif re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE):
248
- image_count += 1
249
- return image_count, video_count
250
-
251
-
252
- def validate_media_constraints(message: dict, history: list[dict]) -> bool:
253
- media_files = []
254
- for f in message["files"]:
255
- if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE) or f.endswith(".mp4"):
256
- media_files.append(f)
257
-
258
- new_image_count, new_video_count = count_files_in_new_message(media_files)
259
- history_image_count, history_video_count = count_files_in_history(history)
260
- image_count = history_image_count + new_image_count
261
- video_count = history_video_count + new_video_count
262
-
263
- if video_count > 1:
264
- gr.Warning("Only one video is supported.")
265
- return False
266
- if video_count == 1:
267
- if image_count > 0:
268
- gr.Warning("Mixing images and videos is not allowed.")
269
- return False
270
- if "<image>" in message["text"]:
271
- gr.Warning("Using <image> tags with video files is not supported.")
272
- return False
273
- if video_count == 0 and image_count > MAX_NUM_IMAGES:
274
- gr.Warning(f"You can upload up to {MAX_NUM_IMAGES} images.")
275
- return False
276
-
277
- if "<image>" in message["text"]:
278
- image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
279
- image_tag_count = message["text"].count("<image>")
280
- if image_tag_count != len(image_files):
281
- gr.Warning("The number of <image> tags in the text does not match the number of image files.")
282
- return False
283
-
284
- return True
285
-
286
-
287
- ##############################################################################
288
- # Video processing - with temp file tracking
289
- ##############################################################################
290
- def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
291
- vidcap = cv2.VideoCapture(video_path)
292
- fps = vidcap.get(cv2.CAP_PROP_FPS)
293
- total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
294
- frame_interval = max(int(fps), int(total_frames / 10))
295
- frames = []
296
-
297
- for i in range(0, total_frames, frame_interval):
298
- vidcap.set(cv2.CAP_PROP_POS_FRAMES, i)
299
- success, image = vidcap.read()
300
- if success:
301
- image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
302
- # Resize image
303
- image = cv2.resize(image, (0, 0), fx=0.5, fy=0.5)
304
- pil_image = Image.fromarray(image)
305
- timestamp = round(i / fps, 2)
306
- frames.append((pil_image, timestamp))
307
- if len(frames) >= 5:
308
- break
309
-
310
- vidcap.release()
311
- return frames
312
-
313
-
314
- def process_video(video_path: str) -> tuple[list[dict], list[str]]:
315
- content = []
316
- temp_files = [] # List for tracking temp files
317
-
318
- frames = downsample_video(video_path)
319
- for frame in frames:
320
- pil_image, timestamp = frame
321
- with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
322
- pil_image.save(temp_file.name)
323
- temp_files.append(temp_file.name) # Track for deletion later
324
- content.append({"type": "text", "text": f"Frame {timestamp}:"})
325
- content.append({"type": "image", "url": temp_file.name})
326
-
327
- return content, temp_files
328
-
329
-
330
- ##############################################################################
331
- # interleaved <image> processing
332
- ##############################################################################
333
- def process_interleaved_images(message: dict) -> list[dict]:
334
- parts = re.split(r"(<image>)", message["text"])
335
- content = []
336
- image_index = 0
337
-
338
- image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
339
-
340
- for part in parts:
341
- if part == "<image>" and image_index < len(image_files):
342
- content.append({"type": "image", "url": image_files[image_index]})
343
- image_index += 1
344
- elif part.strip():
345
- content.append({"type": "text", "text": part.strip()})
346
- else:
347
- if isinstance(part, str) and part != "<image>":
348
- content.append({"type": "text", "text": part})
349
- return content
350
-
351
-
352
- ##############################################################################
353
- # PDF + CSV + TXT + Image/Video
354
- ##############################################################################
355
- def is_image_file(file_path: str) -> bool:
356
- return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
357
-
358
- def is_video_file(file_path: str) -> bool:
359
- return file_path.endswith(".mp4")
360
-
361
- def is_document_file(file_path: str) -> bool:
362
- return (
363
- file_path.lower().endswith(".pdf")
364
- or file_path.lower().endswith(".csv")
365
- or file_path.lower().endswith(".txt")
366
- )
367
-
368
-
369
- def process_new_user_message(message: dict) -> tuple[list[dict], list[str]]:
370
- temp_files = [] # List for tracking temp files
371
-
372
- if not message["files"]:
373
- return [{"type": "text", "text": message["text"]}], temp_files
374
-
375
- video_files = [f for f in message["files"] if is_video_file(f)]
376
- image_files = [f for f in message["files"] if is_image_file(f)]
377
- csv_files = [f for f in message["files"] if f.lower().endswith(".csv")]
378
- txt_files = [f for f in message["files"] if f.lower().endswith(".txt")]
379
- pdf_files = [f for f in message["files"] if f.lower().endswith(".pdf")]
380
-
381
- content_list = [{"type": "text", "text": message["text"]}]
382
-
383
- for csv_path in csv_files:
384
- csv_analysis = analyze_csv_file(csv_path)
385
- content_list.append({"type": "text", "text": csv_analysis})
386
-
387
- for txt_path in txt_files:
388
- txt_analysis = analyze_txt_file(txt_path)
389
- content_list.append({"type": "text", "text": txt_analysis})
390
-
391
- for pdf_path in pdf_files:
392
- pdf_markdown = pdf_to_markdown(pdf_path)
393
- content_list.append({"type": "text", "text": pdf_markdown})
394
-
395
- if video_files:
396
- video_content, video_temp_files = process_video(video_files[0])
397
- content_list += video_content
398
- temp_files.extend(video_temp_files)
399
- return content_list, temp_files
400
-
401
- if "<image>" in message["text"] and image_files:
402
- interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
403
- if content_list and content_list[0]["type"] == "text":
404
- content_list = content_list[1:]
405
- return interleaved_content + content_list, temp_files
406
- else:
407
- for img_path in image_files:
408
- content_list.append({"type": "image", "url": img_path})
409
-
410
- return content_list, temp_files
411
-
412
-
413
- ##############################################################################
414
- # history -> LLM message conversion
415
- ##############################################################################
416
- def process_history(history: list[dict]) -> list[dict]:
417
- messages = []
418
- current_user_content: list[dict] = []
419
- for item in history:
420
- if item["role"] == "assistant":
421
- if current_user_content:
422
- messages.append({"role": "user", "content": current_user_content})
423
- current_user_content = []
424
- messages.append({"role": "assistant", "content": [{"type": "text", "text": item["content"]}]})
425
- else:
426
- content = item["content"]
427
- if isinstance(content, str):
428
- current_user_content.append({"type": "text", "text": content})
429
- elif isinstance(content, list) and len(content) > 0:
430
- file_path = content[0]
431
- if is_image_file(file_path):
432
- current_user_content.append({"type": "image", "url": file_path})
433
- else:
434
- current_user_content.append({"type": "text", "text": f"[File: {os.path.basename(file_path)}]"})
435
-
436
- if current_user_content:
437
- messages.append({"role": "user", "content": current_user_content})
438
-
439
- return messages
440
-
441
-
442
- ##############################################################################
443
- # Model generation function with OOM catch
444
- ##############################################################################
445
- def _model_gen_with_oom_catch(**kwargs):
446
- """
447
- Catch OutOfMemoryError in separate thread
448
- """
449
- try:
450
- model.generate(**kwargs)
451
- except torch.cuda.OutOfMemoryError:
452
- raise RuntimeError(
453
- "[OutOfMemoryError] GPU memory insufficient. "
454
- "Please reduce Max New Tokens or prompt length."
455
- )
456
- finally:
457
- # Clear cache after generation
458
- clear_cuda_cache()
459
-
460
-
461
- ##############################################################################
462
- # Main inference function (with auto web search)
463
- ##############################################################################
464
- @spaces.GPU(duration=120)
465
- def run(
466
- message: dict,
467
- history: list[dict],
468
- system_prompt: str = "",
469
- max_new_tokens: int = 512,
470
- use_web_search: bool = False,
471
- web_search_query: str = "",
472
- ) -> Iterator[str]:
473
 
474
- if not validate_media_constraints(message, history):
475
- yield ""
476
- return
477
-
478
- temp_files = [] # For tracking temp files
479
-
480
  try:
481
- combined_system_msg = ""
482
-
483
- # Used internally only (hidden from UI)
484
- if system_prompt.strip():
485
- combined_system_msg += f"[System Prompt]\n{system_prompt.strip()}\n\n"
486
-
487
- if use_web_search:
488
- user_text = message["text"]
489
- ws_query = extract_keywords(user_text, top_k=5)
490
- if ws_query.strip():
491
- logger.info(f"[Auto WebSearch Keyword] {ws_query!r}")
492
- ws_result = do_web_search(ws_query)
493
- combined_system_msg += f"[X-RAY Security Reference Data]\n{ws_result}\n\n"
494
- else:
495
- combined_system_msg += "[No valid keywords found, skipping WebSearch]\n\n"
496
-
497
- messages = []
498
- if combined_system_msg.strip():
499
- messages.append({
500
- "role": "system",
501
- "content": [{"type": "text", "text": combined_system_msg.strip()}],
502
- })
503
-
504
- messages.extend(process_history(history))
505
-
506
- user_content, user_temp_files = process_new_user_message(message)
507
- temp_files.extend(user_temp_files) # Track temp files
508
 
509
- for item in user_content:
510
- if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
511
- item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
512
- messages.append({"role": "user", "content": user_content})
513
-
514
- inputs = processor.apply_chat_template(
515
- messages,
516
- add_generation_prompt=True,
517
- tokenize=True,
518
- return_dict=True,
519
- return_tensors="pt",
520
- ).to(device=model.device, dtype=torch.bfloat16)
521
 
522
- # Limit input token count
523
- if inputs.input_ids.shape[1] > MAX_INPUT_LENGTH:
524
- inputs.input_ids = inputs.input_ids[:, -MAX_INPUT_LENGTH:]
525
- if 'attention_mask' in inputs:
526
- inputs.attention_mask = inputs.attention_mask[:, -MAX_INPUT_LENGTH:]
527
 
528
- streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
529
- gen_kwargs = dict(
530
- inputs,
531
- streamer=streamer,
532
- max_new_tokens=max_new_tokens,
533
- )
534
-
535
- t = Thread(target=_model_gen_with_oom_catch, kwargs=gen_kwargs)
536
- t.start()
537
-
538
- output = ""
539
- for new_text in streamer:
540
- output += new_text
541
- yield output
542
-
543
- except Exception as e:
544
- logger.error(f"Error in run: {str(e)}")
545
- yield f"Error occurred: {str(e)}"
546
-
547
- finally:
548
- # Delete temp files
549
- for temp_file in temp_files:
550
- try:
551
- if os.path.exists(temp_file):
552
- os.unlink(temp_file)
553
- logger.info(f"Deleted temp file: {temp_file}")
554
- except Exception as e:
555
- logger.warning(f"Failed to delete temp file {temp_file}: {e}")
556
 
557
- # Explicit memory cleanup
558
  try:
559
- del inputs, streamer
560
  except:
561
  pass
562
-
563
- clear_cuda_cache()
564
-
565
-
566
- ##############################################################################
567
- # Gradio UI (Blocks) 구성
568
- ##############################################################################
569
- css = """
570
- /* Global Styles */
571
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
572
-
573
- * {
574
- box-sizing: border-box;
575
- }
576
-
577
- body {
578
- margin: 0;
579
- padding: 0;
580
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
581
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
582
- min-height: 100vh;
583
- color: #2d3748;
584
- }
585
-
586
- /* Container Styling */
587
- .gradio-container {
588
- background: rgba(255, 255, 255, 0.95);
589
- backdrop-filter: blur(20px);
590
- border-radius: 24px;
591
- padding: 40px;
592
- margin: 30px auto;
593
- width: 95% !important;
594
- max-width: 1400px !important;
595
- box-shadow:
596
- 0 25px 50px -12px rgba(0, 0, 0, 0.25),
597
- 0 0 0 1px rgba(255, 255, 255, 0.05);
598
- border: 1px solid rgba(255, 255, 255, 0.2);
599
- }
600
-
601
- /* Header Styling */
602
- .header-container {
603
- text-align: center;
604
- margin-bottom: 2rem;
605
- padding: 2rem 0;
606
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 50%, #4facfe 100%);
607
- background-clip: text;
608
- -webkit-background-clip: text;
609
- -webkit-text-fill-color: transparent;
610
- }
611
-
612
- /* Button Styling */
613
- button, .btn {
614
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
615
- border: none !important;
616
- color: white !important;
617
- padding: 12px 28px !important;
618
- border-radius: 12px !important;
619
- font-weight: 600 !important;
620
- font-size: 14px !important;
621
- text-transform: none !important;
622
- letter-spacing: 0.5px !important;
623
- cursor: pointer !important;
624
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
625
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important;
626
- position: relative !important;
627
- overflow: hidden !important;
628
- }
629
-
630
- button:hover, .btn:hover {
631
- transform: translateY(-2px) !important;
632
- box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6) !important;
633
- background: linear-gradient(135deg, #764ba2 0%, #667eea 100%) !important;
634
- }
635
-
636
- button:active, .btn:active {
637
- transform: translateY(0) !important;
638
- }
639
-
640
- /* Primary Action Button */
641
- button[variant="primary"], .primary-btn {
642
- background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%) !important;
643
- box-shadow: 0 4px 15px rgba(255, 107, 107, 0.4) !important;
644
- }
645
-
646
- button[variant="primary"]:hover, .primary-btn:hover {
647
- box-shadow: 0 8px 25px rgba(255, 107, 107, 0.6) !important;
648
- }
649
-
650
- /* Input Fields */
651
- .multimodal-textbox, textarea, input {
652
- background: rgba(255, 255, 255, 0.8) !important;
653
- backdrop-filter: blur(10px) !important;
654
- border: 2px solid rgba(102, 126, 234, 0.2) !important;
655
- border-radius: 16px !important;
656
- color: #2d3748 !important;
657
- font-family: 'Inter', sans-serif !important;
658
- padding: 16px 20px !important;
659
- transition: all 0.3s ease !important;
660
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1) !important;
661
- }
662
-
663
- .multimodal-textbox:focus, textarea:focus, input:focus {
664
- border-color: #667eea !important;
665
- box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1), 0 8px 30px rgba(0, 0, 0, 0.15) !important;
666
- outline: none !important;
667
- background: rgba(255, 255, 255, 0.95) !important;
668
- }
669
-
670
- /* Chat Interface */
671
- .chatbox, .chatbot {
672
- background: rgba(255, 255, 255, 0.6) !important;
673
- backdrop-filter: blur(15px) !important;
674
- border-radius: 20px !important;
675
- border: 1px solid rgba(255, 255, 255, 0.3) !important;
676
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1) !important;
677
- padding: 24px !important;
678
- }
679
-
680
- .message {
681
- background: rgba(255, 255, 255, 0.9) !important;
682
- border-radius: 16px !important;
683
- padding: 16px 20px !important;
684
- margin: 8px 0 !important;
685
- border: 1px solid rgba(102, 126, 234, 0.1) !important;
686
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important;
687
- transition: all 0.3s ease !important;
688
- }
689
-
690
- .message:hover {
691
- transform: translateY(-1px) !important;
692
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important;
693
- }
694
-
695
- /* Assistant Message Styling */
696
- .message.assistant {
697
- background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%) !important;
698
- border-left: 4px solid #667eea !important;
699
- }
700
-
701
- /* User Message Styling */
702
- .message.user {
703
- background: linear-gradient(135deg, rgba(255, 107, 107, 0.1) 0%, rgba(238, 90, 82, 0.1) 100%) !important;
704
- border-left: 4px solid #ff6b6b !important;
705
- }
706
-
707
- /* Cards and Panels */
708
- .card, .panel {
709
- background: rgba(255, 255, 255, 0.8) !important;
710
- backdrop-filter: blur(15px) !important;
711
- border-radius: 20px !important;
712
- padding: 24px !important;
713
- border: 1px solid rgba(255, 255, 255, 0.3) !important;
714
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1) !important;
715
- transition: all 0.3s ease !important;
716
- }
717
-
718
- .card:hover, .panel:hover {
719
- transform: translateY(-4px) !important;
720
- box-shadow: 0 16px 40px rgba(0, 0, 0, 0.15) !important;
721
- }
722
-
723
- /* Checkbox Styling */
724
- input[type="checkbox"] {
725
- appearance: none !important;
726
- width: 20px !important;
727
- height: 20px !important;
728
- border: 2px solid #667eea !important;
729
- border-radius: 6px !important;
730
- background: rgba(255, 255, 255, 0.8) !important;
731
- cursor: pointer !important;
732
- transition: all 0.3s ease !important;
733
- position: relative !important;
734
- }
735
-
736
- input[type="checkbox"]:checked {
737
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
738
- border-color: #667eea !important;
739
- }
740
-
741
- input[type="checkbox"]:checked::after {
742
- content: "✓" !important;
743
- color: white !important;
744
- font-size: 14px !important;
745
- font-weight: bold !important;
746
- position: absolute !important;
747
- top: 50% !important;
748
- left: 50% !important;
749
- transform: translate(-50%, -50%) !important;
750
- }
751
-
752
- /* Progress Indicators */
753
- .progress {
754
- background: linear-gradient(90deg, #667eea 0%, #764ba2 100%) !important;
755
- border-radius: 10px !important;
756
- height: 8px !important;
757
- }
758
-
759
- /* Tooltips */
760
- .tooltip {
761
- background: rgba(45, 55, 72, 0.95) !important;
762
- backdrop-filter: blur(10px) !important;
763
- color: white !important;
764
- border-radius: 8px !important;
765
- padding: 8px 12px !important;
766
- font-size: 12px !important;
767
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3) !important;
768
- }
769
-
770
- /* Slider Styling */
771
- input[type="range"] {
772
- appearance: none !important;
773
- height: 8px !important;
774
- border-radius: 4px !important;
775
- background: linear-gradient(90deg, #e2e8f0 0%, #667eea 100%) !important;
776
- outline: none !important;
777
- }
778
-
779
- input[type="range"]::-webkit-slider-thumb {
780
- appearance: none !important;
781
- width: 20px !important;
782
- height: 20px !important;
783
- border-radius: 50% !important;
784
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
785
- cursor: pointer !important;
786
- box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4) !important;
787
- }
788
-
789
- /* File Upload Area */
790
- .file-upload {
791
- border: 2px dashed #667eea !important;
792
- border-radius: 16px !important;
793
- background: rgba(102, 126, 234, 0.05) !important;
794
- padding: 40px !important;
795
- text-align: center !important;
796
- transition: all 0.3s ease !important;
797
- }
798
-
799
- .file-upload:hover {
800
- border-color: #764ba2 !important;
801
- background: rgba(102, 126, 234, 0.1) !important;
802
- transform: scale(1.02) !important;
803
- }
804
-
805
- /* Animations */
806
- @keyframes fadeInUp {
807
- from {
808
- opacity: 0;
809
- transform: translateY(30px);
810
- }
811
- to {
812
- opacity: 1;
813
- transform: translateY(0);
814
- }
815
- }
816
-
817
- @keyframes slideIn {
818
- from {
819
- opacity: 0;
820
- transform: translateX(-20px);
821
- }
822
- to {
823
- opacity: 1;
824
- transform: translateX(0);
825
- }
826
- }
827
-
828
- .animate-fade-in {
829
- animation: fadeInUp 0.6s ease-out !important;
830
- }
831
-
832
- .animate-slide-in {
833
- animation: slideIn 0.4s ease-out !important;
834
- }
835
-
836
- /* Responsive Design */
837
- @media (max-width: 768px) {
838
- .gradio-container {
839
- margin: 15px !important;
840
- padding: 24px !important;
841
- width: calc(100% - 30px) !important;
842
- }
843
-
844
- button, .btn {
845
- padding: 10px 20px !important;
846
- font-size: 13px !important;
847
- }
848
- }
849
-
850
- /* Dark Mode Support */
851
- @media (prefers-color-scheme: dark) {
852
- .gradio-container {
853
- background: rgba(26, 32, 44, 0.95) !important;
854
- color: #e2e8f0 !important;
855
- }
856
-
857
- .message {
858
- background: rgba(45, 55, 72, 0.8) !important;
859
- color: #e2e8f0 !important;
860
- }
861
- }
862
-
863
- /* Hide Footer - Safe and Specific Selectors */
864
- footer {
865
- visibility: hidden !important;
866
- display: none !important;
867
- }
868
-
869
- .footer {
870
- visibility: hidden !important;
871
- display: none !important;
872
- }
873
-
874
- /* Hide only Gradio attribution footer specifically */
875
- footer[class*="svelte"] {
876
- visibility: hidden !important;
877
- display: none !important;
878
- }
879
-
880
- /* Hide Gradio attribution links */
881
- a[href*="gradio.app"] {
882
- visibility: hidden !important;
883
- display: none !important;
884
- }
885
-
886
- /* More specific footer hiding for Gradio */
887
- .gradio-container footer,
888
- .gradio-container .footer {
889
- visibility: hidden !important;
890
- display: none !important;
891
- }
892
-
893
- /* Custom Scrollbar */
894
- ::-webkit-scrollbar {
895
- width: 8px !important;
896
- }
897
-
898
- ::-webkit-scrollbar-track {
899
- background: rgba(226, 232, 240, 0.3) !important;
900
- border-radius: 4px !important;
901
- }
902
-
903
- ::-webkit-scrollbar-thumb {
904
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
905
- border-radius: 4px !important;
906
- }
907
-
908
- ::-webkit-scrollbar-thumb:hover {
909
- background: linear-gradient(135deg, #764ba2 0%, #667eea 100%) !important;
910
- }
911
- """
912
-
913
- title_html = """
914
- <div align="center" style="margin-bottom: 2em; padding: 2rem 0;" class="animate-fade-in">
915
- <div style="
916
- background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
917
- background-clip: text;
918
- -webkit-background-clip: text;
919
- -webkit-text-fill-color: transparent;
920
- margin-bottom: 1rem;
921
- ">
922
- <h1 style="
923
- margin: 0;
924
- font-size: 3.5em;
925
- font-weight: 700;
926
- letter-spacing: -0.02em;
927
- text-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
928
- ">
929
- 🤖 Robo Beam-Search
930
- </h1>
931
- </div>
932
-
933
- <div style="
934
- background: rgba(255, 255, 255, 0.9);
935
- backdrop-filter: blur(15px);
936
- border-radius: 16px;
937
- padding: 1.5rem 2rem;
938
- margin: 1rem auto;
939
- max-width: 700px;
940
- border: 1px solid rgba(102, 126, 234, 0.2);
941
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
942
- ">
943
- <p style="
944
- margin: 0.5em 0;
945
- font-size: 1.1em;
946
- color: #4a5568;
947
- font-weight: 500;
948
- ">
949
- <span style="
950
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
951
- background-clip: text;
952
- -webkit-background-clip: text;
953
- -webkit-text-fill-color: transparent;
954
- font-weight: 600;
955
- ">Base LLM:</span> VIDraft/Gemma-3-R1984-4B
956
- </p>
957
- <p style="
958
- margin: 1em 0 0 0;
959
- font-size: 1em;
960
- color: #718096;
961
- line-height: 1.6;
962
- font-weight: 400;
963
- ">
964
- 비파괴 X-RAY 검사/조사 이미지에 대한 위험 요소 식별/분석 기반 대화형 온프레미스 AI 플랫폼
965
- </p>
966
- </div>
967
-
968
- <div style="
969
- display: flex;
970
- justify-content: center;
971
- gap: 1rem;
972
- margin-top: 2rem;
973
- flex-wrap: wrap;
974
- ">
975
- <div style="
976
- background: rgba(102, 126, 234, 0.1);
977
- border: 1px solid rgba(102, 126, 234, 0.3);
978
- border-radius: 12px;
979
- padding: 0.5rem 1rem;
980
- font-size: 0.9em;
981
- color: #667eea;
982
- font-weight: 500;
983
- ">
984
- 🔍 X-RAY 분석
985
- </div>
986
- <div style="
987
- background: rgba(118, 75, 162, 0.1);
988
- border: 1px solid rgba(118, 75, 162, 0.3);
989
- border-radius: 12px;
990
- padding: 0.5rem 1rem;
991
- font-size: 0.9em;
992
- color: #764ba2;
993
- font-weight: 500;
994
- ">
995
- 🛡️ 보안 스캐닝
996
- </div>
997
- <div style="
998
- background: rgba(240, 147, 251, 0.1);
999
- border: 1px solid rgba(240, 147, 251, 0.3);
1000
- border-radius: 12px;
1001
- padding: 0.5rem 1rem;
1002
- font-size: 0.9em;
1003
- color: #f093fb;
1004
- font-weight: 500;
1005
- ">
1006
- 🌐 웹 검색
1007
- </div>
1008
- </div>
1009
- </div>
1010
- """
1011
-
1012
- title_html = """
1013
- <div align="center" style="margin-bottom: 2em; padding: 2rem 0;" class="animate-fade-in">
1014
- <div style="
1015
- background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
1016
- background-clip: text;
1017
- -webkit-background-clip: text;
1018
- -webkit-text-fill-color: transparent;
1019
- margin-bottom: 1rem;
1020
- ">
1021
- <h1 style="
1022
- margin: 0;
1023
- font-size: 3.5em;
1024
- font-weight: 700;
1025
- letter-spacing: -0.02em;
1026
- text-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
1027
- ">
1028
- 🤖 Robo Beam-Search
1029
- </h1>
1030
- </div>
1031
-
1032
- <div style="
1033
- background: rgba(255, 255, 255, 0.9);
1034
- backdrop-filter: blur(15px);
1035
- border-radius: 16px;
1036
- padding: 1.5rem 2rem;
1037
- margin: 1rem auto;
1038
- max-width: 700px;
1039
- border: 1px solid rgba(102, 126, 234, 0.2);
1040
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
1041
- ">
1042
- <p style="
1043
- margin: 0.5em 0;
1044
- font-size: 1.1em;
1045
- color: #4a5568;
1046
- font-weight: 500;
1047
- ">
1048
- <span style="
1049
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1050
- background-clip: text;
1051
- -webkit-background-clip: text;
1052
- -webkit-text-fill-color: transparent;
1053
- font-weight: 600;
1054
- ">Base LLM:</span> VIDraft/Gemma-3-R1984-4B
1055
- </p>
1056
- <p style="
1057
- margin: 1em 0 0 0;
1058
- font-size: 1em;
1059
- color: #718096;
1060
- line-height: 1.6;
1061
- font-weight: 400;
1062
- ">
1063
- 비파괴 X-RAY 검사/조사 이미지에 대한 위험 요소 식별/분석 기반 대화형 온프레미스 AI 플랫폼
1064
- </p>
1065
- </div>
1066
-
1067
- <div style="
1068
- display: flex;
1069
- justify-content: center;
1070
- gap: 1rem;
1071
- margin-top: 2rem;
1072
- flex-wrap: wrap;
1073
- ">
1074
- <div style="
1075
- background: rgba(102, 126, 234, 0.1);
1076
- border: 1px solid rgba(102, 126, 234, 0.3);
1077
- border-radius: 12px;
1078
- padding: 0.5rem 1rem;
1079
- font-size: 0.9em;
1080
- color: #667eea;
1081
- font-weight: 500;
1082
- ">
1083
- 🔍 X-RAY 분석
1084
- </div>
1085
- <div style="
1086
- background: rgba(118, 75, 162, 0.1);
1087
- border: 1px solid rgba(118, 75, 162, 0.3);
1088
- border-radius: 12px;
1089
- padding: 0.5rem 1rem;
1090
- font-size: 0.9em;
1091
- color: #764ba2;
1092
- font-weight: 500;
1093
- ">
1094
- 🛡️ 보안 스캐닝
1095
- </div>
1096
- <div style="
1097
- background: rgba(240, 147, 251, 0.1);
1098
- border: 1px solid rgba(240, 147, 251, 0.3);
1099
- border-radius: 12px;
1100
- padding: 0.5rem 1rem;
1101
- font-size: 0.9em;
1102
- color: #f093fb;
1103
- font-weight: 500;
1104
- ">
1105
- 🌐 웹 검색
1106
- </div>
1107
- </div>
1108
- </div>
1109
- """
1110
-
1111
-
1112
-
1113
- title_html = """
1114
- <div align="center" style="margin-bottom: 1em;">
1115
- <h1 style="margin-bottom: 0.2em; font-size: 1.8em; color: #333;">🤖 Robo Beam-Search</h1>
1116
- <p style="margin: 0.5em 0; font-size: 0.9em; color: #888; max-width: 600px; margin-left: auto; margin-right: auto;">
1117
- 비파괴 X-RAY 검사/조사 이미지에 대한 위험 요소 식별/분석 기반 대화형 온프레미스 AI 플랫폼 <strong>Base LLM:</strong> Gemma-3-R1984-4B / 12B/ 27B @Powered by VIDraft
1118
- </p>
1119
- </div>
1120
- """
1121
-
1122
-
1123
- with gr.Blocks(css=css, title="Gemma-3-R1984-4B-BEAM - X-RAY Security Scanner") as demo:
1124
- gr.Markdown(title_html)
1125
-
1126
- # Display the web search option (while the system prompt and token slider remain hidden)
1127
- web_search_checkbox = gr.Checkbox(
1128
- label="Deep Research",
1129
- value=False
1130
- )
1131
-
1132
- # X-RAY security scanning system prompt
1133
- system_prompt_box = gr.Textbox(
1134
- lines=3,
1135
- value="""반드시 한글로 답변하라. 당신은 위협 탐지와 항공 보안에 특화된 첨단 X-RAY 보안 스캐닝 AI입니다. 당신의 주 임무는 X-RAY 이미지에서 모든 잠재적 보안 위협을 최상의 정확도로 식별하는 것입니다.
1136
-
1137
- 중요: 보고서에 날짜, 시간, 또는 현재 일시를 절대 포함하지 마십시오.
1138
-
1139
- 탐지 우선순위:
1140
- 1. **무기**: 화기(권총, 소총 등), 칼·날붙이·예리한 물체, 호신용·격투 무기
1141
- 2. **폭발물**: 폭탄, 기폭장치, 폭발성 물질, 의심스러운 전자 장치, 배터리가 연결된 전선
1142
- 3. **반입 금지 물품**: 가위, 대용량 배터리, 스프링(무기 부품 가능), 공구류
1143
- 4. **액체**: 100 ml 이상 용기에 담긴 모든 액체(화학 위협 가능)
1144
- 5. **EOD 구성품**: 폭발물로 조립될 수 있는 모든 부품
1145
-
1146
- 분석 프로토콜:
1147
- - 좌상단에서 우하단으로 체계적으로 스캔
1148
- - 위협 위치를 격자 기준으로 보고(예: “좌상단 사분면”)
1149
- - 위협 심각도 분류
1150
- - **HIGH** : 즉각적 위험
1151
- - **MEDIUM** : 반입 금지
1152
- - **LOW** : 추가 검사 필요
1153
- - 전문 보안 용어 사용
1154
- - 각 위협 항목별 권장 조치 제시
1155
- - 보고서에는 분석 결과만 포함하고 날짜/���간 정보는 포함하지 않음
1156
-
1157
- ⚠️ 중대한 사항: 잠재적 위협을 절대 놓치지 마십시오. 의심스러울 경우 반드시 수동 검사를 요청하십시오.""",
1158
- visible=False # hidden from view
1159
- )
1160
-
1161
-
1162
-
1163
- max_tokens_slider = gr.Slider(
1164
- label="Max New Tokens",
1165
- minimum=100,
1166
- maximum=8000,
1167
- step=50,
1168
- value=1000,
1169
- visible=False # hidden from view
1170
- )
1171
-
1172
- web_search_text = gr.Textbox(
1173
- lines=1,
1174
- label="Web Search Query",
1175
- placeholder="",
1176
- visible=False # hidden from view
1177
- )
1178
-
1179
- # Configure the chat interface
1180
- chat = gr.ChatInterface(
1181
- fn=run,
1182
- type="messages",
1183
- chatbot=gr.Chatbot(type="messages", scale=1, allow_tags=["image"]),
1184
- textbox=gr.MultimodalTextbox(
1185
- file_types=[
1186
- ".webp", ".png", ".jpg", ".jpeg", ".gif",
1187
- ".mp4", ".csv", ".txt", ".pdf"
1188
- ],
1189
- file_count="multiple",
1190
- autofocus=True
1191
- ),
1192
- multimodal=True,
1193
- additional_inputs=[
1194
- system_prompt_box,
1195
- max_tokens_slider,
1196
- web_search_checkbox,
1197
- web_search_text,
1198
- ],
1199
- stop_btn=False,
1200
-
1201
- run_examples_on_click=False,
1202
- cache_examples=False,
1203
- css_paths=None,
1204
- delete_cache=(1800, 1800),
1205
- )
1206
-
1207
-
1208
-
1209
 
1210
  if __name__ == "__main__":
1211
- # Run locally
1212
- demo.launch()
 
 
 
1
  import os
2
+ import sys
3
+ import streamlit as st
4
+ from tempfile import NamedTemporaryFile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ def main():
 
 
 
 
 
7
  try:
8
+ # Get the code from secrets
9
+ code = os.environ.get("MAIN_CODE")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ if not code:
12
+ st.error("⚠️ The application code wasn't found in secrets. Please add the MAIN_CODE secret.")
13
+ return
 
 
 
 
 
 
 
 
 
14
 
15
+ # Create a temporary Python file
16
+ with NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp:
17
+ tmp.write(code)
18
+ tmp_path = tmp.name
 
19
 
20
+ # Execute the code
21
+ exec(compile(code, tmp_path, 'exec'), globals())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ # Clean up the temporary file
24
  try:
25
+ os.unlink(tmp_path)
26
  except:
27
  pass
28
+
29
+ except Exception as e:
30
+ st.error(f"⚠️ Error loading or executing the application: {str(e)}")
31
+ import traceback
32
+ st.code(traceback.format_exc())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  if __name__ == "__main__":
35
+ main()