File size: 22,529 Bytes
a378ff0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63d1eff
a378ff0
 
 
 
 
 
 
 
 
 
63d1eff
a378ff0
 
 
 
 
63d1eff
a378ff0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1d0d08
a378ff0
 
 
 
e1d0d08
a378ff0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1d0d08
a378ff0
e1d0d08
 
 
a378ff0
 
 
e1d0d08
a378ff0
e1d0d08
 
a378ff0
e1d0d08
 
a378ff0
 
e1d0d08
a378ff0
e1d0d08
a378ff0
 
 
 
 
 
 
 
e1d0d08
a378ff0
 
 
 
 
 
 
e1d0d08
 
 
 
a378ff0
e1d0d08
 
a378ff0
 
 
e1d0d08
 
a378ff0
 
e1d0d08
a378ff0
 
 
e1d0d08
a378ff0
 
 
 
e1d0d08
a378ff0
e1d0d08
a378ff0
 
e1d0d08
 
 
 
a378ff0
e1d0d08
a378ff0
e1d0d08
 
 
a378ff0
 
e1d0d08
a378ff0
 
e1d0d08
 
 
a378ff0
 
 
e1d0d08
 
 
 
 
 
a378ff0
 
 
 
 
 
e1d0d08
 
a378ff0
e1d0d08
 
 
a378ff0
 
 
 
 
e1d0d08
 
 
a378ff0
 
 
e1d0d08
a378ff0
 
 
 
 
 
 
e1d0d08
a378ff0
 
 
 
 
 
 
 
 
 
 
 
e1d0d08
a378ff0
 
 
e1d0d08
a378ff0
e1d0d08
 
a378ff0
e1d0d08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a378ff0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8680a07
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7088ddc
 
8680a07
 
7088ddc
 
8680a07
 
7088ddc
8680a07
 
a378ff0
 
 
77ba701
a378ff0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7088ddc
8680a07
 
 
 
 
 
 
 
 
 
 
7088ddc
8680a07
7088ddc
8680a07
 
 
7088ddc
8680a07
7088ddc
8680a07
 
7088ddc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a378ff0
 
 
63d1eff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
import gradio as gr
import cv2
import numpy as np
from PIL import Image
import qrcode
from googleapiclient.discovery import build
import os
import yaml
from ultralytics import YOLO
import logging
import glob
import re
import time  # 確保有導入 time 模組
import subprocess  # 用於執行外部命令
import threading  # 用於處理多線程
import queue  # 用於線程間通信
import urllib.request  # 用於檢查 URL 是否可訪問
import tempfile  # 用於創建臨時文件

markdown_content = """
<style>
.title-icon {
  display: inline-flex; /* 讓圖片和文字在同一行 */
  align-items: center; /* 垂直居中 */
  font-size: 2em; /* 調整整體大小 */
}
.title-icon img {
  height: 1em;  /* 圖片高度,與文字大小相關 */
  margin-right: 0.5em; /* 圖片與文字間距 */
}
</style>

<div class="title-icon">
  <h1>台灣常見鳥類偵測系統</h1>
</div>
"""

# 設定日誌
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)

# 載入類別名稱 (這部分保持不變)
try:
    logging.info("載入類別名稱...")
    yaml_path = "multibird_dataset.yaml"  # 改成你的路徑
    with open(yaml_path, "r", encoding="utf-8") as f:
        yaml_data = yaml.safe_load(f)
    class_names = yaml_data["names"]
    total_class_names = list(yaml_data["names"].values())
    logging.info(f"成功載入 {len(set(total_class_names))} 個類別")
except Exception as e:
    logging.error(f"載入類別名稱時發生錯誤: {str(e)}")
    class_names = {}

# 設定 YouTube API (這部分保持不變)
YOUTUBE_API_KEY = os.environ.get("YOUTUBE_API_KEY")
youtube = build("youtube", "v3", developerKey=YOUTUBE_API_KEY)

