seawolf2357 commited on
Commit
477027f
Β·
verified Β·
1 Parent(s): 5324733

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +276 -259
app.py CHANGED
@@ -1,137 +1,193 @@
1
  import gradio as gr
2
- import cv2
3
- import tempfile
4
  import os
5
- from PIL import Image
6
 
7
- def extract_last_frame(video_file):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  """
9
- λΉ„λ””μ˜€ νŒŒμΌμ—μ„œ λ§ˆμ§€λ§‰ ν”„λ ˆμž„μ„ μΆ”μΆœν•˜μ—¬ μ΄λ―Έμ§€λ‘œ λ°˜ν™˜
 
10
  """
11
- if video_file is None:
12
- return None, "⚠️ Please upload a video file first!"
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  try:
15
- # OpenCV둜 λΉ„λ””μ˜€ μ—΄κΈ°
16
- cap = cv2.VideoCapture(video_file)
17
-
18
- if not cap.isOpened():
19
- return None, "❌ Error: Cannot open video file!"
20
-
21
- # λΉ„λ””μ˜€ 정보 κ°€μ Έμ˜€κΈ°
22
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
23
- fps = cap.get(cv2.CAP_PROP_FPS)
24
- width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
25
- height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
26
- duration = total_frames / fps if fps > 0 else 0
27
-
28
- if total_frames <= 0:
29
- cap.release()
30
- return None, "❌ Error: Video has no frames!"
31
-
32
- # λ§ˆμ§€λ§‰ ν”„λ ˆμž„μœΌλ‘œ 이동
33
- cap.set(cv2.CAP_PROP_POS_FRAMES, total_frames - 1)
34
-
35
- # ν”„οΏ½οΏ½οΏ½μž„ 읽기
36
- ret, frame = cap.read()
37
- cap.release()
38
-
39
- if not ret:
40
- return None, "❌ Error: Cannot read the last frame!"
41
-
42
- # BGR to RGB λ³€ν™˜
43
- frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
44
-
45
- # PIL Image둜 λ³€ν™˜
46
- image = Image.fromarray(frame_rgb)
47
 
48
- # 정보 둜그 생성
49
- info_log = f"""βœ… EXTRACTION COMPLETE!
50
- {'=' * 50}
51
- πŸ“Ή Video Info:
52
- β€’ Total Frames: {total_frames:,}
53
- β€’ FPS: {fps:.2f}
54
- β€’ Duration: {duration:.2f} seconds
55
- β€’ Resolution: {width} x {height}
56
- {'=' * 50}
57
- πŸ–ΌοΈ Extracted Frame:
58
- β€’ Frame Number: {total_frames} (Last Frame)
59
- β€’ Image Size: {width} x {height}
60
- {'=' * 50}
61
- πŸ’Ύ Ready to download!"""
62
-
63
- return image, info_log
64
 
65
  except Exception as e:
66
- return None, f"❌ Error: {str(e)}"
 
 
 
 
 
 
 
67
 
 
 
 
68
 
69
  # ============================================
70
- # 🎨 Comic Classic Theme - Toon Playground
71
  # ============================================
