webolavo commited on
Commit
242e181
ยท
verified ยท
1 Parent(s): 5b9a5d8

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1207 -1141
index.html CHANGED
@@ -1,1142 +1,1208 @@
1
- <!DOCTYPE html>
2
- <html lang="ar" dir="rtl">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Video Filter AI</title>
7
- <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Tajawal:wght@300;400;700;900&display=swap" rel="stylesheet">
8
- <style>
9
- :root {
10
- --bg: #0a0a0f;
11
- --surface: #111118;
12
- --border: #1e1e2e;
13
- --accent: #00ff88;
14
- --accent2: #ff3366;
15
- --accent3: #4488ff;
16
- --text: #e8e8f0;
17
- --muted: #555570;
18
- --warn: #ffaa00;
19
- }
20
-
21
- * { margin: 0; padding: 0; box-sizing: border-box; }
22
-
23
- body {
24
- font-family: 'Tajawal', sans-serif;
25
- background: var(--bg);
26
- color: var(--text);
27
- min-height: 100vh;
28
- overflow-x: hidden;
29
- }
30
-
31
- /* โ”€โ”€โ”€ Grid Background โ”€โ”€โ”€ */
32
- body::before {
33
- content: '';
34
- position: fixed;
35
- inset: 0;
36
- background-image:
37
- linear-gradient(rgba(0,255,136,0.03) 1px, transparent 1px),
38
- linear-gradient(90deg, rgba(0,255,136,0.03) 1px, transparent 1px);
39
- background-size: 40px 40px;
40
- pointer-events: none;
41
- z-index: 0;
42
- }
43
-
44
- .container {
45
- max-width: 900px;
46
- margin: 0 auto;
47
- padding: 40px 20px;
48
- position: relative;
49
- z-index: 1;
50
- }
51
-
52
- /* โ”€โ”€โ”€ Header โ”€โ”€โ”€ */
53
- .header {
54
- text-align: center;
55
- margin-bottom: 48px;
56
- }
57
-
58
- .header-badge {
59
- display: inline-block;
60
- font-family: 'IBM Plex Mono', monospace;
61
- font-size: 11px;
62
- color: var(--accent);
63
- border: 1px solid var(--accent);
64
- padding: 4px 12px;
65
- border-radius: 2px;
66
- letter-spacing: 3px;
67
- margin-bottom: 16px;
68
- text-transform: uppercase;
69
- }
70
-
71
- .header h1 {
72
- font-size: 42px;
73
- font-weight: 900;
74
- line-height: 1.1;
75
- margin-bottom: 8px;
76
- }
77
-
78
- .header h1 span { color: var(--accent); }
79
-
80
- .header p {
81
- color: var(--muted);
82
- font-size: 15px;
83
- font-weight: 300;
84
- }
85
-
86
- /* โ”€โ”€โ”€ API Status โ”€โ”€โ”€ */
87
- .api-status {
88
- display: flex;
89
- align-items: center;
90
- gap: 8px;
91
- justify-content: center;
92
- margin-bottom: 32px;
93
- font-family: 'IBM Plex Mono', monospace;
94
- font-size: 12px;
95
- }
96
-
97
- .status-dot {
98
- width: 8px; height: 8px;
99
- border-radius: 50%;
100
- background: var(--muted);
101
- animation: none;
102
- }
103
- .status-dot.ready { background: var(--accent); animation: pulse 2s infinite; }
104
- .status-dot.loading { background: var(--warn); animation: pulse 1s infinite; }
105
- .status-dot.error { background: var(--accent2); }
106
-
107
- @keyframes pulse {
108
- 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 currentColor; }
109
- 50% { opacity: 0.7; box-shadow: 0 0 0 4px transparent; }
110
- }
111
-
112
- /* โ”€โ”€โ”€ Upload Zone โ”€โ”€โ”€ */
113
- .upload-zone {
114
- border: 1px dashed var(--border);
115
- border-radius: 8px;
116
- padding: 60px 40px;
117
- text-align: center;
118
- cursor: pointer;
119
- transition: all 0.3s ease;
120
- background: var(--surface);
121
- position: relative;
122
- overflow: hidden;
123
- }
124
-
125
- .upload-zone::before {
126
- content: '';
127
- position: absolute;
128
- inset: 0;
129
- background: linear-gradient(135deg, rgba(0,255,136,0.03), transparent);
130
- opacity: 0;
131
- transition: opacity 0.3s;
132
- }
133
-
134
- .upload-zone:hover, .upload-zone.drag-over {
135
- border-color: var(--accent);
136
- background: rgba(0,255,136,0.03);
137
- }
138
-
139
- .upload-zone:hover::before, .upload-zone.drag-over::before { opacity: 1; }
140
-
141
- .upload-icon {
142
- font-size: 48px;
143
- margin-bottom: 16px;
144
- display: block;
145
- }
146
-
147
- .upload-zone h3 {
148
- font-size: 18px;
149
- font-weight: 700;
150
- margin-bottom: 8px;
151
- }
152
-
153
- .upload-zone p {
154
- color: var(--muted);
155
- font-size: 13px;
156
- font-family: 'IBM Plex Mono', monospace;
157
- }
158
-
159
- #fileInput { display: none; }
160
-
161
- /* โ”€โ”€โ”€ Video Preview โ”€โ”€โ”€ */
162
- .video-preview {
163
- display: none;
164
- background: var(--surface);
165
- border: 1px solid var(--border);
166
- border-radius: 8px;
167
- overflow: hidden;
168
- margin-bottom: 24px;
169
- }
170
-
171
- .video-preview video {
172
- width: 100%;
173
- max-height: 300px;
174
- display: block;
175
- background: #000;
176
- }
177
-
178
- .video-meta {
179
- padding: 16px 20px;
180
- display: flex;
181
- gap: 24px;
182
- font-family: 'IBM Plex Mono', monospace;
183
- font-size: 12px;
184
- color: var(--muted);
185
- border-top: 1px solid var(--border);
186
- flex-wrap: wrap;
187
- }
188
-
189
- .video-meta span { display: flex; align-items: center; gap: 6px; }
190
- .video-meta strong { color: var(--text); }
191
-
192
- /* โ”€โ”€โ”€ Upload Progress โ”€โ”€โ”€ */
193
- .upload-progress {
194
- display: none;
195
- margin-bottom: 24px;
196
- }
197
-
198
- .progress-label {
199
- display: flex;
200
- justify-content: space-between;
201
- font-family: 'IBM Plex Mono', monospace;
202
- font-size: 12px;
203
- color: var(--muted);
204
- margin-bottom: 8px;
205
- }
206
-
207
- .progress-bar {
208
- height: 3px;
209
- background: var(--border);
210
- border-radius: 2px;
211
- overflow: hidden;
212
- }
213
-
214
- .progress-fill {
215
- height: 100%;
216
- background: var(--accent);
217
- border-radius: 2px;
218
- transition: width 0.3s ease;
219
- width: 0%;
220
- box-shadow: 0 0 8px var(--accent);
221
- }
222
-
223
- /* โ”€โ”€โ”€ Action Buttons โ”€โ”€โ”€ */
224
- .actions {
225
- display: flex;
226
- gap: 12px;
227
- margin-bottom: 32px;
228
- flex-wrap: wrap;
229
- }
230
-
231
- .btn {
232
- flex: 1;
233
- padding: 14px 24px;
234
- border: none;
235
- border-radius: 6px;
236
- font-family: 'Tajawal', sans-serif;
237
- font-size: 15px;
238
- font-weight: 700;
239
- cursor: pointer;
240
- transition: all 0.2s ease;
241
- display: flex;
242
- align-items: center;
243
- justify-content: center;
244
- gap: 8px;
245
- min-width: 160px;
246
- }
247
-
248
- .btn:disabled {
249
- opacity: 0.3;
250
- cursor: not-allowed;
251
- transform: none !important;
252
- }
253
-
254
- .btn-primary {
255
- background: var(--accent);
256
- color: #000;
257
- }
258
- .btn-primary:not(:disabled):hover {
259
- transform: translateY(-2px);
260
- box-shadow: 0 8px 24px rgba(0,255,136,0.3);
261
- }
262
-
263
- .btn-danger {
264
- background: transparent;
265
- color: var(--accent2);
266
- border: 1px solid var(--accent2);
267
- }
268
- .btn-danger:not(:disabled):hover {
269
- background: rgba(255,51,102,0.1);
270
- transform: translateY(-2px);
271
- }
272
-
273
- .btn-secondary {
274
- background: transparent;
275
- color: var(--accent3);
276
- border: 1px solid var(--accent3);
277
- }
278
- .btn-secondary:not(:disabled):hover {
279
- background: rgba(68,136,255,0.1);
280
- transform: translateY(-2px);
281
- }
282
-
283
- /* โ”€โ”€โ”€ Timeline โ”€โ”€โ”€ */
284
- .timeline-section {
285
- display: none;
286
- margin-bottom: 32px;
287
- }
288
-
289
- .timeline-header {
290
- display: flex;
291
- align-items: center;
292
- gap: 12px;
293
- margin-bottom: 20px;
294
- }
295
-
296
- .timeline-header h2 {
297
- font-size: 16px;
298
- font-weight: 700;
299
- font-family: 'IBM Plex Mono', monospace;
300
- color: var(--accent);
301
- letter-spacing: 1px;
302
- text-transform: uppercase;
303
- }
304
-
305
- .timeline {
306
- position: relative;
307
- padding-right: 32px;
308
- }
309
-
310
- .timeline::before {
311
- content: '';
312
- position: absolute;
313
- right: 11px;
314
- top: 0;
315
- bottom: 0;
316
- width: 1px;
317
- background: var(--border);
318
- }
319
-
320
- .timeline-item {
321
- position: relative;
322
- padding: 0 24px 24px 0;
323
- opacity: 0;
324
- transform: translateX(10px);
325
- transition: all 0.4s ease;
326
- }
327
-
328
- .timeline-item.visible {
329
- opacity: 1;
330
- transform: translateX(0);
331
- }
332
-
333
- .timeline-dot {
334
- position: absolute;
335
- right: -21px;
336
- top: 4px;
337
- width: 14px;
338
- height: 14px;
339
- border-radius: 50%;
340
- border: 2px solid var(--border);
341
- background: var(--bg);
342
- transition: all 0.3s ease;
343
- }
344
-
345
- .timeline-item.status-done .timeline-dot {
346
- background: var(--accent);
347
- border-color: var(--accent);
348
- box-shadow: 0 0 12px var(--accent);
349
- }
350
- .timeline-item.status-active .timeline-dot {
351
- background: var(--warn);
352
- border-color: var(--warn);
353
- animation: pulse 1s infinite;
354
- }
355
- .timeline-item.status-error .timeline-dot {
356
- background: var(--accent2);
357
- border-color: var(--accent2);
358
- }
359
-
360
- .timeline-content {
361
- background: var(--surface);
362
- border: 1px solid var(--border);
363
- border-radius: 6px;
364
- padding: 14px 16px;
365
- transition: border-color 0.3s;
366
- }
367
-
368
- .timeline-item.status-done .timeline-content { border-color: rgba(0,255,136,0.3); }
369
- .timeline-item.status-active .timeline-content { border-color: rgba(255,170,0,0.3); }
370
- .timeline-item.status-error .timeline-content { border-color: rgba(255,51,102,0.3); }
371
-
372
- .timeline-title {
373
- display: flex;
374
- align-items: center;
375
- gap: 8px;
376
- margin-bottom: 4px;
377
- }
378
-
379
- .timeline-title strong {
380
- font-size: 14px;
381
- font-weight: 700;
382
- }
383
-
384
- .timeline-title .tag {
385
- font-family: 'IBM Plex Mono', monospace;
386
- font-size: 10px;
387
- padding: 2px 8px;
388
- border-radius: 2px;
389
- background: rgba(255,255,255,0.05);
390
- color: var(--muted);
391
- letter-spacing: 1px;
392
- }
393
-
394
- .timeline-desc {
395
- font-size: 13px;
396
- color: var(--muted);
397
- line-height: 1.5;
398
- }
399
-
400
- .timeline-details {
401
- margin-top: 10px;
402
- padding: 10px 12px;
403
- background: rgba(0,0,0,0.3);
404
- border-radius: 4px;
405
- font-family: 'IBM Plex Mono', monospace;
406
- font-size: 11px;
407
- color: var(--muted);
408
- max-height: 0;
409
- overflow: hidden;
410
- transition: max-height 0.4s ease;
411
- line-height: 1.8;
412
- }
413
-
414
- .timeline-details.expanded { max-height: 400px; }
415
- .timeline-details .highlight { color: var(--accent); }
416
- .timeline-details .warn { color: var(--warn); }
417
- .timeline-details .danger { color: var(--accent2); }
418
-
419
- /* โ”€โ”€โ”€ Frame Log โ”€โ”€โ”€ */
420
- .frame-log {
421
- display: flex;
422
- flex-wrap: wrap;
423
- gap: 4px;
424
- margin-top: 8px;
425
- }
426
-
427
- .frame-badge {
428
- font-family: 'IBM Plex Mono', monospace;
429
- font-size: 10px;
430
- padding: 2px 6px;
431
- border-radius: 2px;
432
- border: 1px solid;
433
- }
434
-
435
- .frame-badge.clean {
436
- color: var(--accent);
437
- border-color: rgba(0,255,136,0.3);
438
- background: rgba(0,255,136,0.05);
439
- }
440
-
441
- .frame-badge.female {
442
- color: var(--accent2);
443
- border-color: rgba(255,51,102,0.3);
444
- background: rgba(255,51,102,0.05);
445
- }
446
-
447
- /* โ”€โ”€โ”€ Results โ”€โ”€โ”€ */
448
- .results-section {
449
- display: none;
450
- margin-bottom: 32px;
451
- }
452
-
453
- .result-card {
454
- background: var(--surface);
455
- border: 1px solid var(--border);
456
- border-radius: 8px;
457
- overflow: hidden;
458
- margin-bottom: 16px;
459
- }
460
-
461
- .result-card-header {
462
- padding: 16px 20px;
463
- display: flex;
464
- align-items: center;
465
- gap: 12px;
466
- border-bottom: 1px solid var(--border);
467
- }
468
-
469
- .result-card-header h3 { font-size: 15px; font-weight: 700; }
470
-
471
- .result-card-body { padding: 16px 20px; }
472
-
473
- .verdict {
474
- display: flex;
475
- align-items: center;
476
- gap: 16px;
477
- padding: 20px;
478
- border-radius: 6px;
479
- margin-bottom: 16px;
480
- }
481
-
482
- .verdict.clean {
483
- background: rgba(0,255,136,0.05);
484
- border: 1px solid rgba(0,255,136,0.2);
485
- }
486
-
487
- .verdict.female {
488
- background: rgba(255,51,102,0.05);
489
- border: 1px solid rgba(255,51,102,0.2);
490
- }
491
-
492
- .verdict-icon { font-size: 32px; }
493
- .verdict-text h4 { font-size: 18px; font-weight: 900; }
494
- .verdict-text p { font-size: 13px; color: var(--muted); margin-top: 4px; }
495
-
496
- .stats-grid {
497
- display: grid;
498
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
499
- gap: 12px;
500
- margin-bottom: 16px;
501
- }
502
-
503
- .stat-box {
504
- background: rgba(0,0,0,0.3);
505
- border: 1px solid var(--border);
506
- border-radius: 6px;
507
- padding: 14px;
508
- text-align: center;
509
- }
510
-
511
- .stat-box .val {
512
- font-family: 'IBM Plex Mono', monospace;
513
- font-size: 22px;
514
- font-weight: 600;
515
- color: var(--accent);
516
- display: block;
517
- }
518
-
519
- .stat-box .lbl {
520
- font-size: 12px;
521
- color: var(--muted);
522
- margin-top: 4px;
523
- display: block;
524
- }
525
-
526
- /* โ”€โ”€โ”€ Video Timeline Bar โ”€โ”€โ”€ */
527
- .video-timeline {
528
- background: rgba(0,0,0,0.3);
529
- border-radius: 4px;
530
- height: 40px;
531
- position: relative;
532
- overflow: hidden;
533
- margin: 16px 0;
534
- }
535
-
536
- .video-timeline .segment {
537
- position: absolute;
538
- height: 100%;
539
- top: 0;
540
- border-radius: 2px;
541
- }
542
-
543
- .video-timeline .seg-clean {
544
- background: rgba(0,255,136,0.3);
545
- border: 1px solid rgba(0,255,136,0.5);
546
- }
547
-
548
- .video-timeline .seg-female {
549
- background: rgba(255,51,102,0.3);
550
- border: 1px solid rgba(255,51,102,0.5);
551
- }
552
-
553
- .timeline-label {
554
- display: flex;
555
- justify-content: space-between;
556
- font-family: 'IBM Plex Mono', monospace;
557
- font-size: 11px;
558
- color: var(--muted);
559
- margin-top: 4px;
560
- }
561
-
562
- /* โ”€โ”€โ”€ Download โ”€โ”€โ”€ */
563
- .download-section {
564
- display: none;
565
- text-align: center;
566
- padding: 32px;
567
- background: var(--surface);
568
- border: 1px solid rgba(0,255,136,0.2);
569
- border-radius: 8px;
570
- margin-bottom: 32px;
571
- }
572
-
573
- .download-section h3 {
574
- font-size: 20px;
575
- font-weight: 900;
576
- margin-bottom: 8px;
577
- color: var(--accent);
578
- }
579
-
580
- .download-section p {
581
- color: var(--muted);
582
- font-size: 13px;
583
- margin-bottom: 24px;
584
- }
585
-
586
- /* โ”€โ”€โ”€ Spinner โ”€โ”€โ”€ */
587
- .spinner {
588
- width: 16px; height: 16px;
589
- border: 2px solid rgba(0,0,0,0.3);
590
- border-top-color: currentColor;
591
- border-radius: 50%;
592
- animation: spin 0.8s linear infinite;
593
- display: inline-block;
594
- }
595
-
596
- @keyframes spin { to { transform: rotate(360deg); } }
597
-
598
- /* โ”€โ”€โ”€ Alert โ”€โ”€โ”€ */
599
- .alert {
600
- padding: 12px 16px;
601
- border-radius: 6px;
602
- font-size: 13px;
603
- margin-bottom: 16px;
604
- display: none;
605
- }
606
- .alert.show { display: block; }
607
- .alert-warn { background: rgba(255,170,0,0.1); border: 1px solid rgba(255,170,0,0.3); color: var(--warn); }
608
- .alert-error { background: rgba(255,51,102,0.1); border: 1px solid rgba(255,51,102,0.3); color: var(--accent2); }
609
-
610
- </style>
611
- </head>
612
- <body>
613
-
614
- <div class="container">
615
-
616
- <!-- Header -->
617
- <div class="header">
618
- <div class="header-badge">AI VIDEO FILTER</div>
619
- <h1>ุชู†ู‚ูŠุฉ <span>ุงู„ููŠุฏูŠูˆ</span> ุงู„ุฅุนู„ุงู†ูŠ</h1>
620
- <p>ุฅุฒุงู„ุฉ ู…ู‚ุงุทุน ุงู„ู†ุณุงุก ุชู„ู‚ุงุฆูŠุงู‹ ุจุงุณุชุฎุฏุงู… BLIP + Florence-2</p>
621
- </div>
622
-
623
- <!-- API Status -->
624
- <div class="api-status">
625
- <div class="status-dot" id="statusDot"></div>
626
- <span id="statusText" style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--muted)">ุฌุงุฑูŠ ุงู„ุงุชุตุงู„ ุจุงู„ู€ API...</span>
627
- </div>
628
-
629
- <!-- Alert -->
630
- <div class="alert alert-warn" id="alertBox"></div>
631
-
632
- <!-- Upload Zone -->
633
- <div class="upload-zone" id="uploadZone">
634
- <span class="upload-icon">๐ŸŽฌ</span>
635
- <h3>ุงุณุญุจ ุงู„ููŠุฏูŠูˆ ู‡ู†ุง ุฃูˆ ุงุถุบุท ู„ู„ุงุฎุชูŠุงุฑ</h3>
636
- <p>MP4 / MOV / AVI ยท ุญุฏ ุฃู‚ุตู‰ 200MB</p>
637
- <input type="file" id="fileInput" accept="video/*">
638
- </div>
639
-
640
- <!-- Video Preview -->
641
- <div class="video-preview" id="videoPreview">
642
- <video id="videoPlayer" controls></video>
643
- <div class="video-meta" id="videoMeta"></div>
644
- </div>
645
-
646
- <!-- Upload Progress -->
647
- <div class="upload-progress" id="uploadProgress">
648
- <div class="progress-label">
649
- <span id="progressLabel">ุฌุงุฑูŠ ุงู„ุฑูุน...</span>
650
- <span id="progressPct">0%</span>
651
- </div>
652
- <div class="progress-bar">
653
- <div class="progress-fill" id="progressFill"></div>
654
- </div>
655
- </div>
656
-
657
- <!-- Action Buttons -->
658
- <div class="actions" id="actionsBar" style="display:none">
659
- <button class="btn btn-danger" id="btnQuickCheck" disabled>
660
- <span>๐Ÿ”</span> ูุญุต ุณุฑูŠุน
661
- </button>
662
- <button class="btn btn-primary" id="btnAnalyze" disabled>
663
- <span>โš™๏ธ</span> ุชุญู„ูŠู„ ูˆุชู‚ุทูŠุน
664
- </button>
665
- <button class="btn btn-secondary" id="btnReset" onclick="resetAll()">
666
- <span>โ†บ</span> ุฅุนุงุฏุฉ
667
- </button>
668
- </div>
669
-
670
- <!-- Timeline -->
671
- <div class="timeline-section" id="timelineSection">
672
- <div class="timeline-header">
673
- <h2>โฌก ุณุฌู„ ุงู„ุนู…ู„ูŠุงุช</h2>
674
- </div>
675
- <div class="timeline" id="timeline"></div>
676
- </div>
677
-
678
- <!-- Results -->
679
- <div class="results-section" id="resultsSection"></div>
680
-
681
- <!-- Download -->
682
- <div class="download-section" id="downloadSection">
683
- <h3>โœ… ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู ุฌุงู‡ุฒ</h3>
684
- <p id="downloadDesc"></p>
685
- <button class="btn btn-primary" id="btnDownload" style="max-width:240px;margin:0 auto">
686
- <span>โฌ‡๏ธ</span> ุชุญู…ูŠู„ ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู
687
- </button>
688
- </div>
689
-
690
- </div>
691
-
692
- <script>
693
- const API_BASE = window.location.origin;
694
- let currentFile = null;
695
- let currentJobId = null;
696
- let apiReady = false;
697
-
698
- // โ”€โ”€โ”€ API Status Check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
699
- async function checkApiStatus() {
700
- try {
701
- const res = await fetch(`${API_BASE}/health`);
702
- const data = await res.json();
703
- const dot = document.getElementById('statusDot');
704
- const txt = document.getElementById('statusText');
705
-
706
- if (data.status === 'ready') {
707
- dot.className = 'status-dot ready';
708
- txt.textContent = 'โœ… ุงู„ู†ู…ุงุฐุฌ ุฌุงู‡ุฒุฉ โ€” BLIP + Florence-2';
709
- txt.style.color = 'var(--accent)';
710
- apiReady = true;
711
- if (currentFile) setButtonsDisabled(false);
712
- } else if (data.status === 'loading') {
713
- dot.className = 'status-dot loading';
714
- txt.textContent = `โณ ${data.message}`;
715
- txt.style.color = 'var(--warn)';
716
- setTimeout(checkApiStatus, 3000);
717
- } else {
718
- dot.className = 'status-dot error';
719
- txt.textContent = `โŒ ${data.message}`;
720
- txt.style.color = 'var(--accent2)';
721
- }
722
- } catch (e) {
723
- const dot = document.getElementById('statusDot');
724
- dot.className = 'status-dot error';
725
- document.getElementById('statusText').textContent = 'โŒ ู„ุง ูŠู…ูƒู† ุงู„ุงุชุตุงู„ ุจุงู„ู€ API';
726
- setTimeout(checkApiStatus, 5000);
727
- }
728
- }
729
-
730
- // โ”€โ”€โ”€ Upload Zone โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
731
- const uploadZone = document.getElementById('uploadZone');
732
- const fileInput = document.getElementById('fileInput');
733
-
734
- uploadZone.addEventListener('click', () => fileInput.click());
735
- uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag-over'); });
736
- uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over'));
737
- uploadZone.addEventListener('drop', e => {
738
- e.preventDefault();
739
- uploadZone.classList.remove('drag-over');
740
- if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
741
- });
742
- fileInput.addEventListener('change', e => { if (e.target.files[0]) handleFile(e.target.files[0]); });
743
-
744
- function handleFile(file) {
745
- if (!file.type.startsWith('video/')) {
746
- showAlert('ุงู„ู…ู„ู ู„ูŠุณ ููŠุฏูŠูˆ! ูŠูู‚ุจู„ ูู‚ุท MP4, MOV, AVI', 'error');
747
- return;
748
- }
749
- if (file.size > 200 * 1024 * 1024) {
750
- showAlert('ุญุฌู… ุงู„ููŠุฏูŠูˆ ูŠุชุฌุงูˆุฒ 200MB', 'warn');
751
- return;
752
- }
753
-
754
- currentFile = file;
755
- showVideoPreview(file);
756
- showTimeline();
757
- addTimelineItem('ready', 'done', 'โธ ุจุงู†ุชุธุงุฑ ุงู„ุฅุฌุฑุงุก', 'READY',
758
- 'ุงู„ููŠุฏูŠูˆ ุฌุงู‡ุฒ. ูŠู…ูƒู†ูƒ ุชุดุบูŠู„ "ูุญุต ุณุฑูŠุน" ุฃูˆ "ุชุญู„ูŠู„ ูˆุชู‚ุทูŠุน".');
759
- document.getElementById('actionsBar').style.display = 'flex';
760
- setButtonsDisabled(false);
761
- }
762
-
763
- function showVideoPreview(file) {
764
- const preview = document.getElementById('videoPreview');
765
- const player = document.getElementById('videoPlayer');
766
- const meta = document.getElementById('videoMeta');
767
-
768
- player.src = URL.createObjectURL(file);
769
- preview.style.display = 'block';
770
- uploadZone.style.display = 'none';
771
-
772
- player.onloadedmetadata = () => {
773
- const dur = formatDuration(player.duration);
774
- const size = (file.size / 1024 / 1024).toFixed(1);
775
- meta.innerHTML = `
776
- <span>๐ŸŽฌ <strong>${file.name}</strong></span>
777
- <span>โฑ ู…ุฏุฉ: <strong>${dur}</strong></span>
778
- <span>๐Ÿ’พ ุงู„ุญุฌู…: <strong>${size} MB</strong></span>
779
- <span>๐Ÿ“ ${player.videoWidth}ร—${player.videoHeight}</span>
780
- `;
781
- };
782
- }
783
-
784
- // โ”€โ”€โ”€ Quick Check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
785
- document.getElementById('btnQuickCheck').addEventListener('click', async () => {
786
- if (!currentFile || !apiReady) return;
787
- setButtonsDisabled(true);
788
- addTimelineItem('quickcheck', 'active', '๐Ÿ” ูุญุต ุณุฑูŠุน ู„ู„ููŠุฏูŠูˆ', 'QUICK-CHECK',
789
- 'ุฌุงุฑูŠ ูุญุต ุฃูˆู„ frame ู…ู† ุงู„ููŠุฏูŠูˆ ู„ู„ุชุญู‚ู‚ ุงู„ุณุฑูŠุน ู…ู† ูˆุฌูˆุฏ ู†ุณุงุก...');
790
-
791
- try {
792
- // ู†ุณุชุฎุฑุฌ frame ู…ุจูƒุฑ ู…ู† ุงู„ููŠุฏูŠูˆ
793
- const canvas = document.createElement('canvas');
794
- const video = document.getElementById('videoPlayer');
795
- const seekTime = Math.min(1, Math.max(0, (video.duration || 1) * 0.1));
796
- video.currentTime = seekTime;
797
-
798
- await new Promise(r => { video.onseeked = r; });
799
-
800
- canvas.width = video.videoWidth;
801
- canvas.height = video.videoHeight;
802
- canvas.getContext('2d').drawImage(video, 0, 0);
803
-
804
- canvas.toBlob(async (blob) => {
805
- const formData = new FormData();
806
- formData.append('file', blob, 'frame.jpg');
807
-
808
- const res = await fetch(`${API_BASE}/analyze-file`, { method: 'POST', body: formData });
809
- const data = await res.json();
810
- if (!res.ok) {
811
- throw new Error(data.detail || 'ูุดู„ ุงู„ูุญุต ุงู„ุณุฑูŠุน');
812
- }
813
-
814
- if (data.decision === 'BLOCK' || data.decision === 'block') {
815
- updateTimelineItem('quickcheck', 'done',
816
- `๐Ÿ”ด ุชู… ุงูƒุชุดุงู ุงู…ุฑุฃุฉ ููŠ ุงู„ู€ frame ุงู„ุฃูˆู„ โ€” ูŠูุญุชู…ู„ ูˆุฌูˆุฏ ู†ุณุงุก ููŠ ุงู„ููŠุฏูŠูˆ`);
817
- addTimelineItem('qc-result', 'active', 'โš ๏ธ ุชุญุฐูŠุฑ: ูŠูˆุฌุฏ ู…ุญุชูˆู‰ ุฃู†ุซูˆูŠ', 'DETECTED',
818
- 'ุงู„ูุญุต ุงู„ุณุฑูŠุน ุงูƒุชุดู ุงู…ุฑุฃุฉ. ูŠูู†ุตุญ ุจุงู„ู…ุชุงุจุนุฉ ุฅู„ู‰ "ุชุญู„ูŠู„ ูˆุชู‚ุทูŠุน" ู„ู„ู…ุนุงู„ุฌุฉ ุงู„ูƒุงู…ู„ุฉ.');
819
- } else {
820
- updateTimelineItem('quickcheck', 'done',
821
- `๐ŸŸข ุงู„ู€ frame ุงู„ุฃูˆู„ ู†ุธูŠู โ€” ู„ุง ูŠูˆุฌุฏ ู†ุณุงุก ููŠ ุงู„ุจุฏุงูŠุฉ`);
822
- addTimelineItem('qc-result', 'done', 'โœ… ุงู„ูุญุต ุงู„ุฃูˆู„ูŠ ู†ุธูŠู', 'CLEAN',
823
- 'ู„ู… ูŠููƒุชุดู ู…ุญุชูˆู‰ ุฃู†ุซูˆูŠ ููŠ ุงู„ู€ frame ุงู„ุฃูˆู„. ูŠูู†ุตุญ ุจุงู„ู…ุชุงุจุนุฉ ู„ุชุญู„ูŠู„ ูƒุงู…ู„ ุงู„ููŠุฏูŠูˆ.');
824
- }
825
-
826
- setButtonsDisabled(false);
827
- }, 'image/jpeg', 0.9);
828
-
829
- } catch (e) {
830
- updateTimelineItem('quickcheck', 'error', `โŒ ุฎุทุฃ: ${e.message}`);
831
- setButtonsDisabled(false);
832
- }
833
- });
834
-
835
- // โ”€โ”€โ”€ Full Analysis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
836
- document.getElementById('btnAnalyze').addEventListener('click', () => {
837
- if (!currentFile || !apiReady) return;
838
- setButtonsDisabled(true);
839
-
840
- addTimelineItem('analyze', 'active', 'โš™๏ธ ุจุฏุก ุงู„ุชุญู„ูŠู„ ุงู„ูƒุงู…ู„', 'ANALYZING',
841
- 'ุฌุงุฑูŠ ุฑูุน ุงู„ููŠุฏูŠูˆ ูˆุชุญู„ูŠู„ ูƒู„ frame...');
842
-
843
- const formData = new FormData();
844
- formData.append('file', currentFile);
845
-
846
- const startTime = Date.now();
847
- const uploadProgress = document.getElementById('uploadProgress');
848
- const progressFill = document.getElementById('progressFill');
849
- const progressPct = document.getElementById('progressPct');
850
- const progressLabel = document.getElementById('progressLabel');
851
- uploadProgress.style.display = 'block';
852
- progressFill.style.width = '15%';
853
- progressPct.textContent = '15%';
854
- progressLabel.textContent = 'ุฌุงุฑูŠ ุฑูุน ุงู„ููŠุฏูŠูˆ...';
855
-
856
- fetch(`${API_BASE}/analyze-video`, {
857
- method: 'POST',
858
- body: formData
859
- })
860
- .then(async res => {
861
- const data = await res.json();
862
- if (!res.ok) throw new Error(data.detail || 'ูุดู„ ุงู„ุชุญู„ูŠู„');
863
- return data;
864
- })
865
- .then(data => {
866
- uploadProgress.style.display = 'none';
867
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
868
- currentJobId = data.output_job_id || null;
869
- updateTimelineItem('analyze', 'done', `โœ… ุงูƒุชู…ู„ ุงู„ุชุญู„ูŠู„ ููŠ ${elapsed}s`);
870
- handleAnalysisResult(data, elapsed);
871
- setButtonsDisabled(false);
872
- })
873
- .catch(e => {
874
- uploadProgress.style.display = 'none';
875
- updateTimelineItem('analyze', 'error', `โŒ ุฎุทุฃ: ${e.message}`);
876
- setButtonsDisabled(false);
877
- });
878
- });
879
-
880
- // โ”€โ”€โ”€ Handle Analysis Result โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
881
- function handleAnalysisResult(data, elapsed) {
882
- const resultsSection = document.getElementById('resultsSection');
883
- resultsSection.style.display = 'block';
884
-
885
- if (!data.has_female) {
886
- // โ”€โ”€ ู†ุธูŠู โ”€โ”€
887
- addTimelineItem('result', 'done', '๐ŸŸข ุงู„ููŠุฏูŠูˆ ู†ุธูŠู ุชู…ุงู…ุงู‹', 'CLEAN',
888
- 'ู„ู… ูŠููƒุชุดู ุฃูŠ ู…ุญุชูˆู‰ ุฃู†ุซูˆูŠ ููŠ ุงู„ููŠุฏูŠูˆ ูƒุงู…ู„ุงู‹. ุงู„ููŠุฏูŠูˆ ุฌุงู‡ุฒ ู„ู„ู†ุดุฑ.');
889
-
890
- resultsSection.innerHTML = `
891
- <div class="result-card">
892
- <div class="result-card-header">
893
- <span>๐Ÿ“Š</span>
894
- <h3>ู†ุชูŠุฌุฉ ุงู„ุชุญู„ูŠู„</h3>
895
- </div>
896
- <div class="result-card-body">
897
- <div class="verdict clean">
898
- <div class="verdict-icon">โœ…</div>
899
- <div class="verdict-text">
900
- <h4 style="color:var(--accent)">ุงู„ููŠุฏูŠูˆ ู†ุธูŠู</h4>
901
- <p>ู„ุง ูŠุญุชูˆูŠ ุนู„ู‰ ุฃูŠ ู…ุญุชูˆู‰ ุฃู†ุซูˆูŠ</p>
902
- </div>
903
- </div>
904
- <div class="stats-grid">
905
- <div class="stat-box">
906
- <span class="val">${data.analysis_log ? data.analysis_log.length : 'โ€”'}</span>
907
- <span class="lbl">frames ุชู… ุชุญู„ูŠู„ู‡ุง</span>
908
- </div>
909
- <div class="stat-box">
910
- <span class="val">${data.analysis_time || elapsed || 'โ€”'}s</span>
911
- <span class="lbl">ูˆู‚ุช ุงู„ุชุญู„ูŠู„</span>
912
- </div>
913
- <div class="stat-box">
914
- <span class="val" style="color:var(--accent)">0</span>
915
- <span class="lbl">ู…ู‚ุงุทุน ู…ุญุฐูˆูุฉ</span>
916
- </div>
917
- </div>
918
- ${renderFrameLog(data.analysis_log)}
919
- </div>
920
- </div>
921
- `;
922
- return;
923
- }
924
-
925
- // โ”€โ”€ ูŠุญุชูˆูŠ ุนู„ู‰ ู†ุณุงุก โ”€โ”€
926
- const femaleSegs = data.female_segments || [];
927
- const keptSegs = data.kept_segments || [];
928
- const totalRemoved = data.total_removed_sec || 0;
929
-
930
- if (femaleSegs.length > 0) {
931
- addTimelineItem('cutting', 'done', `โœ‚๏ธ ุชู… ุชู‚ุทูŠุน ${femaleSegs.length} ู…ู‚ุทุน`, 'CUTTING',
932
- `ุชู… ุญุฐู ${totalRemoved.toFixed(1)} ุซุงู†ูŠุฉ ุชุญุชูˆูŠ ุนู„ู‰ ู†ุณุงุก ูˆุจู†ุงุก ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู.`);
933
- }
934
-
935
- addTimelineItem('result', data.output_available ? 'done' : 'active',
936
- data.output_available ? '๐Ÿ“ฆ ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู ุฌุงู‡ุฒ' : 'โš ๏ธ ุงู„ููŠุฏูŠูˆ ูƒู„ู‡ ูŠุญุชูˆูŠ ู†ุณุงุก',
937
- 'RESULT', data.message);
938
-
939
- // ุญุณุงุจ ุงู„ู…ุฏุฉ ุงู„ุชู‚ุฑูŠุจูŠุฉ
940
- const totalDur = femaleSegs.length > 0 && keptSegs.length > 0
941
- ? (keptSegs[keptSegs.length-1][1] || 0)
942
- : 0;
943
-
944
- resultsSection.innerHTML = `
945
- <div class="result-card">
946
- <div class="result-card-header">
947
- <span>๐Ÿ“Š</span>
948
- <h3>ู†ุชูŠุฌุฉ ุงู„ุชุญู„ูŠู„</h3>
949
- </div>
950
- <div class="result-card-body">
951
- <div class="verdict female">
952
- <div class="verdict-icon">โš ๏ธ</div>
953
- <div class="verdict-text">
954
- <h4 style="color:var(--accent2)">ุชู… ุงูƒุชุดุงู ู…ุญุชูˆู‰ ุฃู†ุซูˆูŠ</h4>
955
- <p>${femaleSegs.length} ู…ู‚ุทุน ูŠุญุชูˆูŠ ุนู„ู‰ ู†ุณุงุก โ€” ุชู… ุญุฐูู‡ุง</p>
956
- </div>
957
- </div>
958
-
959
- <div class="stats-grid">
960
- <div class="stat-box">
961
- <span class="val" style="color:var(--accent2)">${femaleSegs.length}</span>
962
- <span class="lbl">ู…ู‚ุงุทุน ู…ุญุฐูˆูุฉ</span>
963
- </div>
964
- <div class="stat-box">
965
- <span class="val" style="color:var(--accent2)">${totalRemoved.toFixed(1)}s</span>
966
- <span class="lbl">ู…ุฏุฉ ู…ุญุฐูˆูุฉ</span>
967
- </div>
968
- <div class="stat-box">
969
- <span class="val">${data.analysis_log ? data.analysis_log.length : 'โ€”'}</span>
970
- <span class="lbl">frames ุชู… ุชุญู„ูŠู„ู‡ุง</span>
971
- </div>
972
- <div class="stat-box">
973
- <span class="val">${data.analysis_time || elapsed || 'โ€”'}s</span>
974
- <span class="lbl">ูˆู‚ุช ุงู„ุชุญู„ูŠู„</span>
975
- </div>
976
- </div>
977
-
978
- ${renderVideoTimeline(femaleSegs, keptSegs, totalDur)}
979
- ${renderSegmentsTable(femaleSegs, 'female')}
980
- ${renderFrameLog(data.analysis_log)}
981
- </div>
982
- </div>
983
- `;
984
-
985
- if (data.output_available && currentJobId) {
986
- const dl = document.getElementById('downloadSection');
987
- dl.style.display = 'block';
988
- document.getElementById('downloadDesc').textContent =
989
- `ุชู… ุญุฐู ${totalRemoved.toFixed(1)} ุซุงู†ูŠุฉ โ€” ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู ุฌุงู‡ุฒ ู„ู„ุชุญู…ูŠู„`;
990
-
991
- document.getElementById('btnDownload').onclick = () => {
992
- window.location.href = `${API_BASE}/download/${currentJobId}`;
993
- };
994
- }
995
- }
996
-
997
- // โ”€โ”€โ”€ Render Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
998
- function renderVideoTimeline(femaleSegs, keptSegs, totalDur) {
999
- if (!totalDur || totalDur === 0) return '';
1000
- const allSegs = [
1001
- ...femaleSegs.map(s => ({start: s[0], end: s[1], type: 'female'})),
1002
- ...keptSegs.map(s => ({start: s[0], end: s[1], type: 'clean'}))
1003
- ].sort((a,b) => a.start - b.start);
1004
-
1005
- const bars = allSegs.map(s => {
1006
- const left = (s.start / totalDur * 100).toFixed(1);
1007
- const width = ((s.end - s.start) / totalDur * 100).toFixed(1);
1008
- return `<div class="segment seg-${s.type}" style="left:${left}%;width:${width}%" title="${s.type}: ${s.start.toFixed(1)}s - ${s.end.toFixed(1)}s"></div>`;
1009
- }).join('');
1010
-
1011
- return `
1012
- <div style="margin:16px 0">
1013
- <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">
1014
- ุฎุฑูŠุทุฉ ุงู„ููŠุฏูŠูˆ โ€” <span style="color:var(--accent)">โ–  ู†ุธูŠู</span> <span style="color:var(--accent2)">โ–  ู…ุญุฐูˆู</span>
1015
- </div>
1016
- <div class="video-timeline">${bars}</div>
1017
- <div class="timeline-label"><span>0s</span><span>${totalDur.toFixed(0)}s</span></div>
1018
- </div>
1019
- `;
1020
- }
1021
-
1022
- function renderSegmentsTable(segs, type) {
1023
- if (!segs || segs.length === 0) return '';
1024
- const color = type === 'female' ? 'var(--accent2)' : 'var(--accent)';
1025
- const rows = segs.map((s, i) => `
1026
- <tr>
1027
- <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:${color};padding:6px 8px">#${i+1}</td>
1028
- <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;padding:6px 8px">${s[0].toFixed(1)}s</td>
1029
- <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;padding:6px 8px">${s[1].toFixed(1)}s</td>
1030
- <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);padding:6px 8px">${(s[1]-s[0]).toFixed(1)}s</td>
1031
- </tr>
1032
- `).join('');
1033
-
1034
- return `
1035
- <div style="margin-top:12px">
1036
- <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">ุงู„ู…ู‚ุงุทุน ุงู„ู…ุญุฐูˆูุฉ</div>
1037
- <table style="width:100%;border-collapse:collapse;background:rgba(0,0,0,0.3);border-radius:4px;overflow:hidden">
1038
- <tr style="background:rgba(255,255,255,0.03)">
1039
- <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">#</th>
1040
- <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">ุงู„ุจุฏุงูŠุฉ</th>
1041
- <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">ุงู„ู†ู‡ุงูŠุฉ</th>
1042
- <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">ุงู„ู…ุฏุฉ</th>
1043
- </tr>
1044
- ${rows}
1045
- </table>
1046
- </div>
1047
- `;
1048
- }
1049
-
1050
- function renderFrameLog(log) {
1051
- if (!log || log.length === 0) return '';
1052
- const badges = log.map(f =>
1053
- `<span class="frame-badge ${f.has_female ? 'female' : 'clean'}" title="${f.reason || ''}">${f.second}s</span>`
1054
- ).join('');
1055
- return `
1056
- <div style="margin-top:12px">
1057
- <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">
1058
- ุณุฌู„ ุงู„ู€ frames โ€” <span style="color:var(--accent)">โ–  ู†ุธูŠู</span> <span style="color:var(--accent2)">โ–  ูŠุญุชูˆูŠ ู†ุณุงุก</span>
1059
- </div>
1060
- <div class="frame-log">${badges}</div>
1061
- </div>
1062
- `;
1063
- }
1064
-
1065
- // โ”€โ”€โ”€ Timeline Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1066
- function showTimeline() {
1067
- document.getElementById('timelineSection').style.display = 'block';
1068
- }
1069
-
1070
- function addTimelineItem(id, status, title, tag, desc) {
1071
- const timeline = document.getElementById('timeline');
1072
- const existing = document.getElementById(`tl-${id}`);
1073
- if (existing) { updateTimelineItem(id, status, desc); return; }
1074
-
1075
- const item = document.createElement('div');
1076
- item.className = `timeline-item status-${status}`;
1077
- item.id = `tl-${id}`;
1078
- item.innerHTML = `
1079
- <div class="timeline-dot"></div>
1080
- <div class="timeline-content">
1081
- <div class="timeline-title">
1082
- <strong>${title}</strong>
1083
- <span class="tag">${tag}</span>
1084
- ${status === 'active' ? '<div class="spinner"></div>' : ''}
1085
- </div>
1086
- <div class="timeline-desc" id="tl-desc-${id}">${desc}</div>
1087
- </div>
1088
- `;
1089
- timeline.appendChild(item);
1090
- setTimeout(() => item.classList.add('visible'), 50);
1091
- }
1092
-
1093
- function updateTimelineItem(id, status, desc) {
1094
- const item = document.getElementById(`tl-${id}`);
1095
- if (!item) return;
1096
- item.className = `timeline-item status-${status} visible`;
1097
- const descEl = document.getElementById(`tl-desc-${id}`);
1098
- if (descEl && desc) descEl.textContent = desc;
1099
- // ุฅุฒุงู„ุฉ spinner
1100
- const spinner = item.querySelector('.spinner');
1101
- if (spinner) spinner.remove();
1102
- }
1103
-
1104
- // โ”€โ”€โ”€ Utils โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1105
- function formatDuration(sec) {
1106
- const m = Math.floor(sec / 60);
1107
- const s = Math.floor(sec % 60);
1108
- return `${m}:${s.toString().padStart(2,'0')}`;
1109
- }
1110
-
1111
- function showAlert(msg, type) {
1112
- const box = document.getElementById('alertBox');
1113
- box.textContent = msg;
1114
- box.className = `alert alert-${type} show`;
1115
- setTimeout(() => box.classList.remove('show'), 5000);
1116
- }
1117
-
1118
- function setButtonsDisabled(disabled) {
1119
- document.getElementById('btnQuickCheck').disabled = disabled || !apiReady;
1120
- document.getElementById('btnAnalyze').disabled = disabled || !apiReady;
1121
- }
1122
-
1123
- function resetAll() {
1124
- currentFile = null;
1125
- currentJobId = null;
1126
- document.getElementById('uploadZone').style.display = 'block';
1127
- document.getElementById('videoPreview').style.display = 'none';
1128
- document.getElementById('actionsBar').style.display = 'none';
1129
- document.getElementById('timelineSection').style.display = 'none';
1130
- document.getElementById('resultsSection').style.display = 'none';
1131
- document.getElementById('downloadSection').style.display = 'none';
1132
- document.getElementById('timeline').innerHTML = '';
1133
- document.getElementById('resultsSection').innerHTML = '';
1134
- document.getElementById('videoPlayer').src = '';
1135
- fileInput.value = '';
1136
- }
1137
-
1138
- // โ”€โ”€โ”€ Init โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1139
- checkApiStatus();
1140
- </script>
1141
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1142
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="ar" dir="rtl">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Video Filter AI</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Tajawal:wght@300;400;700;900&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg: #0a0a0f;
11
+ --surface: #111118;
12
+ --border: #1e1e2e;
13
+ --accent: #00ff88;
14
+ --accent2: #ff3366;
15
+ --accent3: #4488ff;
16
+ --text: #e8e8f0;
17
+ --muted: #555570;
18
+ --warn: #ffaa00;
19
+ }
20
+
21
+ * { margin: 0; padding: 0; box-sizing: border-box; }
22
+
23
+ body {
24
+ font-family: 'Tajawal', sans-serif;
25
+ background: var(--bg);
26
+ color: var(--text);
27
+ min-height: 100vh;
28
+ overflow-x: hidden;
29
+ }
30
+
31
+ /* โ”€โ”€โ”€ Grid Background โ”€โ”€โ”€ */
32
+ body::before {
33
+ content: '';
34
+ position: fixed;
35
+ inset: 0;
36
+ background-image:
37
+ linear-gradient(rgba(0,255,136,0.03) 1px, transparent 1px),
38
+ linear-gradient(90deg, rgba(0,255,136,0.03) 1px, transparent 1px);
39
+ background-size: 40px 40px;
40
+ pointer-events: none;
41
+ z-index: 0;
42
+ }
43
+
44
+ .container {
45
+ max-width: 900px;
46
+ margin: 0 auto;
47
+ padding: 40px 20px;
48
+ position: relative;
49
+ z-index: 1;
50
+ }
51
+
52
+ /* โ”€โ”€โ”€ Header โ”€โ”€โ”€ */
53
+ .header {
54
+ text-align: center;
55
+ margin-bottom: 48px;
56
+ }
57
+
58
+ .header-badge {
59
+ display: inline-block;
60
+ font-family: 'IBM Plex Mono', monospace;
61
+ font-size: 11px;
62
+ color: var(--accent);
63
+ border: 1px solid var(--accent);
64
+ padding: 4px 12px;
65
+ border-radius: 2px;
66
+ letter-spacing: 3px;
67
+ margin-bottom: 16px;
68
+ text-transform: uppercase;
69
+ }
70
+
71
+ .header h1 {
72
+ font-size: 42px;
73
+ font-weight: 900;
74
+ line-height: 1.1;
75
+ margin-bottom: 8px;
76
+ }
77
+
78
+ .header h1 span { color: var(--accent); }
79
+
80
+ .header p {
81
+ color: var(--muted);
82
+ font-size: 15px;
83
+ font-weight: 300;
84
+ }
85
+
86
+ /* โ”€โ”€โ”€ API Status โ”€โ”€โ”€ */
87
+ .api-status {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 8px;
91
+ justify-content: center;
92
+ margin-bottom: 32px;
93
+ font-family: 'IBM Plex Mono', monospace;
94
+ font-size: 12px;
95
+ }
96
+
97
+ .status-dot {
98
+ width: 8px; height: 8px;
99
+ border-radius: 50%;
100
+ background: var(--muted);
101
+ animation: none;
102
+ }
103
+ .status-dot.ready { background: var(--accent); animation: pulse 2s infinite; }
104
+ .status-dot.loading { background: var(--warn); animation: pulse 1s infinite; }
105
+ .status-dot.error { background: var(--accent2); }
106
+
107
+ @keyframes pulse {
108
+ 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 currentColor; }
109
+ 50% { opacity: 0.7; box-shadow: 0 0 0 4px transparent; }
110
+ }
111
+
112
+ /* โ”€โ”€โ”€ Upload Zone โ”€โ”€โ”€ */
113
+ .upload-zone {
114
+ border: 1px dashed var(--border);
115
+ border-radius: 8px;
116
+ padding: 60px 40px;
117
+ text-align: center;
118
+ cursor: pointer;
119
+ transition: all 0.3s ease;
120
+ background: var(--surface);
121
+ position: relative;
122
+ overflow: hidden;
123
+ }
124
+
125
+ .upload-zone::before {
126
+ content: '';
127
+ position: absolute;
128
+ inset: 0;
129
+ background: linear-gradient(135deg, rgba(0,255,136,0.03), transparent);
130
+ opacity: 0;
131
+ transition: opacity 0.3s;
132
+ }
133
+
134
+ .upload-zone:hover, .upload-zone.drag-over {
135
+ border-color: var(--accent);
136
+ background: rgba(0,255,136,0.03);
137
+ }
138
+
139
+ .upload-zone:hover::before, .upload-zone.drag-over::before { opacity: 1; }
140
+
141
+ .upload-icon {
142
+ font-size: 48px;
143
+ margin-bottom: 16px;
144
+ display: block;
145
+ }
146
+
147
+ .upload-zone h3 {
148
+ font-size: 18px;
149
+ font-weight: 700;
150
+ margin-bottom: 8px;
151
+ }
152
+
153
+ .upload-zone p {
154
+ color: var(--muted);
155
+ font-size: 13px;
156
+ font-family: 'IBM Plex Mono', monospace;
157
+ }
158
+
159
+ #fileInput { display: none; }
160
+
161
+ /* โ”€โ”€โ”€ Video Preview โ”€โ”€โ”€ */
162
+ .video-preview {
163
+ display: none;
164
+ background: var(--surface);
165
+ border: 1px solid var(--border);
166
+ border-radius: 8px;
167
+ overflow: hidden;
168
+ margin-bottom: 24px;
169
+ }
170
+
171
+ .video-preview video {
172
+ width: 100%;
173
+ max-height: 300px;
174
+ display: block;
175
+ background: #000;
176
+ }
177
+
178
+ .video-meta {
179
+ padding: 16px 20px;
180
+ display: flex;
181
+ gap: 24px;
182
+ font-family: 'IBM Plex Mono', monospace;
183
+ font-size: 12px;
184
+ color: var(--muted);
185
+ border-top: 1px solid var(--border);
186
+ flex-wrap: wrap;
187
+ }
188
+
189
+ .video-meta span { display: flex; align-items: center; gap: 6px; }
190
+ .video-meta strong { color: var(--text); }
191
+
192
+ /* โ”€โ”€โ”€ Upload Progress โ”€โ”€โ”€ */
193
+ .upload-progress {
194
+ display: none;
195
+ margin-bottom: 24px;
196
+ }
197
+
198
+ .progress-label {
199
+ display: flex;
200
+ justify-content: space-between;
201
+ font-family: 'IBM Plex Mono', monospace;
202
+ font-size: 12px;
203
+ color: var(--muted);
204
+ margin-bottom: 8px;
205
+ }
206
+
207
+ .progress-bar {
208
+ height: 3px;
209
+ background: var(--border);
210
+ border-radius: 2px;
211
+ overflow: hidden;
212
+ }
213
+
214
+ .progress-fill {
215
+ height: 100%;
216
+ background: var(--accent);
217
+ border-radius: 2px;
218
+ transition: width 0.3s ease;
219
+ width: 0%;
220
+ box-shadow: 0 0 8px var(--accent);
221
+ }
222
+
223
+ /* โ”€โ”€โ”€ Action Buttons โ”€โ”€โ”€ */
224
+ .actions {
225
+ display: flex;
226
+ gap: 12px;
227
+ margin-bottom: 32px;
228
+ flex-wrap: wrap;
229
+ }
230
+
231
+ .btn {
232
+ flex: 1;
233
+ padding: 14px 24px;
234
+ border: none;
235
+ border-radius: 6px;
236
+ font-family: 'Tajawal', sans-serif;
237
+ font-size: 15px;
238
+ font-weight: 700;
239
+ cursor: pointer;
240
+ transition: all 0.2s ease;
241
+ display: flex;
242
+ align-items: center;
243
+ justify-content: center;
244
+ gap: 8px;
245
+ min-width: 160px;
246
+ }
247
+
248
+ .btn:disabled {
249
+ opacity: 0.3;
250
+ cursor: not-allowed;
251
+ transform: none !important;
252
+ }
253
+
254
+ .btn-primary {
255
+ background: var(--accent);
256
+ color: #000;
257
+ }
258
+ .btn-primary:not(:disabled):hover {
259
+ transform: translateY(-2px);
260
+ box-shadow: 0 8px 24px rgba(0,255,136,0.3);
261
+ }
262
+
263
+ .btn-danger {
264
+ background: transparent;
265
+ color: var(--accent2);
266
+ border: 1px solid var(--accent2);
267
+ }
268
+ .btn-danger:not(:disabled):hover {
269
+ background: rgba(255,51,102,0.1);
270
+ transform: translateY(-2px);
271
+ }
272
+
273
+ .btn-secondary {
274
+ background: transparent;
275
+ color: var(--accent3);
276
+ border: 1px solid var(--accent3);
277
+ }
278
+ .btn-secondary:not(:disabled):hover {
279
+ background: rgba(68,136,255,0.1);
280
+ transform: translateY(-2px);
281
+ }
282
+
283
+ /* โ”€โ”€โ”€ Timeline โ”€โ”€โ”€ */
284
+ .timeline-section {
285
+ display: none;
286
+ margin-bottom: 32px;
287
+ }
288
+
289
+ .timeline-header {
290
+ display: flex;
291
+ align-items: center;
292
+ gap: 12px;
293
+ margin-bottom: 20px;
294
+ }
295
+
296
+ .timeline-header h2 {
297
+ font-size: 16px;
298
+ font-weight: 700;
299
+ font-family: 'IBM Plex Mono', monospace;
300
+ color: var(--accent);
301
+ letter-spacing: 1px;
302
+ text-transform: uppercase;
303
+ }
304
+
305
+ .timeline {
306
+ position: relative;
307
+ padding-right: 32px;
308
+ }
309
+
310
+ .timeline::before {
311
+ content: '';
312
+ position: absolute;
313
+ right: 11px;
314
+ top: 0;
315
+ bottom: 0;
316
+ width: 1px;
317
+ background: var(--border);
318
+ }
319
+
320
+ .timeline-item {
321
+ position: relative;
322
+ padding: 0 24px 24px 0;
323
+ opacity: 0;
324
+ transform: translateX(10px);
325
+ transition: all 0.4s ease;
326
+ }
327
+
328
+ .timeline-item.visible {
329
+ opacity: 1;
330
+ transform: translateX(0);
331
+ }
332
+
333
+ .timeline-dot {
334
+ position: absolute;
335
+ right: -21px;
336
+ top: 4px;
337
+ width: 14px;
338
+ height: 14px;
339
+ border-radius: 50%;
340
+ border: 2px solid var(--border);
341
+ background: var(--bg);
342
+ transition: all 0.3s ease;
343
+ }
344
+
345
+ .timeline-item.status-done .timeline-dot {
346
+ background: var(--accent);
347
+ border-color: var(--accent);
348
+ box-shadow: 0 0 12px var(--accent);
349
+ }
350
+ .timeline-item.status-active .timeline-dot {
351
+ background: var(--warn);
352
+ border-color: var(--warn);
353
+ animation: pulse 1s infinite;
354
+ }
355
+ .timeline-item.status-error .timeline-dot {
356
+ background: var(--accent2);
357
+ border-color: var(--accent2);
358
+ }
359
+
360
+ .timeline-content {
361
+ background: var(--surface);
362
+ border: 1px solid var(--border);
363
+ border-radius: 6px;
364
+ padding: 14px 16px;
365
+ transition: border-color 0.3s;
366
+ }
367
+
368
+ .timeline-item.status-done .timeline-content { border-color: rgba(0,255,136,0.3); }
369
+ .timeline-item.status-active .timeline-content { border-color: rgba(255,170,0,0.3); }
370
+ .timeline-item.status-error .timeline-content { border-color: rgba(255,51,102,0.3); }
371
+
372
+ .timeline-title {
373
+ display: flex;
374
+ align-items: center;
375
+ gap: 8px;
376
+ margin-bottom: 4px;
377
+ }
378
+
379
+ .timeline-title strong {
380
+ font-size: 14px;
381
+ font-weight: 700;
382
+ }
383
+
384
+ .timeline-title .tag {
385
+ font-family: 'IBM Plex Mono', monospace;
386
+ font-size: 10px;
387
+ padding: 2px 8px;
388
+ border-radius: 2px;
389
+ background: rgba(255,255,255,0.05);
390
+ color: var(--muted);
391
+ letter-spacing: 1px;
392
+ }
393
+
394
+ .timeline-desc {
395
+ font-size: 13px;
396
+ color: var(--muted);
397
+ line-height: 1.5;
398
+ }
399
+
400
+ .timeline-details {
401
+ margin-top: 10px;
402
+ padding: 10px 12px;
403
+ background: rgba(0,0,0,0.3);
404
+ border-radius: 4px;
405
+ font-family: 'IBM Plex Mono', monospace;
406
+ font-size: 11px;
407
+ color: var(--muted);
408
+ max-height: 0;
409
+ overflow: hidden;
410
+ transition: max-height 0.4s ease;
411
+ line-height: 1.8;
412
+ }
413
+
414
+ .timeline-details.expanded { max-height: 400px; }
415
+ .timeline-details .highlight { color: var(--accent); }
416
+ .timeline-details .warn { color: var(--warn); }
417
+ .timeline-details .danger { color: var(--accent2); }
418
+
419
+ /* โ”€โ”€โ”€ Frame Log โ”€โ”€โ”€ */
420
+ .frame-log {
421
+ display: flex;
422
+ flex-wrap: wrap;
423
+ gap: 4px;
424
+ margin-top: 8px;
425
+ }
426
+
427
+ .frame-badge {
428
+ font-family: 'IBM Plex Mono', monospace;
429
+ font-size: 10px;
430
+ padding: 2px 6px;
431
+ border-radius: 2px;
432
+ border: 1px solid;
433
+ }
434
+
435
+ .frame-badge.clean {
436
+ color: var(--accent);
437
+ border-color: rgba(0,255,136,0.3);
438
+ background: rgba(0,255,136,0.05);
439
+ }
440
+
441
+ .frame-badge.female {
442
+ color: var(--accent2);
443
+ border-color: rgba(255,51,102,0.3);
444
+ background: rgba(255,51,102,0.05);
445
+ }
446
+
447
+ /* โ”€โ”€โ”€ Results โ”€โ”€โ”€ */
448
+ .results-section {
449
+ display: none;
450
+ margin-bottom: 32px;
451
+ }
452
+
453
+ .result-card {
454
+ background: var(--surface);
455
+ border: 1px solid var(--border);
456
+ border-radius: 8px;
457
+ overflow: hidden;
458
+ margin-bottom: 16px;
459
+ }
460
+
461
+ .result-card-header {
462
+ padding: 16px 20px;
463
+ display: flex;
464
+ align-items: center;
465
+ gap: 12px;
466
+ border-bottom: 1px solid var(--border);
467
+ }
468
+
469
+ .result-card-header h3 { font-size: 15px; font-weight: 700; }
470
+
471
+ .result-card-body { padding: 16px 20px; }
472
+
473
+ .verdict {
474
+ display: flex;
475
+ align-items: center;
476
+ gap: 16px;
477
+ padding: 20px;
478
+ border-radius: 6px;
479
+ margin-bottom: 16px;
480
+ }
481
+
482
+ .verdict.clean {
483
+ background: rgba(0,255,136,0.05);
484
+ border: 1px solid rgba(0,255,136,0.2);
485
+ }
486
+
487
+ .verdict.female {
488
+ background: rgba(255,51,102,0.05);
489
+ border: 1px solid rgba(255,51,102,0.2);
490
+ }
491
+
492
+ .verdict-icon { font-size: 32px; }
493
+ .verdict-text h4 { font-size: 18px; font-weight: 900; }
494
+ .verdict-text p { font-size: 13px; color: var(--muted); margin-top: 4px; }
495
+
496
+ .stats-grid {
497
+ display: grid;
498
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
499
+ gap: 12px;
500
+ margin-bottom: 16px;
501
+ }
502
+
503
+ .stat-box {
504
+ background: rgba(0,0,0,0.3);
505
+ border: 1px solid var(--border);
506
+ border-radius: 6px;
507
+ padding: 14px;
508
+ text-align: center;
509
+ }
510
+
511
+ .stat-box .val {
512
+ font-family: 'IBM Plex Mono', monospace;
513
+ font-size: 22px;
514
+ font-weight: 600;
515
+ color: var(--accent);
516
+ display: block;
517
+ }
518
+
519
+ .stat-box .lbl {
520
+ font-size: 12px;
521
+ color: var(--muted);
522
+ margin-top: 4px;
523
+ display: block;
524
+ }
525
+
526
+ /* โ”€โ”€โ”€ Video Timeline Bar โ”€โ”€โ”€ */
527
+ .video-timeline {
528
+ background: rgba(0,0,0,0.3);
529
+ border-radius: 4px;
530
+ height: 40px;
531
+ position: relative;
532
+ overflow: hidden;
533
+ margin: 16px 0;
534
+ }
535
+
536
+ .video-timeline .segment {
537
+ position: absolute;
538
+ height: 100%;
539
+ top: 0;
540
+ border-radius: 2px;
541
+ }
542
+
543
+ .video-timeline .seg-clean {
544
+ background: rgba(0,255,136,0.3);
545
+ border: 1px solid rgba(0,255,136,0.5);
546
+ }
547
+
548
+ .video-timeline .seg-female {
549
+ background: rgba(255,51,102,0.3);
550
+ border: 1px solid rgba(255,51,102,0.5);
551
+ }
552
+
553
+ .timeline-label {
554
+ display: flex;
555
+ justify-content: space-between;
556
+ font-family: 'IBM Plex Mono', monospace;
557
+ font-size: 11px;
558
+ color: var(--muted);
559
+ margin-top: 4px;
560
+ }
561
+
562
+ /* โ”€โ”€โ”€ Download โ”€โ”€โ”€ */
563
+ .download-section {
564
+ display: none;
565
+ text-align: center;
566
+ padding: 32px;
567
+ background: var(--surface);
568
+ border: 1px solid rgba(0,255,136,0.2);
569
+ border-radius: 8px;
570
+ margin-bottom: 32px;
571
+ }
572
+
573
+ .download-section h3 {
574
+ font-size: 20px;
575
+ font-weight: 900;
576
+ margin-bottom: 8px;
577
+ color: var(--accent);
578
+ }
579
+
580
+ .download-section p {
581
+ color: var(--muted);
582
+ font-size: 13px;
583
+ margin-bottom: 24px;
584
+ }
585
+
586
+ /* โ”€โ”€โ”€ Spinner โ”€โ”€โ”€ */
587
+ .spinner {
588
+ width: 16px; height: 16px;
589
+ border: 2px solid rgba(0,0,0,0.3);
590
+ border-top-color: currentColor;
591
+ border-radius: 50%;
592
+ animation: spin 0.8s linear infinite;
593
+ display: inline-block;
594
+ }
595
+
596
+ @keyframes spin { to { transform: rotate(360deg); } }
597
+
598
+ /* โ”€โ”€โ”€ Alert โ”€โ”€โ”€ */
599
+ .alert {
600
+ padding: 12px 16px;
601
+ border-radius: 6px;
602
+ font-size: 13px;
603
+ margin-bottom: 16px;
604
+ display: none;
605
+ }
606
+ .alert.show { display: block; }
607
+ .alert-warn { background: rgba(255,170,0,0.1); border: 1px solid rgba(255,170,0,0.3); color: var(--warn); }
608
+ .alert-error { background: rgba(255,51,102,0.1); border: 1px solid rgba(255,51,102,0.3); color: var(--accent2); }
609
+
610
+ </style>
611
+ </head>
612
+ <body>
613
+
614
+ <div class="container">
615
+
616
+ <!-- Header -->
617
+ <div class="header">
618
+ <div class="header-badge">AI VIDEO FILTER</div>
619
+ <h1>ุชู†ู‚ูŠุฉ <span>ุงู„ููŠุฏูŠูˆ</span> ุงู„ุฅุนู„ุงู†ูŠ</h1>
620
+ <p>ุฅุฒุงู„ุฉ ู…ู‚ุงุทุน ุงู„ู†ุณุงุก ุชู„ู‚ุงุฆูŠุงู‹ ุจุงุณุชุฎุฏุงู… BLIP + Florence-2</p>
621
+ </div>
622
+
623
+ <!-- API Status -->
624
+ <div class="api-status">
625
+ <div class="status-dot" id="statusDot"></div>
626
+ <span id="statusText" style="font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--muted)">ุฌุงุฑูŠ ุงู„ุงุชุตุงู„ ุจุงู„ู€ API...</span>
627
+ </div>
628
+
629
+ <!-- Alert -->
630
+ <div class="alert alert-warn" id="alertBox"></div>
631
+
632
+ <!-- Upload Zone -->
633
+ <div class="upload-zone" id="uploadZone">
634
+ <span class="upload-icon">๐ŸŽฌ</span>
635
+ <h3>ุงุณุญุจ ุงู„ููŠุฏูŠูˆ ู‡ู†ุง ุฃูˆ ุงุถุบุท ู„ู„ุงุฎุชูŠุงุฑ</h3>
636
+ <p>MP4 / MOV / AVI ยท ุญุฏ ุฃู‚ุตู‰ 200MB</p>
637
+ <input type="file" id="fileInput" accept="video/*">
638
+ </div>
639
+
640
+ <!-- Video Preview -->
641
+ <div class="video-preview" id="videoPreview">
642
+ <video id="videoPlayer" controls></video>
643
+ <div class="video-meta" id="videoMeta"></div>
644
+ </div>
645
+
646
+ <!-- Upload Progress -->
647
+ <div class="upload-progress" id="uploadProgress">
648
+ <div class="progress-label">
649
+ <span id="progressLabel">ุฌุงุฑูŠ ุงู„ุฑูุน...</span>
650
+ <span id="progressPct">0%</span>
651
+ </div>
652
+ <div class="progress-bar">
653
+ <div class="progress-fill" id="progressFill"></div>
654
+ </div>
655
+ </div>
656
+
657
+ <!-- Action Buttons -->
658
+ <div class="actions" id="actionsBar" style="display:none">
659
+ <button class="btn btn-danger" id="btnQuickCheck" disabled>
660
+ <span>๐Ÿ”</span> ูุญุต ุณุฑูŠุน
661
+ </button>
662
+ <button class="btn btn-primary" id="btnAnalyze" disabled>
663
+ <span>โš™๏ธ</span> ุชุญู„ูŠู„ ูˆุชู‚ุทูŠุน
664
+ </button>
665
+ <button class="btn btn-secondary" id="btnReset" onclick="resetAll()">
666
+ <span>โ†บ</span> ุฅุนุงุฏุฉ
667
+ </button>
668
+ </div>
669
+
670
+ <!-- Timeline -->
671
+ <div class="timeline-section" id="timelineSection">
672
+ <div class="timeline-header">
673
+ <h2>โฌก ุณุฌู„ ุงู„ุนู…ู„ูŠุงุช</h2>
674
+ </div>
675
+ <div class="timeline" id="timeline"></div>
676
+ </div>
677
+
678
+ <!-- Results -->
679
+ <div class="results-section" id="resultsSection"></div>
680
+
681
+ <!-- Download -->
682
+ <div class="download-section" id="downloadSection">
683
+ <h3>โœ… ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู ุฌุงู‡ุฒ</h3>
684
+ <p id="downloadDesc"></p>
685
+ <button class="btn btn-primary" id="btnDownload" style="max-width:240px;margin:0 auto">
686
+ <span>โฌ‡๏ธ</span> ุชุญู…ูŠู„ ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู
687
+ </button>
688
+ </div>
689
+
690
+ </div>
691
+
692
+ <script>
693
+ const API_BASE = window.location.origin;
694
+ let currentFile = null;
695
+ let currentJobId = null;
696
+ let apiReady = false;
697
+
698
+ // โ”€โ”€โ”€ API Status Check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
699
+ async function checkApiStatus() {
700
+ try {
701
+ const res = await fetch(`${API_BASE}/health`);
702
+ const data = await res.json();
703
+ const dot = document.getElementById('statusDot');
704
+ const txt = document.getElementById('statusText');
705
+
706
+ if (data.status === 'ready') {
707
+ dot.className = 'status-dot ready';
708
+ txt.textContent = 'โœ… ุงู„ู†ู…ุงุฐุฌ ุฌุงู‡ุฒุฉ โ€” BLIP + Florence-2';
709
+ txt.style.color = 'var(--accent)';
710
+ apiReady = true;
711
+ } else if (data.status === 'loading') {
712
+ dot.className = 'status-dot loading';
713
+ txt.textContent = `โณ ${data.message}`;
714
+ txt.style.color = 'var(--warn)';
715
+ setTimeout(checkApiStatus, 3000);
716
+ } else {
717
+ dot.className = 'status-dot error';
718
+ txt.textContent = `โŒ ${data.message}`;
719
+ txt.style.color = 'var(--accent2)';
720
+ }
721
+ } catch (e) {
722
+ const dot = document.getElementById('statusDot');
723
+ dot.className = 'status-dot error';
724
+ document.getElementById('statusText').textContent = 'โŒ ู„ุง ูŠู…ูƒู† ุงู„ุงุชุตุงู„ ุจุงู„ู€ API';
725
+ setTimeout(checkApiStatus, 5000);
726
+ }
727
+ }
728
+
729
+ // โ”€โ”€โ”€ Upload Zone โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
730
+ const uploadZone = document.getElementById('uploadZone');
731
+ const fileInput = document.getElementById('fileInput');
732
+
733
+ uploadZone.addEventListener('click', () => fileInput.click());
734
+ uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag-over'); });
735
+ uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over'));
736
+ uploadZone.addEventListener('drop', e => {
737
+ e.preventDefault();
738
+ uploadZone.classList.remove('drag-over');
739
+ if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
740
+ });
741
+ fileInput.addEventListener('change', e => { if (e.target.files[0]) handleFile(e.target.files[0]); });
742
+
743
+ function handleFile(file) {
744
+ if (!file.type.startsWith('video/')) {
745
+ showAlert('ุงู„ู…ู„ู ู„ูŠุณ ููŠุฏูŠูˆ! ูŠูู‚ุจู„ ูู‚ุท MP4, MOV, AVI', 'error');
746
+ return;
747
+ }
748
+ if (file.size > 200 * 1024 * 1024) {
749
+ showAlert('ุญุฌู… ุงู„ููŠุฏูŠูˆ ูŠุชุฌุงูˆุฒ 200MB', 'warn');
750
+ return;
751
+ }
752
+
753
+ currentFile = file;
754
+ showVideoPreview(file);
755
+ uploadFile(file);
756
+ }
757
+
758
+ function showVideoPreview(file) {
759
+ const preview = document.getElementById('videoPreview');
760
+ const player = document.getElementById('videoPlayer');
761
+ const meta = document.getElementById('videoMeta');
762
+
763
+ player.src = URL.createObjectURL(file);
764
+ preview.style.display = 'block';
765
+ uploadZone.style.display = 'none';
766
+
767
+ player.onloadedmetadata = () => {
768
+ const dur = formatDuration(player.duration);
769
+ const size = (file.size / 1024 / 1024).toFixed(1);
770
+ meta.innerHTML = `
771
+ <span>๐ŸŽฌ <strong>${file.name}</strong></span>
772
+ <span>โฑ ู…ุฏุฉ: <strong>${dur}</strong></span>
773
+ <span>๐Ÿ’พ ุงู„ุญุฌู…: <strong>${size} MB</strong></span>
774
+ <span>๐Ÿ“ ${player.videoWidth}ร—${player.videoHeight}</span>
775
+ `;
776
+ };
777
+ }
778
+
779
+ // โ”€โ”€โ”€ Upload with Progress โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
780
+ function uploadFile(file) {
781
+ const uploadProgress = document.getElementById('uploadProgress');
782
+ const progressFill = document.getElementById('progressFill');
783
+ const progressPct = document.getElementById('progressPct');
784
+ const progressLabel = document.getElementById('progressLabel');
785
+
786
+ uploadProgress.style.display = 'block';
787
+ showTimeline();
788
+ addTimelineItem('upload', 'active', '๐Ÿ“ค ุชุญู…ูŠู„ ุงู„ููŠุฏูŠูˆ', 'UPLOAD', `ุฌุงุฑูŠ ุชุญุถูŠุฑ ${file.name}...`);
789
+
790
+ // โœ… ู†ุญุถู‘ุฑ ุงู„ููŠุฏูŠูˆ ู…ุญู„ูŠุงู‹ ูู‚ุท - ู„ุง ู†ุฑูุนู‡ ุงู„ุขู†
791
+ // ุงู„ุฑูุน ูŠุญุฏุซ ูู‚ุท ุนู†ุฏ ุงู„ุถุบุท ุนู„ู‰ ุฃุฒุฑุงุฑ ุงู„ุฅุฌุฑุงุกุง๏ฟฝ๏ฟฝ
792
+ let progress = 0;
793
+ const interval = setInterval(() => {
794
+ progress = Math.min(progress + 10, 90);
795
+ progressFill.style.width = progress + '%';
796
+ progressPct.textContent = progress + '%';
797
+ progressLabel.textContent = `ุฌุงุฑูŠ ุชุญุถูŠุฑ ุงู„ููŠุฏูŠูˆ...`;
798
+ if (progress >= 90) {
799
+ clearInterval(interval);
800
+ setTimeout(() => {
801
+ progressFill.style.width = '100%';
802
+ progressPct.textContent = '100%';
803
+ setTimeout(() => {
804
+ uploadProgress.style.display = 'none';
805
+ updateTimelineItem('upload', 'done', `โœ… ุงู„ููŠุฏูŠูˆ ุฌุงู‡ุฒ: ${file.name}`);
806
+ document.getElementById('actionsBar').style.display = 'flex';
807
+ document.getElementById('btnQuickCheck').disabled = !apiReady;
808
+ document.getElementById('btnAnalyze').disabled = !apiReady;
809
+ addTimelineItem('ready', 'done', 'โธ ุจุงู†ุชุธุงุฑ ุงู„ุฅุฌุฑุงุก', 'READY',
810
+ 'ุงู„ููŠุฏูŠูˆ ุฌุงู‡ุฒ โ€” ุงุฎุชุฑ "ูุญุต ุณุฑูŠุน" ู„ู„ุชุญู‚ู‚ ุงู„ุณุฑูŠุน ุฃูˆ "ุชุญู„ูŠู„ ูˆุชู‚ุทูŠุน" ู„ู„ู…ุนุงู„ุฌุฉ ุงู„ูƒุงู…ู„ุฉ');
811
+ }, 300);
812
+ }, 200);
813
+ }
814
+ }, 80);
815
+ }
816
+
817
+ // โ”€โ”€โ”€ Quick Check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
818
+ document.getElementById('btnQuickCheck').addEventListener('click', async () => {
819
+ if (!currentFile || !apiReady) return;
820
+ setButtonsDisabled(true);
821
+ addTimelineItem('quickcheck', 'active', '๐Ÿ” ูุญุต ุณุฑูŠุน ู„ู„ููŠุฏูŠูˆ', 'QUICK-CHECK',
822
+ 'ุฌุงุฑูŠ ูุญุต ุฃูˆู„ frame ู…ู† ุงู„ููŠุฏูŠูˆ ู„ู„ุชุญู‚ู‚ ุงู„ุณุฑูŠุน ู…ู† ูˆุฌูˆุฏ ู†ุณุงุก...');
823
+
824
+ try {
825
+ // ู†ุณุชุฎุฑุฌ ุฃูˆู„ frame ู…ู† ุงู„ููŠุฏูŠูˆ
826
+ const canvas = document.createElement('canvas');
827
+ const video = document.getElementById('videoPlayer');
828
+ video.currentTime = 1;
829
+
830
+ await new Promise(r => { video.onseeked = r; });
831
+
832
+ canvas.width = video.videoWidth;
833
+ canvas.height = video.videoHeight;
834
+ canvas.getContext('2d').drawImage(video, 0, 0);
835
+
836
+ canvas.toBlob(async (blob) => {
837
+ const formData = new FormData();
838
+ formData.append('file', blob, 'frame.jpg');
839
+
840
+ const res = await fetch(`${API_BASE}/analyze-frame`, { method: 'POST', body: formData });
841
+ const data = await res.json();
842
+
843
+ if (data.decision === 'BLOCK' || data.decision === 'block') {
844
+ updateTimelineItem('quickcheck', 'done',
845
+ `๐Ÿ”ด ุชู… ุงูƒุชุดุงู ุงู…ุฑุฃุฉ ููŠ ุงู„ู€ frame ุงู„ุฃูˆู„ โ€” ูŠูุญุชู…ู„ ูˆุฌูˆุฏ ู†ุณุงุก ููŠ ุงู„ููŠุฏูŠูˆ`);
846
+ addTimelineItem('qc-result', 'active', 'โš ๏ธ ุชุญุฐูŠุฑ: ูŠูˆุฌุฏ ู…ุญุชูˆู‰ ุฃู†ุซูˆูŠ', 'DETECTED',
847
+ 'ุงู„ูุญุต ุงู„ุณุฑูŠุน ุงูƒุชุดู ุงู…ุฑุฃุฉ. ูŠูู†ุตุญ ุจุงู„ู…ุชุงุจุนุฉ ุฅู„ู‰ "ุชุญู„ูŠู„ ูˆุชู‚ุทูŠุน" ู„ู„ู…ุนุงู„ุฌุฉ ุงู„ูƒุงู…ู„ุฉ.');
848
+ } else {
849
+ updateTimelineItem('quickcheck', 'done',
850
+ `๐ŸŸข ุงู„ู€ frame ุงู„ุฃูˆู„ ู†ุธูŠู โ€” ู„ุง ูŠูˆุฌุฏ ู†ุณุงุก ููŠ ุงู„ุจุฏุงูŠุฉ`);
851
+ addTimelineItem('qc-result', 'done', 'โœ… ุงู„ูุญุต ุงู„ุฃูˆู„ูŠ ู†ุธูŠู', 'CLEAN',
852
+ 'ู„ู… ูŠููƒุชุดู ู…ุญุชูˆู‰ ุฃู†ุซูˆูŠ ููŠ ุงู„ู€ frame ุงู„ุฃูˆู„. ูŠูู†ุตุญ ุจุงู„ู…ุชุงุจุนุฉ ู„ุชุญู„ูŠู„ ูƒุงู…ู„ ุงู„ููŠุฏูŠูˆ.');
853
+ }
854
+
855
+ setButtonsDisabled(false);
856
+ }, 'image/jpeg', 0.9);
857
+
858
+ } catch (e) {
859
+ updateTimelineItem('quickcheck', 'error', `โŒ ุฎุทุฃ: ${e.message}`);
860
+ setButtonsDisabled(false);
861
+ }
862
+ });
863
+
864
+ // โ”€โ”€โ”€ Full Analysis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
865
+ document.getElementById('btnAnalyze').addEventListener('click', () => {
866
+ if (!currentFile || !apiReady) return;
867
+ setButtonsDisabled(true);
868
+
869
+ const uploadProgress = document.getElementById('uploadProgress');
870
+ const progressFill = document.getElementById('progressFill');
871
+ const progressPct = document.getElementById('progressPct');
872
+ const progressLabel = document.getElementById('progressLabel');
873
+
874
+ // โ”€โ”€โ”€ ุงู„ู…ุฑุญู„ุฉ 1: ุฑูุน ุงู„ููŠุฏูŠูˆ ู…ุน progress โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
875
+ addTimelineItem('upload-video', 'active', '๐Ÿ“ค ุฑูุน ุงู„ููŠุฏูŠูˆ ู„ู„ุชุญู„ูŠู„', 'UPLOADING',
876
+ `ุฌุงุฑูŠ ุฑูุน ${currentFile.name} ุฅู„ู‰ ุงู„ุณูŠุฑูุฑ...`);
877
+
878
+ uploadProgress.style.display = 'block';
879
+ progressFill.style.width = '0%';
880
+ progressPct.textContent = '0%';
881
+
882
+ const formData = new FormData();
883
+ formData.append('file', currentFile);
884
+
885
+ const xhr = new XMLHttpRequest();
886
+ const startTime = Date.now();
887
+
888
+ // โ”€โ”€โ”€ progress ุฑูุน ุงู„ู…ู„ู โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
889
+ xhr.upload.onprogress = e => {
890
+ if (e.lengthComputable) {
891
+ const pct = Math.round(e.loaded / e.total * 100);
892
+ progressFill.style.width = pct + '%';
893
+ progressPct.textContent = pct + '%';
894
+ progressLabel.textContent = `ุฑูุน ุงู„ููŠุฏูŠูˆ: ${(e.loaded/1024/1024).toFixed(1)}MB / ${(e.total/1024/1024).toFixed(1)}MB`;
895
+ }
896
+ };
897
+
898
+ xhr.upload.onload = () => {
899
+ // โ”€โ”€โ”€ ุงูƒุชู…ู„ ุงู„ุฑูุน โ†’ ุจุฏุก ุงู„ุชุญู„ูŠู„ ุนู„ู‰ ุงู„ุณูŠุฑูุฑ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
900
+ uploadProgress.style.display = 'none';
901
+ updateTimelineItem('upload-video', 'done', `โœ… ุชู… ุฑูุน ุงู„ููŠุฏูŠูˆ โ€” ุฌุงุฑูŠ ุงู„ุชุญู„ูŠู„ ุนู„ู‰ ุงู„ุณูŠุฑูุฑ...`);
902
+ addTimelineItem('analyze', 'active', 'โš™๏ธ ุชุญู„ูŠู„ ุงู„ููŠุฏูŠูˆ frame ุจู€ frame', 'ANALYZING',
903
+ 'ุงู„ุณูŠุฑูุฑ ูŠุญู„ู„ ูƒู„ ุซุงู†ูŠุฉ ู…ู† ุงู„ููŠุฏูŠูˆ ุจุงุณุชุฎุฏุงู… BLIP + Florence-2...');
904
+ };
905
+
906
+ xhr.onload = () => {
907
+ uploadProgress.style.display = 'none';
908
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
909
+
910
+ if (xhr.status === 200) {
911
+ try {
912
+ const data = JSON.parse(xhr.responseText);
913
+ currentJobId = data.output_job_id || null;
914
+ updateTimelineItem('analyze', 'done', `โœ… ุงูƒุชู…ู„ ุงู„ุชุญู„ูŠู„ ููŠ ${elapsed}s`);
915
+ handleAnalysisResult(data, elapsed);
916
+ } catch(e) {
917
+ updateTimelineItem('analyze', 'error', `โŒ ุฎุทุฃ ููŠ ุชุญู„ูŠู„ ุงู„ุงุณุชุฌุงุจุฉ`);
918
+ }
919
+ } else {
920
+ try {
921
+ const err = JSON.parse(xhr.responseText);
922
+ updateTimelineItem('analyze', 'error', `โŒ ${err.detail || 'ุฎุทุฃ ููŠ ุงู„ุณูŠุฑูุฑ'}`);
923
+ } catch {
924
+ updateTimelineItem('analyze', 'error', `โŒ ุฎุทุฃ ${xhr.status}`);
925
+ }
926
+ }
927
+ setButtonsDisabled(false);
928
+ };
929
+
930
+ xhr.onerror = () => {
931
+ uploadProgress.style.display = 'none';
932
+ updateTimelineItem('upload-video', 'error', 'โŒ ุฎุทุฃ ููŠ ุงู„ุงุชุตุงู„ ุจุงู„ุณูŠุฑูุฑ');
933
+ setButtonsDisabled(false);
934
+ };
935
+
936
+ xhr.ontimeout = () => {
937
+ uploadProgress.style.display = 'none';
938
+ updateTimelineItem('analyze', 'error', 'โŒ ุงู†ุชู‡ุช ู…ู‡ู„ุฉ ุงู„ุงู†ุชุธุงุฑ');
939
+ setButtonsDisabled(false);
940
+ };
941
+
942
+ // timeout ุทูˆูŠู„ ู„ุฃู† ุงู„ุชุญู„ูŠู„ ูŠุณุชุบุฑู‚ ูˆู‚ุชุงู‹ ุนู„ู‰ CPU
943
+ xhr.timeout = 60 * 60 * 1000; // ุณุงุนุฉ ูƒุงู…ู„ุฉ
944
+ xhr.open('POST', `${API_BASE}/analyze-video`);
945
+ xhr.send(formData);
946
+ });
947
+
948
+ // โ”€โ”€โ”€ Handle Analysis Result โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
949
+ function handleAnalysisResult(data, elapsed) {
950
+ const resultsSection = document.getElementById('resultsSection');
951
+ resultsSection.style.display = 'block';
952
+
953
+ if (!data.has_female) {
954
+ // โ”€โ”€ ู†ุธูŠู โ”€โ”€
955
+ addTimelineItem('result', 'done', '๐ŸŸข ุงู„ููŠุฏูŠูˆ ู†ุธูŠู ุชู…ุงู…ุงู‹', 'CLEAN',
956
+ 'ู„ู… ูŠููƒุชุดู ุฃูŠ ู…ุญุชูˆู‰ ุฃู†ุซูˆูŠ ููŠ ุงู„ููŠุฏูŠูˆ ูƒุงู…ู„ุงู‹. ุงู„ููŠุฏูŠูˆ ุฌุงู‡ุฒ ู„ู„ู†ุดุฑ.');
957
+
958
+ resultsSection.innerHTML = `
959
+ <div class="result-card">
960
+ <div class="result-card-header">
961
+ <span>๐Ÿ“Š</span>
962
+ <h3>ู†ุชูŠุฌุฉ ุงู„ุชุญู„ูŠู„</h3>
963
+ </div>
964
+ <div class="result-card-body">
965
+ <div class="verdict clean">
966
+ <div class="verdict-icon">โœ…</div>
967
+ <div class="verdict-text">
968
+ <h4 style="color:var(--accent)">ุงู„ููŠุฏูŠูˆ ู†ุธูŠู</h4>
969
+ <p>ู„ุง ูŠุญุชูˆูŠ ุนู„ู‰ ุฃูŠ ู…ุญุชูˆู‰ ุฃู†ุซูˆูŠ</p>
970
+ </div>
971
+ </div>
972
+ <div class="stats-grid">
973
+ <div class="stat-box">
974
+ <span class="val">${data.analysis_log ? data.analysis_log.length : 'โ€”'}</span>
975
+ <span class="lbl">frames ุชู… ุชุญู„ูŠู„ู‡ุง</span>
976
+ </div>
977
+ <div class="stat-box">
978
+ <span class="val">${data.analysis_time || elapsed || 'โ€”'}s</span>
979
+ <span class="lbl">ูˆู‚ุช ุงู„ุชุญู„ูŠู„</span>
980
+ </div>
981
+ <div class="stat-box">
982
+ <span class="val" style="color:var(--accent)">0</span>
983
+ <span class="lbl">ู…ู‚ุงุทุน ู…ุญุฐูˆูุฉ</span>
984
+ </div>
985
+ </div>
986
+ ${renderFrameLog(data.analysis_log)}
987
+ </div>
988
+ </div>
989
+ `;
990
+ return;
991
+ }
992
+
993
+ // โ”€โ”€ ูŠุญุชูˆูŠ ุนู„ู‰ ู†ุณุงุก โ”€โ”€
994
+ const femaleSegs = data.female_segments || [];
995
+ const keptSegs = data.kept_segments || [];
996
+ const totalRemoved = data.total_removed_sec || 0;
997
+
998
+ if (femaleSegs.length > 0) {
999
+ addTimelineItem('cutting', 'done', `โœ‚๏ธ ุชู… ุชู‚ุทูŠุน ${femaleSegs.length} ู…ู‚ุทุน`, 'CUTTING',
1000
+ `ุชู… ุญุฐู ${totalRemoved.toFixed(1)} ุซุงู†ูŠุฉ ุชุญุชูˆูŠ ุนู„ู‰ ู†ุณุงุก ูˆุจู†ุงุก ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู.`);
1001
+ }
1002
+
1003
+ addTimelineItem('result', data.output_available ? 'done' : 'active',
1004
+ data.output_available ? '๐Ÿ“ฆ ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู ุฌุงู‡ุฒ' : 'โš ๏ธ ุงู„ููŠุฏูŠูˆ ูƒู„ู‡ ูŠุญุชูˆูŠ ู†ุณุงุก',
1005
+ 'RESULT', data.message);
1006
+
1007
+ // ุญุณุงุจ ุงู„ู…ุฏุฉ ุงู„ุชู‚ุฑูŠุจูŠุฉ
1008
+ const totalDur = data.duration_sec || 0;
1009
+
1010
+ resultsSection.innerHTML = `
1011
+ <div class="result-card">
1012
+ <div class="result-card-header">
1013
+ <span>๐Ÿ“Š</span>
1014
+ <h3>ู†ุชูŠุฌุฉ ุงู„ุชุญู„ูŠู„</h3>
1015
+ </div>
1016
+ <div class="result-card-body">
1017
+ <div class="verdict female">
1018
+ <div class="verdict-icon">โš ๏ธ</div>
1019
+ <div class="verdict-text">
1020
+ <h4 style="color:var(--accent2)">ุชู… ุงูƒุชุดุงู ู…ุญุชูˆู‰ ุฃู†ุซูˆูŠ</h4>
1021
+ <p>${femaleSegs.length} ู…ู‚ุทุน ูŠุญุชูˆูŠ ุนู„ู‰ ู†ุณุงุก โ€” ุชู… ุญุฐูู‡ุง</p>
1022
+ </div>
1023
+ </div>
1024
+
1025
+ <div class="stats-grid">
1026
+ <div class="stat-box">
1027
+ <span class="val" style="color:var(--accent2)">${femaleSegs.length}</span>
1028
+ <span class="lbl">ู…ู‚ุงุทุน ู…ุญุฐูˆูุฉ</span>
1029
+ </div>
1030
+ <div class="stat-box">
1031
+ <span class="val" style="color:var(--accent2)">${totalRemoved.toFixed(1)}s</span>
1032
+ <span class="lbl">ู…ุฏุฉ ู…ุญุฐูˆูุฉ</span>
1033
+ </div>
1034
+ <div class="stat-box">
1035
+ <span class="val">${data.analysis_log ? data.analysis_log.length : 'โ€”'}</span>
1036
+ <span class="lbl">frames ุชู… ุชุญู„ูŠู„ู‡ุง</span>
1037
+ </div>
1038
+ <div class="stat-box">
1039
+ <span class="val">${data.analysis_time || elapsed || 'โ€”'}s</span>
1040
+ <span class="lbl">ูˆู‚ุช ุงู„ุชุญู„ูŠู„</span>
1041
+ </div>
1042
+ </div>
1043
+
1044
+ ${renderVideoTimeline(femaleSegs, keptSegs, totalDur)}
1045
+ ${renderSegmentsTable(femaleSegs, 'female')}
1046
+ ${renderFrameLog(data.analysis_log)}
1047
+ </div>
1048
+ </div>
1049
+ `;
1050
+
1051
+ if (data.output_available && currentJobId) {
1052
+ const dl = document.getElementById('downloadSection');
1053
+ dl.style.display = 'block';
1054
+ document.getElementById('downloadDesc').textContent =
1055
+ `ุชู… ุญุฐู ${totalRemoved.toFixed(1)} ุซุงู†ูŠุฉ โ€” ุงู„ููŠุฏูŠูˆ ุงู„ู†ุธูŠู ุฌุงู‡ุฒ ู„ู„ุชุญู…ูŠู„`;
1056
+
1057
+ document.getElementById('btnDownload').onclick = () => {
1058
+ window.location.href = `${API_BASE}/download/${currentJobId}`;
1059
+ };
1060
+ }
1061
+ }
1062
+
1063
+ // โ”€โ”€โ”€ Render Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1064
+ function renderVideoTimeline(femaleSegs, keptSegs, totalDur) {
1065
+ if (!totalDur || totalDur === 0) return '';
1066
+ const allSegs = [
1067
+ ...femaleSegs.map(s => ({start: s[0], end: s[1], type: 'female'})),
1068
+ ...keptSegs.map(s => ({start: s[0], end: s[1], type: 'clean'}))
1069
+ ].sort((a,b) => a.start - b.start);
1070
+
1071
+ const bars = allSegs.map(s => {
1072
+ const left = (s.start / totalDur * 100).toFixed(1);
1073
+ const width = ((s.end - s.start) / totalDur * 100).toFixed(1);
1074
+ return `<div class="segment seg-${s.type}" style="left:${left}%;width:${width}%" title="${s.type}: ${s.start.toFixed(1)}s - ${s.end.toFixed(1)}s"></div>`;
1075
+ }).join('');
1076
+
1077
+ return `
1078
+ <div style="margin:16px 0">
1079
+ <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">
1080
+ ุฎุฑูŠุทุฉ ุงู„ููŠุฏูŠูˆ โ€” <span style="color:var(--accent)">โ–  ู†ุธูŠู</span> <span style="color:var(--accent2)">โ–  ู…ุญุฐูˆู</span>
1081
+ </div>
1082
+ <div class="video-timeline">${bars}</div>
1083
+ <div class="timeline-label"><span>0s</span><span>${totalDur.toFixed(0)}s</span></div>
1084
+ </div>
1085
+ `;
1086
+ }
1087
+
1088
+ function renderSegmentsTable(segs, type) {
1089
+ if (!segs || segs.length === 0) return '';
1090
+ const color = type === 'female' ? 'var(--accent2)' : 'var(--accent)';
1091
+ const rows = segs.map((s, i) => `
1092
+ <tr>
1093
+ <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:${color};padding:6px 8px">#${i+1}</td>
1094
+ <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;padding:6px 8px">${s[0].toFixed(1)}s</td>
1095
+ <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;padding:6px 8px">${s[1].toFixed(1)}s</td>
1096
+ <td style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);padding:6px 8px">${(s[1]-s[0]).toFixed(1)}s</td>
1097
+ </tr>
1098
+ `).join('');
1099
+
1100
+ return `
1101
+ <div style="margin-top:12px">
1102
+ <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">ุงู„ู…ู‚ุงุทุน ุงู„ู…ุญุฐูˆูุฉ</div>
1103
+ <table style="width:100%;border-collapse:collapse;background:rgba(0,0,0,0.3);border-radius:4px;overflow:hidden">
1104
+ <tr style="background:rgba(255,255,255,0.03)">
1105
+ <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">#</th>
1106
+ <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">ุงู„ุจุฏุงูŠุฉ</th>
1107
+ <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">ุงู„ู†ู‡ุงูŠุฉ</th>
1108
+ <th style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--muted);padding:6px 8px;text-align:right;font-weight:400">ุงู„ู…ุฏุฉ</th>
1109
+ </tr>
1110
+ ${rows}
1111
+ </table>
1112
+ </div>
1113
+ `;
1114
+ }
1115
+
1116
+ function renderFrameLog(log) {
1117
+ if (!log || log.length === 0) return '';
1118
+ const badges = log.map(f =>
1119
+ `<span class="frame-badge ${f.has_female ? 'female' : 'clean'}" title="${f.reason || ''}">${f.second}s</span>`
1120
+ ).join('');
1121
+ return `
1122
+ <div style="margin-top:12px">
1123
+ <div style="font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin-bottom:6px">
1124
+ ุณุฌู„ ุงู„ู€ frames โ€” <span style="color:var(--accent)">โ–  ู†ุธูŠู</span> <span style="color:var(--accent2)">โ–  ูŠุญุชูˆูŠ ู†ุณุงุก</span>
1125
+ </div>
1126
+ <div class="frame-log">${badges}</div>
1127
+ </div>
1128
+ `;
1129
+ }
1130
+
1131
+ // โ”€โ”€โ”€ Timeline Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1132
+ function showTimeline() {
1133
+ document.getElementById('timelineSection').style.display = 'block';
1134
+ }
1135
+
1136
+ function addTimelineItem(id, status, title, tag, desc) {
1137
+ const timeline = document.getElementById('timeline');
1138
+ const existing = document.getElementById(`tl-${id}`);
1139
+ if (existing) { updateTimelineItem(id, status, desc); return; }
1140
+
1141
+ const item = document.createElement('div');
1142
+ item.className = `timeline-item status-${status}`;
1143
+ item.id = `tl-${id}`;
1144
+ item.innerHTML = `
1145
+ <div class="timeline-dot"></div>
1146
+ <div class="timeline-content">
1147
+ <div class="timeline-title">
1148
+ <strong>${title}</strong>
1149
+ <span class="tag">${tag}</span>
1150
+ ${status === 'active' ? '<div class="spinner"></div>' : ''}
1151
+ </div>
1152
+ <div class="timeline-desc" id="tl-desc-${id}">${desc}</div>
1153
+ </div>
1154
+ `;
1155
+ timeline.appendChild(item);
1156
+ setTimeout(() => item.classList.add('visible'), 50);
1157
+ }
1158
+
1159
+ function updateTimelineItem(id, status, desc) {
1160
+ const item = document.getElementById(`tl-${id}`);
1161
+ if (!item) return;
1162
+ item.className = `timeline-item status-${status} visible`;
1163
+ const descEl = document.getElementById(`tl-desc-${id}`);
1164
+ if (descEl && desc) descEl.textContent = desc;
1165
+ // ุฅุฒุงู„ุฉ spinner
1166
+ const spinner = item.querySelector('.spinner');
1167
+ if (spinner) spinner.remove();
1168
+ }
1169
+
1170
+ // โ”€โ”€โ”€ Utils โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1171
+ function formatDuration(sec) {
1172
+ const m = Math.floor(sec / 60);
1173
+ const s = Math.floor(sec % 60);
1174
+ return `${m}:${s.toString().padStart(2,'0')}`;
1175
+ }
1176
+
1177
+ function showAlert(msg, type) {
1178
+ const box = document.getElementById('alertBox');
1179
+ box.textContent = msg;
1180
+ box.className = `alert alert-${type} show`;
1181
+ setTimeout(() => box.classList.remove('show'), 5000);
1182
+ }
1183
+
1184
+ function setButtonsDisabled(disabled) {
1185
+ document.getElementById('btnQuickCheck').disabled = disabled || !apiReady;
1186
+ document.getElementById('btnAnalyze').disabled = disabled || !apiReady;
1187
+ }
1188
+
1189
+ function resetAll() {
1190
+ currentFile = null;
1191
+ currentJobId = null;
1192
+ document.getElementById('uploadZone').style.display = 'block';
1193
+ document.getElementById('videoPreview').style.display = 'none';
1194
+ document.getElementById('actionsBar').style.display = 'none';
1195
+ document.getElementById('timelineSection').style.display = 'none';
1196
+ document.getElementById('resultsSection').style.display = 'none';
1197
+ document.getElementById('downloadSection').style.display = 'none';
1198
+ document.getElementById('timeline').innerHTML = '';
1199
+ document.getElementById('resultsSection').innerHTML = '';
1200
+ document.getElementById('videoPlayer').src = '';
1201
+ fileInput.value = '';
1202
+ }
1203
+
1204
+ // โ”€โ”€โ”€ Init โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1205
+ checkApiStatus();
1206
+ </script>
1207
+ </body>
1208
  </html>