# 載入 YOLO 模型 (這部分保持不變)
try:
    logging.info("開始載入 YOLO 模型...")
    model_path = "best.pt"  # 0323 187種鳥的版本
    if not os.path.exists(model_path):
        raise FileNotFoundError(f"找不到模型文件:{model_path}")
    model = YOLO(model_path)
    logging.info("YOLO 模型載入成功")
except Exception as e:
    logging.error(f"載入模型時發生錯誤:{str(e)}")
    raise

# 建立英文鳥名到中文鳥名的對照表
bird_chinese_names = {
    "domestic_goose": "家鵝",
    "black_swan": "黑天鵝",
    "spot_billed_duck": "花嘴鴨",
    "mallard": "綠頭鴨",
    "rock_dove": "野鴿",
    "spotted_dove": "珠頸斑鳩",
    "emerald_dove": "翠翼鳩",
    "common_moorhen": "紅冠水雞",
    "little_grebe": "小鸊鷉",
    "black_crowned_night_heron": "夜鷺",
    "malayan_night_heron": "黑冠麻鷺",
    "cattle_egret": "黃頭鷺",
    "mountain_hawk_eagle": "熊鷹",
    "common_kingfisher": "翠鳥",
    "muller_barbet": "五色鳥",
    "black_drongo": "大卷尾",
    "blue_flycatcher": "黑枕藍鶲",
    "formosan_blue_magpie": "台灣藍鵲",
    "chinese_bulbul": "白頭翁",
    "streak_breasted_scimitar_babbler": "小彎嘴",
    "black_necklaced_scimitar_babbler": "大彎嘴",
    "javan_myna": "家八哥",
    "white_vented_myna": "白腹八哥",
    "oriental_magpie_robin": "鵲鴝",
    "white_rumped_shama": "白腰鵲鴝",
    "daurian_redstart": "黃尾鴝",
    "eurasian_tree_sparrow": "麻雀",
    "white_wagtail": "白鶺鴒",
    "black_naped_oriole": "黃鸝",
    "maroon_oriole": "朱鸝",
    "black_faced_spoonbill": "黑面琵鷺",
    "great_egret": "大白鷺",
    "intermediate_egret": "中白鷺",
    "little_egret": "小白鷺",
    "chinese_egret": "唐白鷺",
    "grey_heron": "蒼鷺",
    "eurasian_spoonbill": "白琵鷺",
    "osprey": "魚鷹",
    "black_winged_kite": "黑翅鳶",
    "black_kite": "黑鳶",
    "mountain_eagle": "林鵰",
    "eastern_imperial_eagle": "花鵰",
    "oriental_honey_buzzard": "東方蜂鷹",
    "crested_serpent_eagle": "大冠鷲",
    "grey_faced_buzzard": "灰面鵟鷹",
    "crested_goshawk": "鳳頭蒼鷹",
    "chinese_sparrowhawk": "赤腹鷹",
    "besra": "松雀鷹",
    "white_tailed_eagle": "白尾海鵰",
    "peregrine_falcon": "遊隼",
    "ancient_murrelet": "扁嘴海雀",
    "tundra_swan": "小天鵝",
    "whooper_swan": "大天鵝",
    "great_crested_grebe": "鳳頭鸊鷉",
    "eared_grebe": "黑頸鸊鷉",
    "cotton_pygmy_goose": "棉鴨",
    "ferruginous_duck": "白眼潛鴨",
    "great_cormorant": "鸕鶿",
    "japanese_cormorant": "暗綠背鸕鶿",
    "brown_noddy": "褐翅燕鷗",
    "brown_headed_gull": "棕頭鷗",
    "black_headed_gull": "紅嘴鷗",
    "saunders_gull": "黑嘴鷗",
    "taiwan_hwamei": "台灣畫眉",
    "green_winged_teal": "小水鴨",
    "baikal_teal": "花臉鴨",
    "garganey": "白眉鴨",
    "common_shelduck": "翹鼻麻鴨",
    "common_pochard": "紅頭潛鴨",
    "anas_falcata": "羅紋鴨",
    "gadwall": "赤膀鴨",
    "mandarin_duck": "鴛鴦",
    "greater_scaup": "斑背潛鴨",
    "tufted_duck": "鳳頭潛鴨",
    "northern_pintail": "尖尾鴨",
    "eurasian_wigeon": "赤頸鴨",
    "northern_shoveller": "琵嘴鴨",
    "eastern_spot_billed_duck": "東方斑嘴鴨",
    "american_wigeon": "美洲赤頸鴨",
    "eurasian_coot": "白骨頂",
    "black_tailed_gull": "黑尾鷗",
    "caspian_gull": "裡海鷗",
    "caspian_tern": "紅嘴巨鷗",
    "relict_gull": "遺鷗",
    "graylag_goose": "灰雁",
    "swan_goose": "鴻雁",
    "tundra_bean_goose": "短嘴豆雁",
    "lesser_white_fronted_goose": "小白額雁",
    "taiga_bean_goose": "豆雁",
    "greater_white_fronted_goose": "大白額雁",
    "red_footed_booby": "紅腳鰹鳥",
    "franklin_gull": "富蘭克林鷗",
    "smew": "白秋沙",
    "brown_headed_thrush": "赤腹鶇",
    "grey_backed_thrush": "灰背鶇",
    "island_thrush": "島鶇",
    "plumbeous_redstart": "鉛色水鶇",
    "red_breasted_flycatcher": "紅胸姬鶲",
    "fairy_pitta": "八色鳥",
    "blue_winged_pitta": "藍翅八色鳥",
    "collared_kingfisher": "蒼翡翠",
    "ruddy_kingfisher": "赤翡翠",
    "dollar_bird": "三寶鳥",
    "eurasian_wryneck": "蟻鴷",
    "grey_capped_woodpecker": "小啄木",
    "japanese_waxwing": "日本太平鳥",
    "mikado_pheasant": "帝雉",
    "swinhoe_pheasant": "藍腹鷴",
    "ring_necked_pheasant": "環頸雉",
    "taiwan_bamboo_partridge": "台灣竹雞",
    "chestnut_backed_shrike": "栗背伯勞",
    "steppe_grey_strike": "草原灰伯勞",
    "brown_shrike": "紅尾伯勞",
    "long_tailed_shrike": "棕背伯勞",
    "taiwan_scimitar_babbler": "台灣彎嘴畫眉",
    "rufous_faced_warbler": "棕面鶯",
    "taiwan_yuhina": "冠羽畫眉",
    "green_backed_tit": "青背山雀",
    "yellow_tit": "黃山雀",
    "coal_tit": "煤山雀",
    "grey_chinned_minivet": "灰喉山椒鳥",
    "black_winged_cuckooshrike": "黑翅鵑鵙",
    "large_cuckoo_shrike": "大鵑鵙",
    "large_billed_crow": "巨嘴鴉",
    "olive_backed_pipit": "樹鷚",
    "alpine_accentor": "岩鷚",
    "eurasian_nuthatch": "茶腹鳾",
    "asian_koel": "噪鵑",
    "chestnut_bunting": "栗鵐",
    "little_bunting": "小鵐",
    "black_faced_bunting": "黑臉鵐",
    "yellow_throated_bunting": "黃喉鵐",
    "oriental_reed_warbler": "東方大葦鶯",
    "narcissus_flycatcher": "黃眉姬鶲",
    "mugimaki_flycatcher": "鴝姬鶲",
    "verditer_flycatcher": "銅藍鶲",
    "siberian_rubythroat": "紅喉歌鴝",
    "forest_wagtail": "山鶺鴒",
    "vinous_breasted_starling": "亞洲輝椋鳥",
    "plain_flowerpecker": "純色啄花鳥",
    "fire_breasted_flowerpecker": "紅胸啄花鳥",
    "taiwan_barbet": "台灣擬啄木",
    "collared_scops_owl": "領角鴞",
    "brown_hawk_owl": "褐鷹鴞",
    "brown_wood_owl": "褐林鴞",
    "collared_owlet": "鵂鶹",
    "austrialasian_grass_owl": "草鴞",
    "tawny_fish_owl": "黃魚鴞",
    "short_eared_owl": "短耳鴞",
    "hooded_crane": "白頭鶴",
    "siberian_crane": "白鶴",
    "sandhill_crane": "沙丘鶴",
    "common_crane": "灰鶴",
    "chinese_pond_heron": "池鷺",
    "pacific_reef_heron": "岩鷺",
    "purple_heron": "紫鷺",
    "yellow_bittern": "黃葦鳽",
    "great_bittern": "大麻鳽",
    "glossy_ibis": "彩鷺",
    "common_kestrel": "紅隼",
    "chinese_crested_tern": "中華鳳頭燕鷗",
    "white_eared_sibia": "白耳畫眉",
    "asian_dowitcher": "半蹼鷸",
    "bar_tailed_godwit": "斑尾塍鷸",
    "common": "田鷸",
    "far_eastern_curlew": "大杓鷸",
    "ruddy_turnstone": "翻石鷸",
    "little_stint": "小濱鷸",
    "curlew_sandpiper": "彎嘴濱鷸",
    "dunlin": "黑腹濱鷸",
    "spotted_redshank": "鶴鷸",
    "greater_painted_snipe": "彩鷸",
    "nordmann_greenshank": "諾氏鷸",
    "wood_sandpiper": "林鷸",
    "terek_sandpiper": "翹嘴鷸",
    "sanderling": "三趾濱鷸",
    "eurasian_curlew": "白腰杓鷸",
    "long_toed_stint": "長趾濱鷸",
    "common_greenshank": "青足鷸",
    "whimbrel": "中杓鷸",
    "marsh_sandpiper": "澤鷸",
    "red_necked_phalarope": "紅頸瓣足鷸",
    "common_redshank": "紅腳鷸",
    "little_ringed_plover": "小環頸鴴",
    "kentish_plover": "環頸鴴",
    "lesser_sand_plover": "蒙古鴴",
    "black_winged_stilt": "高蹺鴴",
    "oriental_pratincole": "燕鴴",
    "pacific_golden_plover": "太平洋金斑鴴",
    "pied_avocet": "反嘴鷸",
    "little_curlew": "小杓鷸",
    "common_ringed_plover": "劍鴴",
    "northern_lapwing": "田鳧",
    "grey_headed_lapwing": "灰頭麥雞",
    "savanna_nightjar": "南亞夜鷹",
    "little_tern": "小燕鷗",
    "glareola_maldivarum": "燕鴴",
    "barn_swallow": "家燕",
    "greater_crested_tern": "鳳頭燕鷗",
    "common_tern": "普通燕鷗",
    "whiskered_tern": "黑腹燕鷗",
    "gull_billed_tern": "鷗嘴燕鷗",
    "black_naped_tern": "黑枕燕鷗",
    "white_backed_woodpecker": "大赤啄木",
    "oriental_stork": "東方白鸛",
    "red_collared_dove": "紅鳩",
    "black_chinned_fruit_dove": "小綠鳩",
    "zebra_dove": "斑馬鳩",
    "common_goldeneye": "鵲鴨",
    "red_breasted_merganser": "紅胸秋沙",
    "scaly_sided_merganser": "中華秋沙",
    "great_white_pelican": "白鵜鶘",
    "bar_headed_goose": "斑頭雁",
    "adelie_penguin": "阿德利企鵝",
    "gentoo_penguin": "巴布亞企鵝",
    "chinstrap_penguin": "頰帶企鵝",
    "macaroni_penguin": "長冠企鵝",
    "king_penguin": "國王企鵝",
    "tufted_puffin": "簇絨海鸚",
    "hoopoe": "戴勝",
    "anna_hummingbird": "安氏蜂鳥",
}