72
-
73
  css = """
74
  /* ===== 🎨 Google Fonts Import ===== */
75
  @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
76
 
77
- /* ===== 🎨 Comic Classic λ°°κ²½ - λΉˆν‹°μ§€ 페이퍼 + λ„νŠΈ νŒ¨ν„΄ ===== */
78
  .gradio-container {
79
  background-color: #FEF9C3 !important;
80
- background-image:
81
- radial-gradient(#1F2937 1px, transparent 1px) !important;
82
  background-size: 20px 20px !important;
83
  min-height: 100vh !important;
84
  font-family: 'Comic Neue', cursive, sans-serif !important;
85
  }
86
 
87
- /* ===== ν—ˆκΉ…νŽ˜μ΄μŠ€ 상단 μš”μ†Œ μˆ¨κΉ€ ===== */
88
  .huggingface-space-header,
89
  #space-header,
90
  .space-header,
91
  [class*="space-header"],
92
  .svelte-1ed2p3z,
93
  .space-header-badge,
94
- .header-badge,
95
- [data-testid="space-header"],
96
- .svelte-kqij2n,
97
- .svelte-1ax1toq,
98
- .embed-container > div:first-child {
99
  display: none !important;
100
  visibility: hidden !important;
101
  height: 0 !important;
102
  width: 0 !important;
103
- overflow: hidden !important;
104
- opacity: 0 !important;
105
- pointer-events: none !important;
106
  }
107
 
108
- /* ===== Footer μ™„μ „ μˆ¨κΉ€ ===== */
109
  footer,
110
  .footer,
111
  .gradio-container footer,
112
  .built-with,
113
  [class*="footer"],
114
  .gradio-footer,
115
- .main-footer,
116
- div[class*="footer"],
117
  .show-api,
118
- .built-with-gradio,
119
- a[href*="gradio.app"],
120
- a[href*="huggingface.co/spaces"] {
121
  display: none !important;
122
  visibility: hidden !important;
123
  height: 0 !important;
124
- padding: 0 !important;
125
- margin: 0 !important;
126
  }
127
 
128
- /* ===== 메인 μ»¨ν…Œμ΄λ„ˆ ===== */
129
- #col-container {
130
- max-width: 1000px;
131
- margin: 0 auto;
132
- }
133
-
134
- /* ===== 🎨 헀더 타이틀 - μ½”λ―Ή μŠ€νƒ€μΌ ===== */
135
  .header-text h1 {
136
  font-family: 'Bangers', cursive !important;
137
  color: #1F2937 !important;
@@ -156,7 +212,7 @@ a[href*="huggingface.co/spaces"] {
156
  font-weight: 700 !important;
157
  }
158
 
159
- /* ===== 🎨 μΉ΄λ“œ/νŒ¨λ„ - λ§Œν™” ν”„λ ˆμž„ μŠ€νƒ€μΌ ===== */
160
  .gr-panel,
161
  .gr-box,
162
  .gr-form,
@@ -175,7 +231,7 @@ a[href*="huggingface.co/spaces"] {
175
  box-shadow: 8px 8px 0px #1F2937 !important;
176
  }
177
 
178
- /* ===== 🎨 μž…λ ₯ ν•„λ“œ (Textbox) ===== */
179
  textarea,
180
  input[type="text"],
181
  input[type="number"] {
@@ -186,23 +242,16 @@ input[type="number"] {
186
  font-family: 'Comic Neue', cursive !important;
187
  font-size: 1rem !important;
188
  font-weight: 700 !important;
189
- transition: all 0.2s ease !important;
190
  }
191
 
192
  textarea:focus,
193
- input[type="text"]:focus,
194
- input[type="number"]:focus {
195
  border-color: #3B82F6 !important;
196
  box-shadow: 4px 4px 0px #3B82F6 !important;
197
  outline: none !important;
198
  }
199
 
200
- textarea::placeholder {
201
- color: #9CA3AF !important;
202
- font-weight: 400 !important;
203
- }
204
-
205
- /* ===== 🎨 Primary λ²„νŠΌ - μ½”λ―Ή 블루 ===== */
206
  .gr-button-primary,
207
  button.primary,
208
  .gr-button.primary {
@@ -216,29 +265,25 @@ button.primary,
216
  letter-spacing: 2px !important;
217
  padding: 14px 28px !important;
218
  box-shadow: 5px 5px 0px #1F2937 !important;
219
- transition: all 0.1s ease !important;
220
  text-shadow: 1px 1px 0px #1F2937 !important;
221
  }
222
 
223
  .gr-button-primary:hover,
224
- button.primary:hover,
225
- .gr-button.primary:hover {
226
  background: #2563EB !important;
227
  transform: translate(-2px, -2px) !important;
228
  box-shadow: 7px 7px 0px #1F2937 !important;
229
  }
230
 
231
  .gr-button-primary:active,
232
- button.primary:active,
233
- .gr-button.primary:active {
234
  transform: translate(3px, 3px) !important;
235
  box-shadow: 2px 2px 0px #1F2937 !important;
236
  }
237
 
238
- /* ===== 🎨 Secondary λ²„νŠΌ - μ½”λ―Ή λ ˆλ“œ ===== */
239
  .gr-button-secondary,
240
- button.secondary,
241
- .extract-btn {
242
  background: #EF4444 !important;
243
  border: 3px solid #1F2937 !important;
244
  border-radius: 8px !important;
@@ -246,53 +291,29 @@ button.secondary,
246
  font-family: 'Bangers', cursive !important;
247
  font-weight: 400 !important;
248
  font-size: 1.1rem !important;
249
- letter-spacing: 1px !important;
250
  box-shadow: 4px 4px 0px #1F2937 !important;
251
- transition: all 0.1s ease !important;
252
  text-shadow: 1px 1px 0px #1F2937 !important;
253
  }
254
 
255
  .gr-button-secondary:hover,
256
- button.secondary:hover,
257
- .extract-btn:hover {
258
  background: #DC2626 !important;
259
  transform: translate(-2px, -2px) !important;
260
  box-shadow: 6px 6px 0px #1F2937 !important;
261
  }
262
 
263
- .gr-button-secondary:active,
264
- button.secondary:active,
265
- .extract-btn:active {
266
- transform: translate(2px, 2px) !important;
267
- box-shadow: 2px 2px 0px #1F2937 !important;
268
- }
269
-
270
- /* ===== 🎨 둜그 좜λ ₯ μ˜μ—­ ===== */
271
- .info-log textarea {
272
  background: #1F2937 !important;
273
  color: #10B981 !important;
274
  font-family: 'Courier New', monospace !important;
275
  font-size: 0.9rem !important;
276
- font-weight: 400 !important;
277
  border: 3px solid #10B981 !important;
278
  border-radius: 8px !important;
279
  box-shadow: 4px 4px 0px #10B981 !important;
280
  }
281
 
282
- /* ===== 🎨 λΉ„λ””μ˜€ μ—…λ‘œλ“œ μ˜μ—­ ===== */
283
- .video-upload {
284
- border: 4px dashed #3B82F6 !important;
285
- border-radius: 12px !important;
286
- background: #EFF6FF !important;
287
- transition: all 0.2s ease !important;
288
- }
289
-
290
- .video-upload:hover {
291
- border-color: #EF4444 !important;
292
- background: #FEF2F2 !important;
293
- }
294
-
295
- /* ===== 🎨 μ•„μ½”λ””μ–Έ - 말풍선 μŠ€νƒ€μΌ ===== */
296
  .gr-accordion {
297
  background: #FACC15 !important;
298
  border: 3px solid #1F2937 !important;
@@ -300,57 +321,35 @@ button.secondary:active,
300
  box-shadow: 4px 4px 0px #1F2937 !important;
301
  }
302
 
303
- .gr-accordion-header {
304
- color: #1F2937 !important;
305
- font-family: 'Comic Neue', cursive !important;
306
- font-weight: 700 !important;
307
- font-size: 1.1rem !important;
 
308
  }
309
 
310
- /* ===== 🎨 이미지 좜λ ₯ μ˜μ—­ ===== */
311
- .gr-image,
312
- .image-container {
313
- border: 4px solid #1F2937 !important;
314
- border-radius: 8px !important;
315
- box-shadow: 8px 8px 0px #1F2937 !important;
316
- overflow: hidden !important;
317
- background: #FFFFFF !important;
318
  }
319
 
320
- /* ===== 🎨 라벨 μŠ€νƒ€μΌ ===== */
321
  label,
322
  .gr-input-label,
323
  .gr-block-label {
324
  color: #1F2937 !important;
325
  font-family: 'Comic Neue', cursive !important;
326
  font-weight: 700 !important;
327
- font-size: 1rem !important;
328
- }
329
-
330
- span.gr-label {
331
- color: #1F2937 !important;
332
- }
333
-
334
- /* ===== 🎨 정보 ν…μŠ€νŠΈ ===== */
335
- .gr-info,
336
- .info {
337
- color: #6B7280 !important;
338
- font-family: 'Comic Neue', cursive !important;
339
- font-size: 0.9rem !important;
340
  }
341
 
342
- /* ===== 🎨 ν”„λ‘œκ·Έλ ˆμŠ€ λ°” ===== */
343
- .progress-bar,
344
- .gr-progress-bar {
345
- background: #3B82F6 !important;
346
- border: 2px solid #1F2937 !important;
347
- border-radius: 4px !important;
348
- }
349
-
350
- /* ===== 🎨 μŠ€ν¬λ‘€λ°” - μ½”λ―Ή μŠ€νƒ€μΌ ===== */
351
  ::-webkit-scrollbar {
352
  width: 12px;
353
- height: 12px;
354
  }
355
 
356
  ::-webkit-scrollbar-track {
@@ -361,7 +360,6 @@ span.gr-label {
361
  ::-webkit-scrollbar-thumb {
362
  background: #3B82F6;
363
  border: 2px solid #1F2937;
364
- border-radius: 0px;
365
  }
366
 
367
  ::-webkit-scrollbar-thumb:hover {
@@ -374,33 +372,11 @@ span.gr-label {
374
  color: #1F2937;
375
  }
376
 
377
- /* ===== 🎨 링크 μŠ€νƒ€μΌ ===== */
378
- a {
379
- color: #3B82F6 !important;
380
- text-decoration: none !important;
381
- font-weight: 700 !important;
382
- }
383
-
384
- a:hover {
385
- color: #EF4444 !important;
386
- }
387
-
388
- /* ===== 🎨 Row/Column 간격 ===== */
389
- .gr-row {
390
- gap: 1.5rem !important;
391
- }
392
-
393
- .gr-column {
394
- gap: 1rem !important;
395
- }
396
-
397
- /* ===== λ°˜μ‘ν˜• μ‘°μ • ===== */
398
  @media (max-width: 768px) {
399
  .header-text h1 {
400
  font-size: 2.2rem !important;
401
- text-shadow:
402
- 3px 3px 0px #FACC15,
403
- 4px 4px 0px #1F2937 !important;
404
  }
405
 
406
  .gr-button-primary,
@@ -408,22 +384,12 @@ a:hover {
408
  padding: 12px 20px !important;
409
  font-size: 1.1rem !important;
410
  }
411
-
412
- .gr-panel,
413
- .block {
414
- box-shadow: 4px 4px 0px #1F2937 !important;
415
- }
416
- }
417
-
418
- /* ===== 🎨 닀크λͺ¨λ“œ λΉ„ν™œμ„±ν™” (코믹은 밝아야 함) ===== */
419
- @media (prefers-color-scheme: dark) {
420
- .gradio-container {
421
- background-color: #FEF9C3 !important;
422
- }
423
  }
424
  """