# 將yaml中的類別名稱加入bird_chinese_names
for key, value in class_names.items():
    if key not in bird_chinese_names:
        bird_chinese_names[key] = value


def search_youtube_video(query):
    """搜尋 YouTube 影片"""
    try:
        # 將英文鳥名轉換為中文鳥名
        if query in bird_chinese_names:
            chinese_query = bird_chinese_names[query]
            logging.info(f"將英文鳥名 '{query}' 轉換為中文鳥名 '{chinese_query}'")
            query = chinese_query
        else:
            logging.warning(f"未找到 '{query}' 的中文對照名稱,使用原始名稱進行搜尋")

        logging.info(f"搜尋 YouTube 影片,關鍵字:{query}")
        search_response = (
            youtube.search()
            .list(
                q=query,
                part="id,snippet",
                maxResults=1,
                type="video",
                channelId="UClCaVtB-pYpNOJGiGzQAhNA",  # 你的頻道 ID
            )
            .execute()
        )

        if not search_response["items"]:
            logging.warning("未找到相關影片")
            return None, None, None

        video_id = search_response["items"][0]["id"]["videoId"]
        video_url = f"https://www.youtube.com/watch?v={video_id}"
        logging.info(f"找到影片:{video_url}")

        embed_html = f'<iframe width="100%" height="315" src="https://www.youtube.com/embed/{video_id}" frameborder="0" allowfullscreen></iframe>'

        # 生成 QR Code
        qr = qrcode.QRCode(version=1, box_size=10, border=5)
        qr.add_data(video_url)
        qr.make(fit=True)
        qr_img = qr.make_image(fill_color="black", back_color="white")
        qr_array = np.array(qr_img.convert("RGB"))  # 轉換為RGB模式以確保兼容性

        return video_url, embed_html, qr_array
    except Exception as e:
        logging.error(f"YouTube 搜尋錯誤: {str(e)}")
        return None, None, None


def is_valid_youtube_url(url):
    """檢查是否為有效的 YouTube URL"""
    youtube_regex = (
        r"(https?://)?(www\.)?"
        r"(youtube|youtu|youtube-nocookie)\.(com|be)/"
        r"(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})"
    )
    match = re.match(youtube_regex, url)
    return match is not None


def get_youtube_video_id(url):
    """從 YouTube URL 中提取視頻 ID"""
    youtube_regex = (
        r"(https?://)?(www\.)?"
        r"(youtube|youtu|youtube-nocookie)\.(com|be)/"
        r"(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})"
    )
    match = re.match(youtube_regex, url)
    if match:
        return match.group(6)
    return None


def process_image(input_image):
    """處理圖片並進行物件偵測 (這部分保持不變)"""
    try:
        if input_image is None:
            logging.warning("未提供輸入圖片")
            return None, "請先上傳圖片", "", None

        logging.info("開始處理圖片...")
        # 建立輸出目錄
        output_dir = "results"
        os.makedirs(output_dir, exist_ok=True)

        # 執行物件偵測
        results = model.predict(input_image, save=False)
        result = results[0]

        # 取得偵測結果
        detected_objects = []
        for box in result.boxes:
            cls = int(box.cls[0])
            conf = float(box.conf[0])
            if conf > 0.8:  # 信心度閾值
                label = class_names.get(cls, f"類別 {cls}")
                detected_objects.append(label)
                logging.info(f"檢測到物件:{label},信心度:{conf:.2f}")

        # 取得處理後的圖片
        result_image = result.plot()
        result_pil = Image.fromarray(result_image)

        # 搜尋布萊克獨木舟生態休閒中心 YouTube 影片
        if detected_objects:
            video_url, embed_html, qr_image = search_youtube_video(detected_objects[0])
            result_text = f"檢測到: {', '.join(detected_objects)}"
        else:
            video_url = "偵測該物件的信心度不足,未查詢到相關影片,偵測結果僅供參考"
            embed_html = ""
            qr_image = None
            result_text = "偵測該物件的信心度不足,未查詢到相關影片,偵測結果僅供參考"

        return result_pil, video_url, embed_html, qr_image
    except Exception as e:
        logging.error(f"處理錯誤: {str(e)}")
        return None, f"處理錯誤: {str(e)}", "", None