425
 
426
- # Build the Gradio interface
 
 
427
  with gr.Blocks(fill_height=True, css=css) as demo:
428
 
429
  # HOME Badge
@@ -432,79 +398,130 @@ with gr.Blocks(fill_height=True, css=css) as demo:
432
  <a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
433
  <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
434
  </a>
 
 
 
435
  </div>
436
  """)
437
 
438
  # Header Title
439
  gr.Markdown(
440
- """
441
- # 🎬 VIDEO LAST FRAME EXTRACTOR πŸ–ΌοΈ
442
- """,
443
  elem_classes="header-text"
444
  )
445
 
446
  gr.Markdown(
447
- """
448
- <p class="subtitle">πŸ“Ή Upload a video and extract the LAST FRAME instantly! πŸ’Ύ</p>
449
- """,
450
  )
451
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  with gr.Row(equal_height=False):
453
- # Left column - Input
454
- with gr.Column(scale=1, min_width=320):
455
- video_input = gr.Video(
456
- label="πŸ“Ή Upload Your Video",
457
- sources=["upload"],
458
- elem_classes="video-upload"
459
- )
 
 
 
 
 
460
 
461
- extract_btn = gr.Button(
462
- "🎬 EXTRACT LAST FRAME! πŸ–ΌοΈ",
463
- variant="primary",
464
- size="lg",
465
- elem_classes="extract-btn"
 
466
  )
467
 
468
- with gr.Accordion("πŸ“œ Extraction Info", open=True):
469
- info_log = gr.Textbox(
 
470
  label="",
471
- placeholder="Upload a video and click extract to see info...",
472
- lines=12,
473
- max_lines=20,
474
- interactive=False,
475
- elem_classes="info-log"
476
  )
 
 
 
 
477
 
478
- # Right column - Output
479
- with gr.Column(scale=1, min_width=320):
480
- output_image = gr.Image(
481
- label="πŸ–ΌοΈ Last Frame",
482
- type="pil",
483
- show_label=True,
484
- height=500,
485
- )
 
 
 
 
 
 
 
 
486
 
487
- gr.Markdown(
488
- """
489
- <p style="text-align: center; margin-top: 10px; font-weight: 700; color: #1F2937;">
490
- πŸ’‘ Right-click on the image to save, or use the download button!
491
- </p>
492
- """
493
- )
494
-
495
- # Connect the extract button
496
- extract_btn.click(
497
- fn=extract_last_frame,
498
- inputs=[video_input],
499
- outputs=[output_image, info_log],
500
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
- # Auto-extract when video is uploaded
503
- video_input.change(
504
- fn=extract_last_frame,
505
- inputs=[video_input],
506
- outputs=[output_image, info_log],
 
507
  )
 
 
508
 
509
  if __name__ == "__main__":
510
  demo.launch()
 
1
  import gradio as gr
2
+ from huggingface_hub import InferenceClient
3
+ from openai import OpenAI
4
  import os
 
5
 
6
+ # ============================================
7
+ # MiniMax-M2.1 Streaming Chat
8
+ # Dual Provider: Novita (HF) + MiniMax Official API
9
+ # Comic Classic Theme
10
+ # ============================================
11
+
12
+ # Provider μƒνƒœ 관리
13
+ class ProviderManager:
14
+ def __init__(self):
15
+ self.current_provider = "novita"
16
+ self.novita_available = True
17
+
18
+ # Novita Client (via HuggingFace)
19
+ self.novita_client = InferenceClient(
20
+ provider="novita",
21
+ api_key=os.environ.get("HF_TOKEN"),
22
+ )
23
+
24
+ # MiniMax Official API Client
25
+ self.minimax_client = OpenAI(
26
+ api_key=os.environ.get("MINIMAX_API_KEY", ""),
27
+ base_url="https://api.minimax.chat/v1"
28
+ )
29
+
30
+ def get_status(self):
31
+ if self.current_provider == "novita":
32
+ return "🟒 Novita (HuggingFace)"
33
+ else:
34
+ return "🟑 MiniMax Official API"
35
+
36
+ def switch_to_minimax(self):
37
+ self.current_provider = "minimax"
38
+ self.novita_available = False
39
+ print("⚠️ Switched to MiniMax Official API")
40
+
41
+ def reset_to_novita(self):
42
+ self.current_provider = "novita"
43
+ self.novita_available = True
44
+ print("βœ… Reset to Novita provider")
45
+
46
+ # Global provider manager
47
+ provider = ProviderManager()
48
+
49
+ def chat_with_novita(messages):
50
+ """Novita providerλ₯Ό ν†΅ν•œ 슀트리밍"""
51
+ stream = provider.novita_client.chat.completions.create(
52
+ model="MiniMaxAI/MiniMax-M2.1",
53
+ messages=messages,
54
+ max_tokens=4096,
55
+ temperature=1.0,
56
+ top_p=0.95,
57
+ stream=True
58
+ )
59
+
60
+ for chunk in stream:
61
+ if chunk.choices[0].delta.content:
62
+ yield chunk.choices[0].delta.content
63
+
64
+ def chat_with_minimax(messages):
65
+ """MiniMax 곡식 APIλ₯Ό ν†΅ν•œ 슀트리밍"""
66
+ stream = provider.minimax_client.chat.completions.create(
67
+ model="MiniMax-M2.1",
68
+ messages=messages,
69
+ max_tokens=4096,
70
+ temperature=1.0,
71
+ top_p=0.95,
72
+ stream=True
73
+ )
74
+
75
+ for chunk in stream:
76
+ if chunk.choices and chunk.choices[0].delta.content:
77
+ yield chunk.choices[0].delta.content
78
+
79
+ def chat_respond(message, history):
80
  """
81
+ Streaming chat with automatic fallback
82
+ Returns: tuple (history, status)
83
  """
84
+ if not message.strip():
85
+ return history, provider.get_status()
86
 
87
+ # Build API messages
88
+ api_messages = [{
89
+ "role": "system",
90
+ "content": "You are MiniMax-M2.1, a helpful AI assistant built by MiniMax. You excel at coding, tool use, and complex reasoning tasks. Respond in the same language as the user."
91
+ }]
92
+
93
+ # Add history
94
+ for h in history:
95
+ if isinstance(h, (list, tuple)) and len(h) == 2:
96
+ if h[0]:
97
+ api_messages.append({"role": "user", "content": h[0]})
98
+ if h[1]:
99
+ api_messages.append({"role": "assistant", "content": h[1]})
100
+
101
+ api_messages.append({"role": "user", "content": message})
102
+
103
+ response_text = ""
104
+ current_status = provider.get_status()
105
+
106
+ # 1μ°¨: Novita
107
+ if provider.novita_available and provider.current_provider == "novita":
108
+ try:
109
+ for chunk in chat_with_novita(api_messages):
110
+ response_text += chunk
111
+ yield history + [[message, response_text]], "🟒 Novita (HuggingFace)"
112
+ return
113
+ except Exception as e:
114
+ error_msg = str(e).lower()
115
+ if any(kw in error_msg for kw in ["rate limit", "quota", "exceeded", "insufficient", "credit", "balance", "limit", "429", "402", "payment"]):
116
+ print(f"⚠️ Novita error: {e}")
117
+ provider.switch_to_minimax()
118
+ else:
119
+ print(f"⚠️ Novita error: {e}")
120
+
121
+ # 2μ°¨: MiniMax
122
  try:
123
+ if not os.environ.get("MINIMAX_API_KEY"):
124
+ yield history + [[message, "❌ MINIMAX_API_KEY not configured."]], "πŸ”΄ No API Key"
125
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
+ for chunk in chat_with_minimax(api_messages):
128
+ response_text += chunk
129
+ yield history + [[message, response_text]], "🟑 MiniMax Official API"
130
+ return
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
  except Exception as e:
133
+ if "rate limit" not in str(e).lower():
134
+ provider.reset_to_novita()
135
+ yield history + [[message, f"❌ Error: {str(e)}"]], "πŸ”΄ Error"
136
+
137
+ def reset_provider_fn():
138
+ """μˆ˜λ™μœΌλ‘œ Novita둜 리셋"""
139
+ provider.reset_to_novita()
140
+ return "βœ… Reset to Novita!"
141
 
142
+ def clear_chat_fn():
143
+ """μ±„νŒ… μ΄ˆκΈ°ν™”"""
144
+ return [], "", provider.get_status()
145
 
146
  # ============================================
147
+ # 🎨 Comic Classic Theme CSS
148
  # ============================================
 
149
  css = """