def process_video(input_video):
    """處理影片並進行物件偵測"""
    try:
        if input_video is None:
            logging.warning("未提供輸入影片")
            return None, "未提供輸入影片", "", None  # 修正返回的訊息數量

        logging.info("開始處理影片...")
        output_dir = "results"
        os.makedirs(output_dir, exist_ok=True)

        timestamp = int(time.time())
        run_name = f"predict_{timestamp}"
        logging.info(f"使用唯一識別名稱進行偵測: {run_name}")

        # 執行物件偵測,並直接處理結果
        results = model(
            input_video, save=True, project=output_dir, name=run_name, stream=True
        )

        # 儲存處理過的frame
        processed_frames = []

        # 取得偵測結果和處理frame
        detected_objects = set()
        for result in results:
            for box in result.boxes:
                cls = int(box.cls[0])
                conf = float(box.conf[0])
                if conf > 0.8:
                    label = class_names.get(cls, f"類別 {cls}")
                    detected_objects.add(label)
                    logging.info(f"檢測到物件:{label},信心度:{conf:.2f}")
            # 將result.plot()存入processed_frames
            processed_frames.append(result.plot())

        # 影片格式轉換和輸出
        predict_folder = os.path.join(output_dir, run_name)
        output_video_name = os.path.join(predict_folder, f"output_{timestamp}.mp4")

        if not os.path.exists(predict_folder):
            os.makedirs(predict_folder)

        # 取得影片的基本資訊 (從原始影片取得)
        cap = cv2.VideoCapture(input_video)  # 改為讀取原始影片
        fps = int(cap.get(cv2.CAP_PROP_FPS))
        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        cap.release()  # 讀取完後就釋放

        # 建立 VideoWriter 物件 (使用 mp4v codec)
        fourcc = cv2.VideoWriter_fourcc(*"avc1")
        out = cv2.VideoWriter(output_video_name, fourcc, fps, (width, height))

        # 將處理過的 frame 寫入影片
        for frame in processed_frames:
            out.write(frame)
        out.release()

        # 搜尋 YouTube 影片 (其餘部分保持不變)
        if detected_objects:
            video_url, embed_html, qr_image = search_youtube_video(
                list(detected_objects)[0]
            )
            result_text = f"檢測到: {', '.join(detected_objects)}"
        else:
            video_url = "偵測該物件的信心度不足,未查詢到相關影片,偵測結果僅供參考"
            embed_html = ""
            qr_image = None
            result_text = "偵測該物件的信心度不足,未查詢到相關影片,偵測結果僅供參考"

        return output_video_name, video_url, embed_html, qr_image  # 直接回傳名稱

    except Exception as e:
        logging.error(f"影片處理錯誤: {str(e)}")
        return None, f"影片處理錯誤: {str(e)}", "", None