150
  /* ===== 🎨 Google Fonts Import ===== */
151
  @import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&display=swap');
152
 
153
+ /* ===== 🎨 Comic Classic 배경 ===== */
154
  .gradio-container {
155
  background-color: #FEF9C3 !important;
156
+ background-image: radial-gradient(#1F2937 1px, transparent 1px) !important;
 
157
  background-size: 20px 20px !important;
158
  min-height: 100vh !important;
159
  font-family: 'Comic Neue', cursive, sans-serif !important;
160
  }
161
 
162
+ /* ===== ν—ˆκΉ…νŽ˜μ΄μŠ€ 헀더 μˆ¨κΉ€ ===== */
163
  .huggingface-space-header,
164
  #space-header,
165
  .space-header,
166
  [class*="space-header"],
167
  .svelte-1ed2p3z,
168
  .space-header-badge,
169
+ [data-testid="space-header"] {
 
 
 
 
170
  display: none !important;
171
  visibility: hidden !important;
172
  height: 0 !important;
173
  width: 0 !important;
 
 
 
174
  }
175
 
176
+ /* ===== Footer μˆ¨κΉ€ ===== */
177
  footer,
178
  .footer,
179
  .gradio-container footer,
180
  .built-with,
181
  [class*="footer"],
182
  .gradio-footer,
 
 
183
  .show-api,
184
+ .built-with-gradio {
 
 
185
  display: none !important;
186
  visibility: hidden !important;
187
  height: 0 !important;
 
 
188
  }
189
 
190
+ /* ===== 🎨 헀더 타이틀 ===== */
 
 
 
 
 
 
191
  .header-text h1 {
192
  font-family: 'Bangers', cursive !important;
193
  color: #1F2937 !important;
 
212
  font-weight: 700 !important;
213
  }
214
 
215
+ /* ===== 🎨 μΉ΄λ“œ/νŒ¨λ„ ===== */
216
  .gr-panel,
217
  .gr-box,
218
  .gr-form,
 
231
  box-shadow: 8px 8px 0px #1F2937 !important;
232
  }