def search_channel_videos(query):
    """搜尋布萊克獨木舟生態休閒中心頻道的影片"""
    try:
        logging.info(f"搜尋頻道影片,關鍵字:{query}")
        search_response = (
            youtube.search()
            .list(
                q=query,
                part="id,snippet",
                maxResults=6,
                type="video",
                channelId="UClCaVtB-pYpNOJGiGzQAhNA",  # 布萊克獨木舟生態休閒中心頻道 ID
                order="relevance"
            )
            .execute()
        )

        results = []
        for item in search_response.get("items", []):
            video_id = item["id"]["videoId"]
            title = item["snippet"]["title"]
            description = item["snippet"]["description"]
            published_at = item["snippet"]["publishedAt"]
            embed_html = f'<div style="margin-bottom: 20px;"><h3>{title}</h3><p>發布時間:{published_at}</p><iframe width="100%" height="315" src="https://www.youtube.com/embed/{video_id}" frameborder="0" allowfullscreen></iframe><p>{description}</p></div>'
            results.append(embed_html)

        if not results:
            return "<p>查無相關影片</p>"
        return "<div style='max-width: 800px; margin: 0 auto;'>" + "".join(results) + "</div>"
    except Exception as e:
        logging.error(f"頻道影片搜尋錯誤: {str(e)}")
        return f"<p>搜尋錯誤: {str(e)}</p>"