233
 
234
+ /* ===== 🎨 μž…λ ₯ ν•„λ“œ ===== */
235
  textarea,
236
  input[type="text"],
237
  input[type="number"] {
 
242
  font-family: 'Comic Neue', cursive !important;
243
  font-size: 1rem !important;
244
  font-weight: 700 !important;
 
245
  }
246
 
247
  textarea:focus,
248
+ input[type="text"]:focus {
 
249
  border-color: #3B82F6 !important;
250
  box-shadow: 4px 4px 0px #3B82F6 !important;
251
  outline: none !important;
252
  }
253
 
254
+ /* ===== 🎨 Primary λ²„νŠΌ ===== */
 
 
 
 
 
255
  .gr-button-primary,
256
  button.primary,
257
  .gr-button.primary {
 
265
  letter-spacing: 2px !important;
266
  padding: 14px 28px !important;
267
  box-shadow: 5px 5px 0px #1F2937 !important;
 
268
  text-shadow: 1px 1px 0px #1F2937 !important;
269
  }
270
 
271
  .gr-button-primary:hover,
272
+ button.primary:hover {
 
273
  background: #2563EB !important;
274
  transform: translate(-2px, -2px) !important;
275
  box-shadow: 7px 7px 0px #1F2937 !important;
276
  }
277
 
278
  .gr-button-primary:active,
279
+ button.primary:active {
 
280
  transform: translate(3px, 3px) !important;
281
  box-shadow: 2px 2px 0px #1F2937 !important;
282
  }
283
 
284
+ /* ===== 🎨 Secondary λ²„νŠΌ ===== */
285
  .gr-button-secondary,
286
+ button.secondary {
 
287
  background: #EF4444 !important;
288
  border: 3px solid #1F2937 !important;
289
  border-radius: 8px !important;
 
291
  font-family: 'Bangers', cursive !important;
292
  font-weight: 400 !important;
293
  font-size: 1.1rem !important;
 
294
  box-shadow: 4px 4px 0px #1F2937 !important;
 
295
  text-shadow: 1px 1px 0px #1F2937 !important;
296
  }
297
 
298
  .gr-button-secondary:hover,
299
+ button.secondary:hover {
 
300
  background: #DC2626 !important;
301
  transform: translate(-2px, -2px) !important;
302
  box-shadow: 6px 6px 0px #1F2937 !important;
303
  }
304
 
305
+ /* ===== 🎨 둜그/μƒνƒœ 좜λ ₯ ===== */
306
+ .status-box textarea {
 
 
 
 
 
 
 
307
  background: #1F2937 !important;
308
  color: #10B981 !important;
309
  font-family: 'Courier New', monospace !important;
310
  font-size: 0.9rem !important;
 
311
  border: 3px solid #10B981 !important;
312
  border-radius: 8px !important;
313
  box-shadow: 4px 4px 0px #10B981 !important;
314
  }
315
 
316
+ /* ===== 🎨 μ•„μ½”λ””μ–Έ ===== */
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  .gr-accordion {
318
  background: #FACC15 !important;
319
  border: 3px solid #1F2937 !important;
 
321
  box-shadow: 4px 4px 0px #1F2937 !important;
322
  }
323
 
324
+ /* ===== 🎨 μ±„νŒ… μ˜μ—­ ===== */
325
+ .chatbot {
326
+ border: 3px solid #1F2937 !important;
327
+ border-radius: 12px !important;
328
+ box-shadow: 6px 6px 0px #1F2937 !important;
329
+ background: #FFFFFF !important;
330
  }
331
 
332
+ /* ===== 🎨 μ½”λ“œ 블둝 ===== */
333
+ pre, code {
334
+ background: #1F2937 !important;
335
+ color: #10B981 !important;
336
+ border: 2px solid #10B981 !important;
337
+ border-radius: 6px !important;
338
+ font-family: 'Courier New', monospace !important;
 
339
  }
340
 
341
+ /* ===== 🎨 라벨 ===== */
342
  label,
343
  .gr-input-label,
344
  .gr-block-label {
345
  color: #1F2937 !important;
346
  font-family: 'Comic Neue', cursive !important;
347
  font-weight: 700 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  }
349
 
350
+ /* ===== 🎨 μŠ€ν¬λ‘€λ°” ===== */
 
 
 
 
 
 
 
 
351
  ::-webkit-scrollbar {
352
  width: 12px;
 
353
  }
354
 
355
  ::-webkit-scrollbar-track {
 
360
  ::-webkit-scrollbar-thumb {
361
  background: #3B82F6;
362
  border: 2px solid #1F2937;
 
363
  }
364
 
365
  ::-webkit-scrollbar-thumb:hover {
 
372
  color: #1F2937;
373
  }
374
 
375
+ /* ===== λ°˜μ‘ν˜• ===== */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  @media (max-width: 768px) {
377
  .header-text h1 {
378
  font-size: 2.2rem !important;
379
+ text-shadow: 3px 3px 0px #FACC15, 4px 4px 0px #1F2937 !important;
 
 
380
  }
381
 
382
  .gr-button-primary,
 
384
  padding: 12px 20px !important;
385
  font-size: 1.1rem !important;
386
  }
 
 
 
 
 
 
 
 
 
 
 
 
387
  }
388
  """
389
 
390
+ # ============================================
391
+ # Gradio Interface (gr.Blocks μŠ€νƒ€μΌ)
392
+ # ============================================
393
  with gr.Blocks(fill_height=True, css=css) as demo:
394
 
395
  # HOME Badge
 
398
  <a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;">
399
  <img src="https://img.shields.io/static/v1?label=🏠 HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME">
400
  </a>
401
+ <a href="https://huggingface.co/MiniMaxAI/MiniMax-M2.1" target="_blank" style="text-decoration: none; margin-left: 10px;">
402
+ <img src="https://img.shields.io/static/v1?label=πŸ€—&message=MiniMax-M2.1&color=ff6f00&labelColor=1F2937&style=for-the-badge" alt="Model">
403
+ </a>
404
  </div>
405
  """)
406
 
407
  # Header Title
408
  gr.Markdown(
409
+ """# πŸ€– MINIMAX-M2.1 CHAT πŸ’¬""",
 
 
410
  elem_classes="header-text"
411
  )
412
 
413
  gr.Markdown(
414
+ """<p class="subtitle">⚑ Claude Sonnet 4.5 μˆ˜μ€€μ˜ μ½”λ”© & μ—μ΄μ „νŠΈ μ„±λŠ₯! 230B νŒŒλΌλ―Έν„° μ˜€ν”ˆμ†ŒμŠ€ λͺ¨λΈ πŸš€</p>"""
 
 
415
  )
416
 
417
+ # Model Info Box
418
+ gr.HTML("""
419
+ <div style="background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%); border: 3px solid #1F2937; border-radius: 12px; padding: 15px; color: white; box-shadow: 5px 5px 0px #1F2937; margin: 0 auto 20px auto; max-width: 800px;">
420
+ <div style="display: flex; justify-content: space-around; flex-wrap: wrap; text-align: center;">
421
+ <div><strong style="font-size: 1.5rem;">230B</strong><br><span style="font-size: 0.9rem;">Total Params</span></div>
422
+ <div><strong style="font-size: 1.5rem;">10B</strong><br><span style="font-size: 0.9rem;">Active Params</span></div>
423
+ <div><strong style="font-size: 1.5rem;">88.6</strong><br><span style="font-size: 0.9rem;">VIBE Score</span></div>
424
+ <div><strong style="font-size: 1.5rem;">#1</strong><br><span style="font-size: 0.9rem;">Open Source</span></div>
425
+ </div>
426
+ </div>
427
+ """)
428
+
429
  with gr.Row(equal_height=False):