# 創建 Gradio 介面
with gr.Blocks(title="鳥類偵測系統") as demo:
    gr.Markdown(markdown_content)
    gr.Markdown("### 支援圖片、影片、即時攝影機")

    with gr.Tab("圖片偵測"):
        with gr.Row():
            with gr.Column(scale=1):
                image_input = gr.Image(label="上傳圖片", type="numpy")
                image_button = gr.Button("開始偵測")
            with gr.Column(scale=1):
                image_output = gr.Image(label="偵測結果")
        with gr.Row():
            with gr.Column(scale=1):
                image_video_url = gr.Textbox(
                    label="YouTube 影片連結", visible=True
                )  # 保留但隱藏
                image_video_embed = gr.HTML(label="YouTube 影片")
            with gr.Column(scale=1):
                image_qr_output = gr.Image(label="QR Code")

        image_button.click(
            process_image,
            inputs=[image_input],
            outputs=[image_output, image_video_url, image_video_embed, image_qr_output],
        )

        # 新增頻道影片搜尋頁籤
    with gr.Tab("頻道影片搜尋"):
        gr.Markdown("### 布萊克獨木舟生態休閒中心頻道影片搜尋")
        with gr.Row():
            search_input = gr.Textbox(
                label="請輸入關鍵字",
                placeholder="輸入要搜尋的關鍵字...",
                scale=4
            )
            search_button = gr.Button("搜尋", scale=1)

        with gr.Row():
            results_html = gr.HTML(
                label="搜尋結果",
                value="<p>請輸入關鍵字進行搜尋</p>"
            )

        search_button.click(
            fn=search_channel_videos,
            inputs=[search_input],
            outputs=[results_html]
        )

    # 影片偵測頁籤
    # with gr.Tab("影片偵測"):
    #     with gr.Row():
    #         with gr.Column(scale=1):
    #             video_input = gr.Video(label="上傳影片")
    #             video_button = gr.Button("開始偵測")
    #         with gr.Column(scale=1):
    #             video_output = gr.Video(
    #                 label="偵測結果",
    #                 # streaming=True,  # 不再需要 streaming
    #                 # autoplay=True   # autoplay 根據你的需求決定是否保留
    #             )
    #     with gr.Row():
    #         with gr.Column(scale=1):
    #             video_video_url = gr.Textbox(
    #                 label="YouTube 影片連結", visible=True
    #             )
    #             video_video_embed = gr.HTML(label="YouTube 影片")
    #         with gr.Column(scale=1):
    #             video_qr_output = gr.Image(label="QR Code")

    #     video_button.click(
    #         process_video,
    #         inputs=[video_input],
    #         outputs=[video_output, video_video_url, video_video_embed, video_qr_output],
    #     )

# 啟動應用程式
if __name__ == "__main__":
    logging.info("啟動網頁應用程式...")
    demo.launch(share=True, server_name="0.0.0.0", pwa=True)