430
+ # Left Column - Chat
431
+ with gr.Column(scale=2, min_width=400):
432
+ # Provider Status
433
+ with gr.Row():
434
+ provider_status = gr.Textbox(
435
+ value="🟒 Novita (HuggingFace)",
436
+ label="πŸ”Œ Current Provider",
437
+ interactive=False,
438
+ elem_classes="status-box",
439
+ scale=3
440
+ )
441
+ reset_btn = gr.Button("πŸ”„ Reset", variant="secondary", scale=1)
442
 
443
+ # Chatbot
444
+ chatbot = gr.Chatbot(
445
+ label="πŸ’¬ Chat",
446
+ height=450,
447
+ show_label=False,
448
+ elem_classes="chatbot"
449
  )
450
 
451
+ # Input Row
452
+ with gr.Row():
453
+ msg = gr.Textbox(
454
  label="",
455
+ placeholder="λ©”μ‹œμ§€λ₯Ό μž…λ ₯ν•˜μ„Έμš”... (μ½”λ”©, 뢄석, μ°½μž‘ λ“± 무엇이든!)",
456
+ scale=8,
457
+ container=False
 
 
458
  )
459
+ submit_btn = gr.Button("πŸ“€ SEND", variant="primary", scale=2)
460
+
461
+ # Clear Button
462
+ clear_btn = gr.Button("πŸ—‘οΈ CLEAR CHAT", variant="secondary")
463
 
464
+ # Right Column - Info
465
+ with gr.Column(scale=1, min_width=300):
466
+ with gr.Accordion("πŸ’‘ Example Prompts", open=True):
467
+ gr.Markdown("""
468
+ **μ½”λ”©:**
469
+ - Python으둜 ν€΅μ†ŒνŠΈ κ΅¬ν˜„ν•΄μ€˜
470
+ - React Todo μ•± λ§Œλ“€μ–΄μ€˜
471
+ - FastAPI JWT 인증 κ΅¬ν˜„ν•΄μ€˜
472
+
473
+ **μ„€λͺ…:**
474
+ - Docker vs Kubernetes 차이점
475
+ - REST API 섀계 베슀트 ν”„λž™ν‹°μŠ€
476
+
477
+ **뢄석:**
478
+ - λ¨Έμ‹ λŸ¬λ‹ 배포 νŒŒμ΄ν”„λΌμΈ 섀계
479
+ """)
480
 
481
+ with gr.Accordion("πŸ”Œ Provider Info", open=False):
482
+ gr.Markdown("""
483
+ ### λ“€μ–Ό ν”„λ‘œλ°”μ΄λ”
484
+
485
+ **1μ°¨: Novita (HuggingFace)**
486
+ - HF PRO ν¬λ ˆλ”§ μ‚¬μš©
487
+
488
+ **2μ°¨: MiniMax Official API**
489
+ - ν¬λ ˆλ”§ μ†Œμ§„ μ‹œ μžλ™ μ „ν™˜
490
+ - ν˜„μž¬ ν•œμ‹œμ  무료
491
+
492
+ ### Secrets μ„€μ •
493
+ ```
494
+ HF_TOKEN
495
+ MINIMAX_API_KEY
496
+ ```
497
+ """)
498
+
499
+ with gr.Accordion("🎯 Model Info", open=False):
500
+ gr.Markdown("""
501
+ - **πŸ† SOTA μ½”λ”©** β€” Claude Sonnet 4.5 λŠ₯κ°€
502
+ - **🌍 λ‹€κ΅­μ–΄** β€” Python, Rust, Java, Go, C++
503
+ - **πŸ€– μ—μ΄μ „νŠΈ** β€” λ©€ν‹°μŠ€ν… νƒœμŠ€ν¬
504
+ - **πŸ’‘ μΆ”λ‘ ** β€” Interleaved Thinking
505
+ - **πŸ”§ 도ꡬ** β€” ν•¨μˆ˜ 호좜 지원
506
+ """)
507
+
508
+ # Event Handlers
509
+ def respond(message, history):
510
+ if not message.strip():
511
+ yield history, provider.get_status()
512
+ return
513
+ for result in chat_respond(message, history):
514
+ yield result
515
 
516
+ # Connect events
517
+ msg.submit(respond, [msg, chatbot], [chatbot, provider_status]).then(
518
+ lambda: "", outputs=[msg]
519
+ )
520
+ submit_btn.click(respond, [msg, chatbot], [chatbot, provider_status]).then(
521
+ lambda: "", outputs=[msg]
522
  )
523
+ clear_btn.click(clear_chat_fn, outputs=[chatbot, msg, provider_status])
524
+ reset_btn.click(reset_provider_fn, outputs=[provider_status])
525
 
526
  if __name__ == "__main__":
527
  demo.launch()