Spaces:
Running
Running
Commit ·
a22790d
1
Parent(s): 50e1d54
Reorganize 2026_AI camp: Traditional names, Gemini key prompt, deps
Browse files- Add main.py entries and fix student scripts for Arcade Hub
- Traditional Chinese project folder names; Gemini terminal API key UI
- requirements: pandas, matplotlib, bs4, lxml, google-genai SDKs
This view is limited to 50 files because it contains too many changes. See raw diff
- projects/2026_AI營1/劉宗穎/{DataFrame指令练习 → DataFrame指令練習}/d1_1_pandas_intro.py +37 -36
- projects/2026_AI營1/劉宗穎/DataFrame指令練習/main.py +18 -0
- projects/2026_AI營1/劉宗穎/{DataFrame指令练习 → DataFrame指令練習}/taipei_weather_2026.csv +0 -0
- projects/2026_AI營1/劉宗穎/{ai_api串接练习 → ai_api串接練習}/d2_1_gemini.py +62 -40
- projects/2026_AI營1/劉宗穎/ai_api串接練習/main.py +18 -0
- projects/2026_AI營1/劉宗穎/{mock_密室逃脱游戏 → mock_密室逃脫遊戲}/d2_3_chatbot_memory_game_template.py +0 -0
- projects/2026_AI營1/劉宗穎/mock_密室逃脫遊戲/main.py +18 -0
- projects/2026_AI營1/劉宗穎/{mock_关键词抓取 → mock_關鍵詞抓取}/d2_3_chatbot_memory_mock.py +0 -0
- projects/2026_AI營1/劉宗穎/mock_關鍵詞抓取/main.py +18 -0
- projects/2026_AI營1/劉宗穎/{mock&AI_api双模式 → mock與AI_api雙模式}/d2_3_chatbot_memory.py +124 -95
- projects/2026_AI營1/劉宗穎/{mock&AI_api双模式 → mock與AI_api雙模式}/d2_4_chatbot_memory.py +158 -129
- projects/2026_AI營1/劉宗穎/mock與AI_api雙模式/main.py +18 -0
- projects/2026_AI營1/劉宗穎/{三指捏游戏 → 三指捏遊戲}/d5_pinch_challenge_solution.py +216 -217
- projects/2026_AI營1/劉宗穎/三指捏遊戲/main.py +18 -0
- projects/2026_AI營1/劉宗穎/{人脸辨识 → 人臉辨識}/d3_2_face_detection.py +0 -0
- projects/2026_AI營1/劉宗穎/{人脸辨识 → 人臉辨識}/d3_2_face_detection2.py +0 -0
- projects/2026_AI營1/劉宗穎/人臉辨識/main.py +18 -0
- projects/2026_AI營1/劉宗穎/人臉辨識/onepiece(2).jpeg +3 -0
- projects/2026_AI營1/劉宗穎/{咖啡店分析图表 → 咖啡店分析圖表}/coffee_sales_analysis.png +0 -0
- projects/2026_AI營1/劉宗穎/{咖啡店分析图表 → 咖啡店分析圖表}/coffee_sales_data.csv +0 -0
- projects/2026_AI營1/劉宗穎/{咖啡店分析图表 → 咖啡店分析圖表}/d1_4_visualization_coffee.py +31 -1
- projects/2026_AI營1/劉宗穎/咖啡店分析圖表/main.py +18 -0
- projects/2026_AI營1/劉宗穎/嘴巴偵測/main.py +18 -0
- projects/2026_AI營1/劉宗穎/嘴巴偵測/onepiece(2).jpeg +3 -0
- projects/2026_AI營1/劉宗穎/{嘴巴侦测 → 嘴巴偵測}/step3_mouth_ratio.py +113 -113
- projects/2026_AI營1/劉宗穎/{天气资料图表 → 天氣資料圖表}/d1_3_visualization.py +117 -87
- projects/2026_AI營1/劉宗穎/天氣資料圖表/main.py +18 -0
- projects/2026_AI營1/劉宗穎/天氣資料圖表/onepiece(2).jpeg +3 -0
- projects/2026_AI營1/劉宗穎/{天气资料图表 → 天氣資料圖表}/taipei_weather_2026.csv +0 -0
- projects/2026_AI營1/劉宗穎/{影像处理 → 影像處理}/d3_1_opencv_basic.py +110 -70
- projects/2026_AI營1/劉宗穎/影像處理/main.py +18 -0
- projects/2026_AI營1/劉宗穎/影像處理/onepiece(2).jpeg +3 -0
- projects/2026_AI營1/劉宗穎/{手指画画游戏 → 手指畫畫遊戲}/draw_game_student.py +2 -1
- projects/2026_AI營1/劉宗穎/手指畫畫遊戲/main.py +18 -0
- projects/2026_AI營1/劉宗穎/手指畫畫遊戲/onepiece(2).jpeg +3 -0
- projects/2026_AI營1/劉宗穎/{爬虫分析 → 爬蟲分析}/d4_1_ex1_books.py +0 -0
- projects/2026_AI營1/劉宗穎/{爬虫分析 → 爬蟲分析}/d4_1_ex2_ptt.py +0 -0
- projects/2026_AI營1/劉宗穎/{爬虫分析 → 爬蟲分析}/d4_1_ex3_ithelp.py +0 -0
- projects/2026_AI營1/劉宗穎/爬蟲分析/main.py +26 -0
- projects/2026_AI營1/劉宗穎/爬蟲分析_PTT/d4_1_ex2_ptt.py +81 -0
- projects/2026_AI營1/劉宗穎/爬蟲分析_PTT/main.py +18 -0
- projects/2026_AI營1/劉宗穎/爬蟲分析_books/d4_1_ex1_books.py +94 -0
- projects/2026_AI營1/劉宗穎/爬蟲分析_books/main.py +18 -0
- projects/2026_AI營1/劉宗穎/爬蟲分析_ithelp/d4_1_ex3_ithelp.py +98 -0
- projects/2026_AI營1/劉宗穎/爬蟲分析_ithelp/main.py +18 -0
- projects/2026_AI營1/劉宗穎/{爬虫分析与AI报告 → 爬蟲分析與AI報告}/d4_1_web_scraping.py +0 -0
- projects/2026_AI營1/劉宗穎/{爬虫分析与AI报告 → 爬蟲分析與AI報告}/d4_2_agent_task_chain.py +33 -2
- projects/2026_AI營1/劉宗穎/{爬虫分析与AI报告 → 爬蟲分析與AI報告}/d4_2_ex2_crawler_ai.py +33 -2
- projects/2026_AI營1/劉宗穎/爬蟲分析與AI報告/main.py +18 -0
- projects/2026_AI營1/劉宗穎/眼睛距離偵測/main.py +18 -0
projects/2026_AI營1/劉宗穎/{DataFrame指令练习 → DataFrame指令練習}/d1_1_pandas_intro.py
RENAMED
|
@@ -1,37 +1,38 @@
|
|
| 1 |
-
|
| 2 |
-
#
|
| 3 |
-
#
|
| 4 |
-
#
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
print(
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
print(
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
print(
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
print(
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
print(f"
|
| 32 |
-
print(f"
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
| 37 |
print(egy)
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
# d1_1_pandas_intro.py
|
| 3 |
+
# Day 1 學員填空:Pandas 基礎(完成 TODO 後執行)
|
| 4 |
+
# cd examples/Day1/student && python3 d1_1_pandas_intro.py
|
| 5 |
+
# 參考答案:../Teacher/d1_1_pandas_intro.py
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
|
| 9 |
+
df = pd.read_csv(os.path.join(os.path.dirname(__file__), "taipei_weather_2026.csv"))
|
| 10 |
+
print("=== 數據載入成功! ===")
|
| 11 |
+
|
| 12 |
+
print("\n--- 1. head ---")
|
| 13 |
+
print(df.head())
|
| 14 |
+
|
| 15 |
+
print("\n--- 2. info ---")
|
| 16 |
+
df.info()
|
| 17 |
+
|
| 18 |
+
print("\n--- 3. describe ---")
|
| 19 |
+
print(df.describe())
|
| 20 |
+
|
| 21 |
+
print("\n--- 4. Temperature 前 5 筆 ---")
|
| 22 |
+
print(df['Temperature'].head())
|
| 23 |
+
|
| 24 |
+
print("\n--- 5. Date 與 Rainfall ---")
|
| 25 |
+
print(df[['Rainfall', 'Date']].head())
|
| 26 |
+
|
| 27 |
+
print("\n--- 挑戰:最高溫與平均濕度 ---")
|
| 28 |
+
max_temp = df['Temperature'].max()
|
| 29 |
+
avg_humidity = df['Humidity'].mean()
|
| 30 |
+
min_rainfall = df['Rainfall'].min()
|
| 31 |
+
print(f"最高溫度:{max_temp}°C")
|
| 32 |
+
print(f"平均濕度:{avg_humidity:.2f}%")
|
| 33 |
+
print(f"最低降雨:{min_rainfall:.2f}%")
|
| 34 |
+
|
| 35 |
+
print("\n--- whether it is good for egyptians")
|
| 36 |
+
|
| 37 |
+
egy = df[(df['Temperature'] > 27) & (df['Rainfall'] == 0)]
|
| 38 |
print(egy)
|
projects/2026_AI營1/劉宗穎/DataFrame指令練習/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d1_1_pandas_intro.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd1_1_pandas_intro.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/{DataFrame指令练习 → DataFrame指令練習}/taipei_weather_2026.csv
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/{ai_api串接练习 → ai_api串接練習}/d2_1_gemini.py
RENAMED
|
@@ -1,41 +1,63 @@
|
|
| 1 |
-
# d2_1_gemini.py — Day2 學員填空
|
| 2 |
-
# cd examples/Day2/Student && python3 d2_1_gemini.py
|
| 3 |
-
# 參考答案:../Teacher/d2_1_gemini.py
|
| 4 |
-
|
| 5 |
-
import os
|
| 6 |
-
from google import genai
|
| 7 |
-
|
| 8 |
-
# === 【學員填空練習點 1:設置與載入 API Key】 ===
|
| 9 |
-
# TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
print(
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
# print(response.text)
|
|
|
|
| 1 |
+
# d2_1_gemini.py — Day2 學員填空
|
| 2 |
+
# cd examples/Day2/Student && python3 d2_1_gemini.py
|
| 3 |
+
# 參考答案:../Teacher/d2_1_gemini.py
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from google import genai
|
| 7 |
+
|
| 8 |
+
# === 【學員填空練習點 1:設置與載入 API Key】 ===
|
| 9 |
+
# TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。
|
| 10 |
+
|
| 11 |
+
def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str:
|
| 12 |
+
"""終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。"""
|
| 13 |
+
import os
|
| 14 |
+
key = (os.environ.get("GEMINI_API_KEY") or "").strip()
|
| 15 |
+
if key:
|
| 16 |
+
print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY")
|
| 17 |
+
return key
|
| 18 |
+
print()
|
| 19 |
+
print("=" * 54)
|
| 20 |
+
print(" 本專案需要 Google Gemini API Key")
|
| 21 |
+
print(" 取得金鑰:https://aistudio.google.com/apikey")
|
| 22 |
+
print(" 請在下方貼上 Key 後按 Enter")
|
| 23 |
+
print(" (僅本次執行使用,不會寫入任何檔案)")
|
| 24 |
+
if allow_skip:
|
| 25 |
+
print(" 若要改用離線 Mock 模式:直接按 Enter 略過")
|
| 26 |
+
print("=" * 54)
|
| 27 |
+
try:
|
| 28 |
+
key = input("請輸入 GEMINI_API_KEY: ").strip()
|
| 29 |
+
except EOFError:
|
| 30 |
+
key = ""
|
| 31 |
+
if key:
|
| 32 |
+
os.environ["GEMINI_API_KEY"] = key
|
| 33 |
+
print("[Telearn] 已接收 API Key,開始連線…")
|
| 34 |
+
return key
|
| 35 |
+
if allow_skip:
|
| 36 |
+
print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能")
|
| 37 |
+
return ""
|
| 38 |
+
print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。")
|
| 39 |
+
raise SystemExit(0)
|
| 40 |
+
|
| 41 |
+
api_key = _telearn_require_gemini_api_key(allow_skip=False)
|
| 42 |
+
|
| 43 |
+
# === 【學員填空練習點 2:設定模型名稱】 ===
|
| 44 |
+
# TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite')
|
| 45 |
+
model_name = "gemini-3.1-flash-lite"
|
| 46 |
+
|
| 47 |
+
print(f"正在連線至 Google Cloud Model ({model_name})...")
|
| 48 |
+
|
| 49 |
+
# === 【學員填空練習點 3:初始化 Gemini Client】 ===
|
| 50 |
+
# TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數
|
| 51 |
+
client = genai.Client(api_key=api_key)
|
| 52 |
+
|
| 53 |
+
# === 【學員填空練習點 4:發送內容生成請求】 ===
|
| 54 |
+
# TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞
|
| 55 |
+
# 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。
|
| 56 |
+
response = client.models.generate_content(model=model_name, contents="请用中文解释为什么泡面很好吃,字数一百字以内")
|
| 57 |
+
# === 【學員填空練習點 5:讀取並印出回應】 ===
|
| 58 |
+
# TODO: 從 response 物件中取得 text 欄位內容,並列印出來。
|
| 59 |
+
print("\n=== AI 回應結果 ===")
|
| 60 |
+
# 請在下方實作列印邏輯:
|
| 61 |
+
print(response.text)
|
| 62 |
+
# for i in range(10):
|
| 63 |
# print(response.text)
|
projects/2026_AI營1/劉宗穎/ai_api串接練習/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d2_1_gemini.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd2_1_gemini.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/{mock_密室逃脱游戏 → mock_密室逃脫遊戲}/d2_3_chatbot_memory_game_template.py
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/mock_密室逃脫遊戲/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d2_3_chatbot_memory_game_template.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd2_3_chatbot_memory_game_template.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/{mock_关键词抓取 → mock_關鍵詞抓取}/d2_3_chatbot_memory_mock.py
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/mock_關鍵詞抓取/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d2_3_chatbot_memory_mock.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd2_3_chatbot_memory_mock.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/{mock&AI_api双模式 → mock與AI_api雙模式}/d2_3_chatbot_memory.py
RENAMED
|
@@ -1,95 +1,124 @@
|
|
| 1 |
-
# d2_3_chatbot_memory.py
|
| 2 |
-
# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模)
|
| 3 |
-
|
| 4 |
-
import os
|
| 5 |
-
from google import genai
|
| 6 |
-
|
| 7 |
-
# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite
|
| 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 |
-
print("
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
except
|
| 88 |
-
print("
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# d2_3_chatbot_memory.py
|
| 2 |
+
# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模)
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
from google import genai
|
| 6 |
+
|
| 7 |
+
# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite
|
| 8 |
+
|
| 9 |
+
def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str:
|
| 10 |
+
"""終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。"""
|
| 11 |
+
import os
|
| 12 |
+
key = (os.environ.get("GEMINI_API_KEY") or "").strip()
|
| 13 |
+
if key:
|
| 14 |
+
print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY")
|
| 15 |
+
return key
|
| 16 |
+
print()
|
| 17 |
+
print("=" * 54)
|
| 18 |
+
print(" 本專案需要 Google Gemini API Key")
|
| 19 |
+
print(" 取得金鑰:https://aistudio.google.com/apikey")
|
| 20 |
+
print(" 請在下方貼上 Key 後按 Enter")
|
| 21 |
+
print(" (僅本次執行使用,不會寫入任何檔案)")
|
| 22 |
+
if allow_skip:
|
| 23 |
+
print(" 若要改用離線 Mock 模式:直接按 Enter 略過")
|
| 24 |
+
print("=" * 54)
|
| 25 |
+
try:
|
| 26 |
+
key = input("請輸入 GEMINI_API_KEY: ").strip()
|
| 27 |
+
except EOFError:
|
| 28 |
+
key = ""
|
| 29 |
+
if key:
|
| 30 |
+
os.environ["GEMINI_API_KEY"] = key
|
| 31 |
+
print("[Telearn] 已接收 API Key,開始連線…")
|
| 32 |
+
return key
|
| 33 |
+
if allow_skip:
|
| 34 |
+
print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能")
|
| 35 |
+
return ""
|
| 36 |
+
print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。")
|
| 37 |
+
raise SystemExit(0)
|
| 38 |
+
|
| 39 |
+
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite")
|
| 40 |
+
api_key = _telearn_require_gemini_api_key(allow_skip=True)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def local_mock_response(user_input, history):
|
| 44 |
+
user_input_lower = user_input.lower()
|
| 45 |
+
|
| 46 |
+
# TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句
|
| 47 |
+
if "營業時間" in user_input_lower or "時間" in user_input_lower:
|
| 48 |
+
return "我們觀測站的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。"
|
| 49 |
+
# elif "XX" in user_input_lower or "XX" in user_input_lower:
|
| 50 |
+
# return "XXXXXXXXXXXXXXXXX"
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
else:
|
| 54 |
+
turns = len(history) // 2 + 1
|
| 55 |
+
return (
|
| 56 |
+
f"(模擬模式)收到您的訊息:『{user_input}』。"
|
| 57 |
+
f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def main():
|
| 62 |
+
print("=" * 50)
|
| 63 |
+
print(" 得能 AI 智慧客服對話系統 (支援對話上下文記憶)")
|
| 64 |
+
print("=" * 50)
|
| 65 |
+
|
| 66 |
+
use_mock = False
|
| 67 |
+
chat_session = None
|
| 68 |
+
|
| 69 |
+
if not api_key:
|
| 70 |
+
print("[提示] 未輸入 GEMINI_API_KEY,改用「離線模擬模式 (Local Mock Mode)」。")
|
| 71 |
+
use_mock = True
|
| 72 |
+
else:
|
| 73 |
+
try:
|
| 74 |
+
print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...")
|
| 75 |
+
client = genai.Client(api_key=api_key)
|
| 76 |
+
# TODO: 設定系統指令 (System Instruction / System Prompt)
|
| 77 |
+
# system_instruction = (
|
| 78 |
+
# "你是一位專業的得能教育客服人員,語氣溫和客氣,"
|
| 79 |
+
# "XXXXXXXXXXXXXXXXXXXXXXX"
|
| 80 |
+
# )
|
| 81 |
+
|
| 82 |
+
chat_session = client.chats.create(
|
| 83 |
+
model=GEMINI_MODEL,
|
| 84 |
+
config={'system_instruction': system_instruction}
|
| 85 |
+
)
|
| 86 |
+
print("雲端客服系統已就緒!")
|
| 87 |
+
except Exception as e:
|
| 88 |
+
print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...")
|
| 89 |
+
use_mock = True
|
| 90 |
+
|
| 91 |
+
local_history = []
|
| 92 |
+
|
| 93 |
+
print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):")
|
| 94 |
+
print("-" * 50)
|
| 95 |
+
|
| 96 |
+
while True:
|
| 97 |
+
try:
|
| 98 |
+
user_input = input("\n[您]: ").strip()
|
| 99 |
+
if not user_input:
|
| 100 |
+
continue
|
| 101 |
+
|
| 102 |
+
if user_input.lower() in ["quit", "exit"]:
|
| 103 |
+
print("對話結束,感謝您的使用!")
|
| 104 |
+
break
|
| 105 |
+
|
| 106 |
+
if use_mock:
|
| 107 |
+
ai_reply = local_mock_response(user_input, local_history)
|
| 108 |
+
local_history.append({"role": "user", "text": user_input})
|
| 109 |
+
local_history.append({"role": "model", "text": ai_reply})
|
| 110 |
+
else:
|
| 111 |
+
response = chat_session.send_message(user_input)
|
| 112 |
+
ai_reply = response.text
|
| 113 |
+
|
| 114 |
+
print(f"[AI 客服]: {ai_reply}")
|
| 115 |
+
|
| 116 |
+
except KeyboardInterrupt:
|
| 117 |
+
print("\n對話強行終止。再見!")
|
| 118 |
+
break
|
| 119 |
+
except Exception as e:
|
| 120 |
+
print(f"\n[錯誤] 發生異常:{e}")
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == "__main__":
|
| 124 |
+
main()
|
projects/2026_AI營1/劉宗穎/{mock&AI_api双模式 → mock與AI_api雙模式}/d2_4_chatbot_memory.py
RENAMED
|
@@ -1,129 +1,158 @@
|
|
| 1 |
-
# d2_3_chatbot_memory.py
|
| 2 |
-
# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模)
|
| 3 |
-
|
| 4 |
-
import os
|
| 5 |
-
from google import genai
|
| 6 |
-
|
| 7 |
-
# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite
|
| 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 |
-
return "
|
| 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 |
-
except
|
| 122 |
-
print("
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# d2_3_chatbot_memory.py
|
| 2 |
+
# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模)
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
from google import genai
|
| 6 |
+
|
| 7 |
+
# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite
|
| 8 |
+
|
| 9 |
+
def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str:
|
| 10 |
+
"""終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。"""
|
| 11 |
+
import os
|
| 12 |
+
key = (os.environ.get("GEMINI_API_KEY") or "").strip()
|
| 13 |
+
if key:
|
| 14 |
+
print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY")
|
| 15 |
+
return key
|
| 16 |
+
print()
|
| 17 |
+
print("=" * 54)
|
| 18 |
+
print(" 本專案需要 Google Gemini API Key")
|
| 19 |
+
print(" 取得金鑰:https://aistudio.google.com/apikey")
|
| 20 |
+
print(" 請在下方貼上 Key 後按 Enter")
|
| 21 |
+
print(" (僅本次執行使用,不會寫入任何檔案)")
|
| 22 |
+
if allow_skip:
|
| 23 |
+
print(" 若要改用離線 Mock 模式:直接按 Enter 略過")
|
| 24 |
+
print("=" * 54)
|
| 25 |
+
try:
|
| 26 |
+
key = input("請輸入 GEMINI_API_KEY: ").strip()
|
| 27 |
+
except EOFError:
|
| 28 |
+
key = ""
|
| 29 |
+
if key:
|
| 30 |
+
os.environ["GEMINI_API_KEY"] = key
|
| 31 |
+
print("[Telearn] 已接收 API Key,開始連線…")
|
| 32 |
+
return key
|
| 33 |
+
if allow_skip:
|
| 34 |
+
print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能")
|
| 35 |
+
return ""
|
| 36 |
+
print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。")
|
| 37 |
+
raise SystemExit(0)
|
| 38 |
+
|
| 39 |
+
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite")
|
| 40 |
+
api_key = _telearn_require_gemini_api_key(allow_skip=True)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def local_mock_response(user_input, history):
|
| 44 |
+
user_input_lower = user_input.lower()
|
| 45 |
+
|
| 46 |
+
if "營業時間" in user_input_lower or "時間" in user_input_lower:
|
| 47 |
+
return "我们冰淇淋店的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。"
|
| 48 |
+
elif "地址" in user_input_lower or "地点" in user_input_lower or "地方" in user_input_lower:
|
| 49 |
+
return "我们的店在求毛星球毛笔城市QQ镇喜喜街167号一楼。"
|
| 50 |
+
elif "冰淇淋的口味" in user_input_lower or "口味" in user_input_lower:
|
| 51 |
+
return "我们的冰淇淋的口味有草莓,芒果,巧克力,咖啡,蓝莓,汽水和芭乐。"
|
| 52 |
+
elif "冰淇淋的价格" in user_input_lower or "价格" in user_input_lower or "冰淇淋怎么卖" in user_input_lower:
|
| 53 |
+
return "一球100元,两球170元,三球250元,五球400元,十球750元。"
|
| 54 |
+
elif "有打折吗" in user_input_lower or "有优惠吗" in user_input_lower or "有进行促销活动吗" in user_input_lower or "有活动吗" in user_input_lower or "折扣" in user_input_lower:
|
| 55 |
+
return "我们有和吃鸡炸鸡店合作,去那里消费满1000元即可换一球冰淇淋。"
|
| 56 |
+
elif "help" in user_input_lower or "帮助" in user_input_lower:
|
| 57 |
+
return "\n輸入「營業時間、時間」即可知道營業時間\n輸入「地址、地點、地方」即可知道地址\n輸入「冰淇淋的口味、口味」即可知道口味\n輸入「冰淇淋的價格、價格、冰淇淋怎麼賣」即可知道價格\n輸入「有打折嗎、有優惠嗎、有進行促銷活動嗎、有活動嗎、折扣」即可知道優惠資訊\n輸入「停車場、停車、附近有沒有停車場」即可知道停車資訊\n輸入「可以客製化嗎、客製化的服務、客製化」即可知道客製化服務\n輸入「客製化的價錢、客製化要多少錢」即可知道客製化價格\n輸入「推薦、招牌」即可知道推薦口味\n輸入「外帶、內用」即可知道用餐方式\n輸入「付款、支付、可以刷卡」即可知道付款方式。"
|
| 58 |
+
elif "你好" in user_input_lower or "hello" in user_input_lower or "hi" in user_input_lower or "Hi" in user_input_lower:
|
| 59 |
+
return "你好!请问您需要什么帮助吗?"
|
| 60 |
+
elif "停车场" in user_input_lower or "停车" in user_input_lower or "附近有没有停车场" in user_input_lower:
|
| 61 |
+
return "我们的店附近有三个停车场,步行三分钟即可到达我们的店。"
|
| 62 |
+
elif "可以克制化吗" in user_input_lower or "客制化的服务" in user_input_lower or "客制化" in user_input_lower:
|
| 63 |
+
return "我们的店可以克制化冰淇淋,可以选择彩糖、珍珠、草莓酱、巧克力酱、布丁、奶油和炼乳。"
|
| 64 |
+
elif "客制化的价钱" in user_input_lower or "客制化要多少钱" in user_input_lower or "客制化要多少钱?" in user_input_lower:
|
| 65 |
+
return "客制化每一种材料都要再加15元。"
|
| 66 |
+
elif "推薦" in user_input_lower or "招牌" in user_input_lower:
|
| 67 |
+
return "推薦您試試草莓、巧克力和芭樂口味,都是熱門選擇。"
|
| 68 |
+
elif "外帶" in user_input_lower or "內用" in user_input_lower:
|
| 69 |
+
return "我們提供內用及外帶服務。"
|
| 70 |
+
elif "付款" in user_input_lower or "支付" in user_input_lower or "可以刷卡" in user_input_lower:
|
| 71 |
+
return "我們接受現金、信用卡及行動支付。"
|
| 72 |
+
elif "谢谢" in user_input_lower or "Thank you" in user_input_lower:
|
| 73 |
+
return "不客气!还需要什么帮助吗?"
|
| 74 |
+
|
| 75 |
+
else:
|
| 76 |
+
# 計算目前是第幾輪對話(一輪包含使用者輸入與客服回覆,共 2 個元素)
|
| 77 |
+
# Why: 藉由傳入的 history 串列長度除以 2,即可動態得知目前互動的輪數,模擬記憶狀態。
|
| 78 |
+
turns = len(history) // 2 + 1
|
| 79 |
+
return (
|
| 80 |
+
f"(模擬模式)收到您的訊息:『{user_input}』。"
|
| 81 |
+
f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def main():
|
| 86 |
+
print("=" * 50)
|
| 87 |
+
print(" 吃吃吃冰淇淋店客服對話系統 (支援對話上下文記憶)")
|
| 88 |
+
print("=" * 50)
|
| 89 |
+
|
| 90 |
+
use_mock = False
|
| 91 |
+
chat_session = None
|
| 92 |
+
|
| 93 |
+
if not api_key:
|
| 94 |
+
print("[提示] 未輸入 GEMINI_API_KEY,改用「離線模擬模式 (Local Mock Mode)」。")
|
| 95 |
+
use_mock = True
|
| 96 |
+
else:
|
| 97 |
+
try:
|
| 98 |
+
print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...")
|
| 99 |
+
client = genai.Client(api_key=api_key)
|
| 100 |
+
# TODO: 設定系統指令 (System Instruction / System Prompt)
|
| 101 |
+
system_instruction = (
|
| 102 |
+
"你是一位冰淇淋店的客服,负责提供资讯"
|
| 103 |
+
"我们冰淇淋店的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。"
|
| 104 |
+
"我们的店在求毛星球毛笔城市QQ镇喜喜街167号一楼。"
|
| 105 |
+
"冰淇淋的��格一球100元,两球170元,三球250元,五球400元,十球750元。"
|
| 106 |
+
"我们的店有进行优惠,有和吃鸡炸鸡店合作,去那里消费满1000元即可换一球冰淇淋。"
|
| 107 |
+
"这是我们的资讯:輸入「營業時間、時間」即可知道營業時間,輸入「地址、地點、地方」即可知道地址,輸入「冰淇淋的口味、口味」即可知道口味,輸入「冰淇淋的價格、價格、冰淇淋怎麼賣」即可知道價格,輸入「有打折嗎、有優惠嗎、有進行促銷活動嗎、有活動嗎、折扣」即可知道優惠資訊,輸入「停車場、停車、附近有沒有停車場」即可知道停車資訊,輸入「可以客製化嗎、客製化的服務、客製化」即可知道客製化服務,輸入「客製化的價錢、客製化要多少錢」即可知道客製化價格,輸入「推薦、招牌」即可知道推薦口味,輸入「外帶、內用」即可知道用餐方式,輸入「付款、支付、可以刷卡」即可知道付款方式。"
|
| 108 |
+
"你应该亲切的向客户问好。"
|
| 109 |
+
"我们的店附近有三个停车场,步行三分钟即可到达我们的店。"
|
| 110 |
+
"我们的店可以克制化冰淇淋,可以选择彩糖、珍珠、草莓酱、巧克力酱、布丁、奶油和炼乳。"
|
| 111 |
+
"客制化每一种材料都要再加15元。"
|
| 112 |
+
"我們提供內用及外帶服務。"
|
| 113 |
+
"我們接受現金、信用卡及行動支付。"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
chat_session = client.chats.create(
|
| 117 |
+
model=GEMINI_MODEL,
|
| 118 |
+
config={'system_instruction': system_instruction}
|
| 119 |
+
)
|
| 120 |
+
print("雲端客服系統已就緒!")
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...")
|
| 123 |
+
use_mock = True
|
| 124 |
+
|
| 125 |
+
local_history = []
|
| 126 |
+
|
| 127 |
+
print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):")
|
| 128 |
+
print("-" * 50)
|
| 129 |
+
|
| 130 |
+
while True:
|
| 131 |
+
try:
|
| 132 |
+
user_input = input("\n[您]: ").strip()
|
| 133 |
+
if not user_input:
|
| 134 |
+
continue
|
| 135 |
+
|
| 136 |
+
if user_input.lower() in ["quit", "exit"]:
|
| 137 |
+
print("對話結束,感謝您的使用!")
|
| 138 |
+
break
|
| 139 |
+
|
| 140 |
+
if use_mock:
|
| 141 |
+
ai_reply = local_mock_response(user_input, local_history)
|
| 142 |
+
local_history.append({"role": "user", "text": user_input})
|
| 143 |
+
local_history.append({"role": "model", "text": ai_reply})
|
| 144 |
+
else:
|
| 145 |
+
response = chat_session.send_message(user_input)
|
| 146 |
+
ai_reply = response.text
|
| 147 |
+
|
| 148 |
+
print(f"[AI 客服]: {ai_reply}")
|
| 149 |
+
|
| 150 |
+
except KeyboardInterrupt:
|
| 151 |
+
print("\n對話強行終止。再見!")
|
| 152 |
+
break
|
| 153 |
+
except Exception as e:
|
| 154 |
+
print(f"\n[錯誤] 發生異常:{e}")
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
if __name__ == "__main__":
|
| 158 |
+
main()
|
projects/2026_AI營1/劉宗穎/mock與AI_api雙模式/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d2_4_chatbot_memory.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd2_4_chatbot_memory.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/{三指捏游戏 → 三指捏遊戲}/d5_pinch_challenge_solution.py
RENAMED
|
@@ -1,217 +1,216 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版)
|
| 3 |
-
==================================================================
|
| 4 |
-
本週挑戰目標:
|
| 5 |
-
1. 實作「三指捏合 (Three-Finger Pinch)」:
|
| 6 |
-
必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」
|
| 7 |
-
以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch
|
| 8 |
-
2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊
|
| 9 |
-
3. 使用邊緣觸發 (Edge Detection) 防止重複觸發
|
| 10 |
-
4. 新增 30 秒限時機制,時間到顯示最終得分
|
| 11 |
-
"""
|
| 12 |
-
import pygame
|
| 13 |
-
import cv2
|
| 14 |
-
import mediapipe as mp
|
| 15 |
-
import math
|
| 16 |
-
import sys
|
| 17 |
-
import os
|
| 18 |
-
import random
|
| 19 |
-
|
| 20 |
-
# ============================================
|
| 21 |
-
# 1. 初始化
|
| 22 |
-
# ============================================
|
| 23 |
-
pygame.init()
|
| 24 |
-
WIDTH, HEIGHT = 800, 600
|
| 25 |
-
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 26 |
-
pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ")
|
| 27 |
-
clock = pygame.time.Clock()
|
| 28 |
-
from mediapipe.tasks import python
|
| 29 |
-
from mediapipe.tasks.python import vision
|
| 30 |
-
|
| 31 |
-
# 攝影機
|
| 32 |
-
cap = cv2.VideoCapture(0)
|
| 33 |
-
|
| 34 |
-
# 【TODO 1】初始
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
from mediapipe.tasks import
|
| 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 |
-
button_rect.
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
btn_color
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
time_text
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
pygame.draw.circle(screen,
|
| 183 |
-
pygame.draw.circle(screen, (
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
#
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
warning_text
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
screen.blit(
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
sys.exit()
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版)
|
| 3 |
+
==================================================================
|
| 4 |
+
本週挑戰目標:
|
| 5 |
+
1. 實作「三指捏合 (Three-Finger Pinch)」:
|
| 6 |
+
必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」
|
| 7 |
+
以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch
|
| 8 |
+
2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊
|
| 9 |
+
3. 使用邊緣觸發 (Edge Detection) 防止重複觸發
|
| 10 |
+
4. 新增 30 秒限時機制,時間到顯示最終得分
|
| 11 |
+
"""
|
| 12 |
+
import pygame
|
| 13 |
+
import cv2
|
| 14 |
+
import mediapipe as mp
|
| 15 |
+
import math
|
| 16 |
+
import sys
|
| 17 |
+
import os
|
| 18 |
+
import random
|
| 19 |
+
|
| 20 |
+
# ============================================
|
| 21 |
+
# 1. 初始化
|
| 22 |
+
# ============================================
|
| 23 |
+
pygame.init()
|
| 24 |
+
WIDTH, HEIGHT = 800, 600
|
| 25 |
+
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
| 26 |
+
pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ")
|
| 27 |
+
clock = pygame.time.Clock()
|
| 28 |
+
from mediapipe.tasks import python
|
| 29 |
+
from mediapipe.tasks.python import vision
|
| 30 |
+
|
| 31 |
+
# 攝影機
|
| 32 |
+
cap = cv2.VideoCapture(0)
|
| 33 |
+
|
| 34 |
+
# 【TODO 1】初始�� MediaPipe 手勢偵測器 (Hand Landmarker)
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
from mediapipe.tasks import python
|
| 38 |
+
from mediapipe.tasks.python import vision
|
| 39 |
+
base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hand_landmarker.task'))
|
| 40 |
+
options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
|
| 41 |
+
detector = vision.HandLandmarker.create_from_options(options)
|
| 42 |
+
except Exception as e:
|
| 43 |
+
print(f"Error loading model: {e}")
|
| 44 |
+
pygame.quit()
|
| 45 |
+
sys.exit()
|
| 46 |
+
|
| 47 |
+
# ============================================
|
| 48 |
+
# 2. 遊戲變數
|
| 49 |
+
# ============================================
|
| 50 |
+
# 手指座標
|
| 51 |
+
finger_x, finger_y = 400, 300 # 食指
|
| 52 |
+
thumb_x, thumb_y = 400, 300 # 大拇指
|
| 53 |
+
middle_x, middle_y = 400, 300 # 中指
|
| 54 |
+
|
| 55 |
+
# 距離變數
|
| 56 |
+
dist_thumb_index = 0.0
|
| 57 |
+
dist_thumb_middle = 0.0
|
| 58 |
+
|
| 59 |
+
# Pinch 相關變數
|
| 60 |
+
is_pinching = False
|
| 61 |
+
was_pinching = False # 邊緣觸發
|
| 62 |
+
PINCH_THRESHOLD = 50 # 距離臨界值
|
| 63 |
+
|
| 64 |
+
# 按鈕設定
|
| 65 |
+
button_rect = pygame.Rect(300, 250, 200, 100)
|
| 66 |
+
button_colors = [(100, 100, 220), (220, 100, 100),
|
| 67 |
+
(100, 220, 100), (220, 180, 50)]
|
| 68 |
+
current_color_index = 0
|
| 69 |
+
|
| 70 |
+
# 點擊計數器
|
| 71 |
+
click_count = 0
|
| 72 |
+
|
| 73 |
+
# --- 限時機制變數 ---
|
| 74 |
+
START_TIME = pygame.time.get_ticks()
|
| 75 |
+
GAME_DURATION = 30 # 遊戲限制時間(秒)
|
| 76 |
+
game_state = "PLAYING" # 狀態機
|
| 77 |
+
|
| 78 |
+
# ============================================
|
| 79 |
+
# 3. 主迴圈
|
| 80 |
+
# ============================================
|
| 81 |
+
running = True
|
| 82 |
+
while running:
|
| 83 |
+
for event in pygame.event.get():
|
| 84 |
+
if event.type == pygame.QUIT:
|
| 85 |
+
running = False
|
| 86 |
+
|
| 87 |
+
# --- 讀取攝影機與手勢偵測 ---
|
| 88 |
+
success, img = cap.read()
|
| 89 |
+
is_pinching = False # 每幀重設狀態,防止手離屏時卡死
|
| 90 |
+
|
| 91 |
+
if success:
|
| 92 |
+
img = cv2.flip(img, 1) # 鏡像翻轉
|
| 93 |
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 94 |
+
|
| 95 |
+
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
|
| 96 |
+
results = detector.detect(mp_image)
|
| 97 |
+
|
| 98 |
+
# === 【TODO 2:挑戰關卡三之手部安全偵測】 ===
|
| 99 |
+
# 1. 判斷手部是否在畫面中 (results.hand_landmarks 是否有資料)
|
| 100 |
+
# 2. 如果在畫面中,取得食指 (8)、大拇指 (4)、中指 (12) 的像素座標
|
| 101 |
+
# 3. 如果手不在畫面中,將 is_pinching 設為 False
|
| 102 |
+
#
|
| 103 |
+
hand_in_frame = False
|
| 104 |
+
if results.hand_landmarks:
|
| 105 |
+
hand_in_frame = True
|
| 106 |
+
for hand_lms in results.hand_landmarks:
|
| 107 |
+
# 食指 (8)
|
| 108 |
+
lm8 = hand_lms[8]
|
| 109 |
+
finger_x = int(lm8.x * WIDTH)
|
| 110 |
+
finger_y = int(lm8.y * HEIGHT)
|
| 111 |
+
|
| 112 |
+
# 大拇指 (4)
|
| 113 |
+
lm4 = hand_lms[4]
|
| 114 |
+
thumb_x = int(lm4.x * WIDTH)
|
| 115 |
+
thumb_y = int(lm4.y * HEIGHT)
|
| 116 |
+
|
| 117 |
+
# 中指 (12)
|
| 118 |
+
lm12 = hand_lms[12]
|
| 119 |
+
middle_x = int(lm12.x * WIDTH)
|
| 120 |
+
middle_y = int(lm12.y * HEIGHT)
|
| 121 |
+
|
| 122 |
+
# === 【TODO 3:挑戰關卡一之三指捏合判定】 ===
|
| 123 |
+
# 提示:計算食指與大拇指距離,以及中指與大拇指距離
|
| 124 |
+
# 兩組距離皆小於 PINCH_THRESHOLD 時,is_pinching 才為 True
|
| 125 |
+
dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y)
|
| 126 |
+
dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y)
|
| 127 |
+
is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD)
|
| 128 |
+
|
| 129 |
+
dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y)
|
| 130 |
+
dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y)
|
| 131 |
+
is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD)
|
| 132 |
+
|
| 133 |
+
# --- 狀態機邏輯處理 ---
|
| 134 |
+
if game_state == "PLAYING":
|
| 135 |
+
# 1. 計算剩餘時間
|
| 136 |
+
current_ticks = pygame.time.get_ticks()
|
| 137 |
+
elapsed_seconds = (current_ticks - START_TIME) / 1000
|
| 138 |
+
remaining_time = max(0, GAME_DURATION - elapsed_seconds)
|
| 139 |
+
|
| 140 |
+
# 2. 檢查時間是否到了
|
| 141 |
+
if remaining_time <= 0:
|
| 142 |
+
game_state = "GAME_OVER"
|
| 143 |
+
|
| 144 |
+
# 3. 邊緣觸發點擊判斷
|
| 145 |
+
if is_pinching and not was_pinching:
|
| 146 |
+
finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10)
|
| 147 |
+
if finger_rect.colliderect(button_rect):
|
| 148 |
+
click_count += 1
|
| 149 |
+
current_color_index = (current_color_index + 1) % len(button_colors)
|
| 150 |
+
|
| 151 |
+
# === 【TODO 4:挑戰關卡二之按鈕隨機移動】 ===
|
| 152 |
+
# 提示:當點擊發生時,讓按鈕的 x 和 y 座標隨機分配在畫面內
|
| 153 |
+
|
| 154 |
+
button_rect.x = random.randint(0, WIDTH - button_rect.width)
|
| 155 |
+
button_rect.y = random.randint(0, HEIGHT - button_rect.height)
|
| 156 |
+
print(f" 魔法三指點擊!第 {click_count} 次")
|
| 157 |
+
|
| 158 |
+
# 更新上一幀狀態
|
| 159 |
+
was_pinching = is_pinching
|
| 160 |
+
|
| 161 |
+
# --- 繪製畫面 ---
|
| 162 |
+
screen.fill((20, 20, 35))
|
| 163 |
+
font = pygame.font.SysFont(None, 32)
|
| 164 |
+
large_font = pygame.font.SysFont(None, 64)
|
| 165 |
+
|
| 166 |
+
if game_state == "PLAYING":
|
| 167 |
+
# 繪製按鈕
|
| 168 |
+
btn_color = button_colors[current_color_index]
|
| 169 |
+
pygame.draw.rect(screen, btn_color, button_rect, border_radius=16)
|
| 170 |
+
|
| 171 |
+
btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255))
|
| 172 |
+
screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2,
|
| 173 |
+
button_rect.centery - btn_text.get_height() // 2))
|
| 174 |
+
|
| 175 |
+
# 顯示倒數計時
|
| 176 |
+
time_text = font.render(f"Time Left: {remaining_time:.1f}s", True, (255, 220, 100))
|
| 177 |
+
screen.blit(time_text, (WIDTH - time_text.get_width() - 20, 10))
|
| 178 |
+
|
| 179 |
+
# 畫手指游標
|
| 180 |
+
cursor_color = (50, 255, 50) if is_pinching else (255, 50, 50)
|
| 181 |
+
pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) # 食指
|
| 182 |
+
pygame.draw.circle(screen, (255, 255, 255), (thumb_x, thumb_y), 12) # 大拇指
|
| 183 |
+
pygame.draw.circle(screen, (100, 150, 255), (middle_x, middle_y), 12) # 中指
|
| 184 |
+
|
| 185 |
+
# === 【TODO 5:挑戰關卡三之畫面提示】 ===
|
| 186 |
+
# 提示:當手不在畫面中時,在畫面上方顯示警告文字提醒學生將手部放回
|
| 187 |
+
|
| 188 |
+
if not hand_in_frame:
|
| 189 |
+
warning_text = font.render("OUT OF FRAME - PLEASE PUT HANDS BACK", True, (255, 100, 100))
|
| 190 |
+
screen.blit(warning_text, (WIDTH // 2 - warning_text.get_width() // 2, 100))
|
| 191 |
+
|
| 192 |
+
# 顯示調試數值
|
| 193 |
+
small_font = pygame.font.SysFont(None, 24)
|
| 194 |
+
status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150))
|
| 195 |
+
screen.blit(status, (10, 10))
|
| 196 |
+
|
| 197 |
+
dist_text = small_font.render(f"Dist Thumb-Index: {dist_thumb_index:.0f} px", True, (150, 150, 200))
|
| 198 |
+
screen.blit(dist_text, (10, 35))
|
| 199 |
+
|
| 200 |
+
dist_text2 = small_font.render(f"Dist Thumb-Middle: {dist_thumb_middle:.0f} px", True, (150, 150, 200))
|
| 201 |
+
screen.blit(dist_text2, (10, 60))
|
| 202 |
+
|
| 203 |
+
elif game_state == "GAME_OVER":
|
| 204 |
+
# 繪製遊戲結束畫面
|
| 205 |
+
over_text = large_font.render("GAME OVER", True, (255, 50, 50))
|
| 206 |
+
score_text = font.render(f"Total Clicks: {click_count} times", True, (255, 255, 255))
|
| 207 |
+
|
| 208 |
+
screen.blit(over_text, (WIDTH // 2 - over_text.get_width() // 2, HEIGHT // 2 - 50))
|
| 209 |
+
screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, HEIGHT // 2 + 20))
|
| 210 |
+
|
| 211 |
+
pygame.display.update()
|
| 212 |
+
clock.tick(30)
|
| 213 |
+
|
| 214 |
+
cap.release()
|
| 215 |
+
pygame.quit()
|
| 216 |
+
sys.exit()
|
|
|
projects/2026_AI營1/劉宗穎/三指捏遊戲/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d5_pinch_challenge_solution.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd5_pinch_challenge_solution.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/{人脸辨识 → 人臉辨識}/d3_2_face_detection.py
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/{人脸辨识 → 人臉辨識}/d3_2_face_detection2.py
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/人臉辨識/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d3_2_face_detection2.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd3_2_face_detection2.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/人臉辨識/onepiece(2).jpeg
ADDED
|
Git LFS Details
|
projects/2026_AI營1/劉宗穎/{咖啡店分析图表 → 咖啡店分析圖表}/coffee_sales_analysis.png
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/{咖啡店分析图表 → 咖啡店分析圖表}/coffee_sales_data.csv
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/{咖啡店分析图表 → 咖啡店分析圖表}/d1_4_visualization_coffee.py
RENAMED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
# d1_4_visualization_coffee.py
|
| 2 |
# Day 1: 資料科學與數據視覺化 - 咖啡廳營運數據進階分析(學員填空)
|
| 3 |
# cd examples/Day1/student && python3 d1_4_visualization_coffee.py
|
|
@@ -8,6 +10,34 @@ import pandas as pd
|
|
| 8 |
import matplotlib.pyplot as plt
|
| 9 |
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
def load_csv(filename="coffee_sales_data.csv"):
|
| 12 |
"""安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題"""
|
| 13 |
# === 【學員填空練習點 1:CSV 檔案安全讀取】 ===
|
|
@@ -112,4 +142,4 @@ plt.tight_layout() # 自動調整間距,避免文字重疊
|
|
| 112 |
output_image = "coffee_sales_analysis.png"
|
| 113 |
# 請在下方完成圖表存檔與顯示:
|
| 114 |
plt.savefig(output_image, dpi=300)
|
| 115 |
-
|
|
|
|
| 1 |
+
import matplotlib
|
| 2 |
+
matplotlib.use('Agg') # Telearn hub headless
|
| 3 |
# d1_4_visualization_coffee.py
|
| 4 |
# Day 1: 資料科學與數據視覺化 - 咖啡廳營運數據進階分析(學員填空)
|
| 5 |
# cd examples/Day1/student && python3 d1_4_visualization_coffee.py
|
|
|
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
|
| 12 |
|
| 13 |
+
|
| 14 |
+
def _telearn_plt_show(*_a, **_k):
|
| 15 |
+
"""Hub-friendly plt.show: save current figure and display via OpenCV."""
|
| 16 |
+
import os
|
| 17 |
+
import matplotlib.pyplot as plt
|
| 18 |
+
try:
|
| 19 |
+
import cv2
|
| 20 |
+
except ImportError:
|
| 21 |
+
plt.savefig("_telearn_last_plot.png", dpi=150, bbox_inches="tight")
|
| 22 |
+
print("[Telearn] 已儲存 _telearn_last_plot.png(無 cv2 可顯示)")
|
| 23 |
+
return
|
| 24 |
+
path = os.path.join(os.getcwd(), "_telearn_last_plot.png")
|
| 25 |
+
plt.savefig(path, dpi=150, bbox_inches="tight")
|
| 26 |
+
img = cv2.imread(path)
|
| 27 |
+
if img is not None:
|
| 28 |
+
# fit to screen-ish width
|
| 29 |
+
h, w = img.shape[:2]
|
| 30 |
+
max_w = 960
|
| 31 |
+
if w > max_w:
|
| 32 |
+
scale = max_w / float(w)
|
| 33 |
+
img = cv2.resize(img, (int(w * scale), int(h * scale)))
|
| 34 |
+
cv2.imshow("Chart (press any key)", img)
|
| 35 |
+
print("[Telearn] 圖表已顯示,按任意鍵關閉…")
|
| 36 |
+
cv2.waitKey(0)
|
| 37 |
+
cv2.destroyAllWindows()
|
| 38 |
+
else:
|
| 39 |
+
print(f"[Telearn] 圖表已儲存: {path}")
|
| 40 |
+
|
| 41 |
def load_csv(filename="coffee_sales_data.csv"):
|
| 42 |
"""安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題"""
|
| 43 |
# === 【學員填空練習點 1:CSV 檔案安全讀取】 ===
|
|
|
|
| 142 |
output_image = "coffee_sales_analysis.png"
|
| 143 |
# 請在下方完成圖表存檔與顯示:
|
| 144 |
plt.savefig(output_image, dpi=300)
|
| 145 |
+
_telearn_plt_show()
|
projects/2026_AI營1/劉宗穎/咖啡店分析圖表/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d1_4_visualization_coffee.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd1_4_visualization_coffee.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/嘴巴偵測/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: step3_mouth_ratio.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'step3_mouth_ratio.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/嘴巴偵測/onepiece(2).jpeg
ADDED
|
Git LFS Details
|
projects/2026_AI營1/劉宗穎/{嘴巴侦测 → 嘴巴偵測}/step3_mouth_ratio.py
RENAMED
|
@@ -1,113 +1,113 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Week 11 - Step 2: Mouth Opening Ratio Detection (Template 填空版)
|
| 3 |
-
=============================================================
|
| 4 |
-
本步驟目標:
|
| 5 |
-
1. 找出特定的臉部特徵點:眼角(33, 263)與嘴唇(13, 14)
|
| 6 |
-
2. 計算兩眼距離,當作臉寬基準比例尺,防止人靠近或遠離鏡頭造成的誤判 (歸一化)
|
| 7 |
-
3. 計算嘴巴張開比例:嘴巴距離 / 兩眼距離
|
| 8 |
-
4. 在 OpenCV 畫面上即時顯示比例與嘴巴開合狀態 (OPEN / CLOSED)
|
| 9 |
-
"""
|
| 10 |
-
import cv2
|
| 11 |
-
import mediapipe as mp
|
| 12 |
-
import math
|
| 13 |
-
import sys
|
| 14 |
-
import os
|
| 15 |
-
|
| 16 |
-
# 1. 初始化攝影機
|
| 17 |
-
cap = cv2.VideoCapture(0)
|
| 18 |
-
if not cap.isOpened():
|
| 19 |
-
print("Error: Could not open camera.")
|
| 20 |
-
sys.exit()
|
| 21 |
-
|
| 22 |
-
# 2. 載入 MediaPipe 臉部偵
|
| 23 |
-
model_path
|
| 24 |
-
if not os.path.exists(model_path):
|
| 25 |
-
print(f"Error: Model file not found at {model_path}")
|
| 26 |
-
sys.exit()
|
| 27 |
-
|
| 28 |
-
try:
|
| 29 |
-
from mediapipe.tasks import python
|
| 30 |
-
from mediapipe.tasks.python import vision
|
| 31 |
-
base_options = python.BaseOptions(model_asset_path=model_path)
|
| 32 |
-
options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1)
|
| 33 |
-
detector = vision.FaceLandmarker.create_from_options(options)
|
| 34 |
-
print("Face Landmarker initialized successfully.")
|
| 35 |
-
except Exception as e:
|
| 36 |
-
print(f"Error: Failed to initialize detector: {e}")
|
| 37 |
-
sys.exit()
|
| 38 |
-
|
| 39 |
-
# 特徵點索引定義
|
| 40 |
-
LEFT_EYE_OUTER = 33
|
| 41 |
-
RIGHT_EYE_OUTER = 263
|
| 42 |
-
LIP_TOP = 13
|
| 43 |
-
LIP_BOTTOM = 14
|
| 44 |
-
|
| 45 |
-
# 嘴巴張開的比例閾值 (超過代表張開)
|
| 46 |
-
THRESHOLD = 0.25
|
| 47 |
-
|
| 48 |
-
print("Press 'q' in the window to exit.")
|
| 49 |
-
|
| 50 |
-
# 3. 主迴圈
|
| 51 |
-
while True:
|
| 52 |
-
success, img = cap.read()
|
| 53 |
-
if not success:
|
| 54 |
-
break
|
| 55 |
-
|
| 56 |
-
img = cv2.flip(img, 1)
|
| 57 |
-
img_h, img_w, _ = img.shape
|
| 58 |
-
|
| 59 |
-
# 影像處理
|
| 60 |
-
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 61 |
-
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
|
| 62 |
-
results = detector.detect(mp_image)
|
| 63 |
-
|
| 64 |
-
if results.face_landmarks:
|
| 65 |
-
face_lms = results.face_landmarks[0]
|
| 66 |
-
|
| 67 |
-
# 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中
|
| 68 |
-
pts = {}
|
| 69 |
-
for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]:
|
| 70 |
-
pt = face_lms[idx]
|
| 71 |
-
pts[idx] = (int(pt.x * img_w), int(pt.y * img_h))
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
# 【TODO 2】計算兩眼外角距離 (做為基準比例尺)
|
| 75 |
-
p_lx, p_ly = pts[LEFT_EYE_OUTER]
|
| 76 |
-
p_rx, p_ry = pts[RIGHT_EYE_OUTER]
|
| 77 |
-
base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
if base_dist > 0:
|
| 81 |
-
# 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」
|
| 82 |
-
p_top_x, p_top_y = pts[LIP_TOP]
|
| 83 |
-
p_bot_x, p_bot_y = pts[LIP_BOTTOM]
|
| 84 |
-
mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y)
|
| 85 |
-
mouth_ratio = mouth_dist / base_dist
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
# 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線
|
| 89 |
-
# 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色)
|
| 90 |
-
is_open = mouth_ratio > THRESHOLD
|
| 91 |
-
cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線
|
| 92 |
-
cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
# 畫出特徵點小黃圓圈
|
| 96 |
-
for idx, pos in pts.items():
|
| 97 |
-
cv2.circle(img, pos, 5, (0, 255, 255), -1)
|
| 98 |
-
|
| 99 |
-
# 顯示比例與開合狀態文字
|
| 100 |
-
cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
| 101 |
-
status_text = "OPEN" if is_open else "CLOSED"
|
| 102 |
-
status_color = (0, 255, 0) if is_open else (0, 0, 255)
|
| 103 |
-
cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2)
|
| 104 |
-
else:
|
| 105 |
-
cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
|
| 106 |
-
|
| 107 |
-
cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img)
|
| 108 |
-
|
| 109 |
-
if cv2.waitKey(1) & 0xFF == ord('q'):
|
| 110 |
-
break
|
| 111 |
-
|
| 112 |
-
cap.release()
|
| 113 |
-
cv2.destroyAllWindows()
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Week 11 - Step 2: Mouth Opening Ratio Detection (Template 填空版)
|
| 3 |
+
=============================================================
|
| 4 |
+
本步驟目標:
|
| 5 |
+
1. 找出特定的臉部特徵點:眼角(33, 263)與嘴唇(13, 14)
|
| 6 |
+
2. 計算兩眼距離,當作臉寬基準比例尺,防止人靠近或遠離鏡頭造成的誤判 (歸一化)
|
| 7 |
+
3. 計算嘴巴張開比例:嘴巴距離 / 兩眼距離
|
| 8 |
+
4. 在 OpenCV 畫面上即時顯示比例與嘴巴開合狀態 (OPEN / CLOSED)
|
| 9 |
+
"""
|
| 10 |
+
import cv2
|
| 11 |
+
import mediapipe as mp
|
| 12 |
+
import math
|
| 13 |
+
import sys
|
| 14 |
+
import os
|
| 15 |
+
|
| 16 |
+
# 1. 初始化攝影機
|
| 17 |
+
cap = cv2.VideoCapture(0)
|
| 18 |
+
if not cap.isOpened():
|
| 19 |
+
print("Error: Could not open camera.")
|
| 20 |
+
sys.exit()
|
| 21 |
+
|
| 22 |
+
# 2. 載入 MediaPipe 臉部偵���器模型 (Face Landmarker)
|
| 23 |
+
model_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task')
|
| 24 |
+
if not os.path.exists(model_path):
|
| 25 |
+
print(f"Error: Model file not found at {model_path}")
|
| 26 |
+
sys.exit()
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
from mediapipe.tasks import python
|
| 30 |
+
from mediapipe.tasks.python import vision
|
| 31 |
+
base_options = python.BaseOptions(model_asset_path=model_path)
|
| 32 |
+
options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1)
|
| 33 |
+
detector = vision.FaceLandmarker.create_from_options(options)
|
| 34 |
+
print("Face Landmarker initialized successfully.")
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"Error: Failed to initialize detector: {e}")
|
| 37 |
+
sys.exit()
|
| 38 |
+
|
| 39 |
+
# 特徵點索引定義
|
| 40 |
+
LEFT_EYE_OUTER = 33
|
| 41 |
+
RIGHT_EYE_OUTER = 263
|
| 42 |
+
LIP_TOP = 13
|
| 43 |
+
LIP_BOTTOM = 14
|
| 44 |
+
|
| 45 |
+
# 嘴巴張開的比例閾值 (超過代表張開)
|
| 46 |
+
THRESHOLD = 0.25
|
| 47 |
+
|
| 48 |
+
print("Press 'q' in the window to exit.")
|
| 49 |
+
|
| 50 |
+
# 3. 主迴圈
|
| 51 |
+
while True:
|
| 52 |
+
success, img = cap.read()
|
| 53 |
+
if not success:
|
| 54 |
+
break
|
| 55 |
+
|
| 56 |
+
img = cv2.flip(img, 1)
|
| 57 |
+
img_h, img_w, _ = img.shape
|
| 58 |
+
|
| 59 |
+
# 影像處理
|
| 60 |
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 61 |
+
mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
|
| 62 |
+
results = detector.detect(mp_image)
|
| 63 |
+
|
| 64 |
+
if results.face_landmarks:
|
| 65 |
+
face_lms = results.face_landmarks[0]
|
| 66 |
+
|
| 67 |
+
# 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中
|
| 68 |
+
pts = {}
|
| 69 |
+
for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]:
|
| 70 |
+
pt = face_lms[idx]
|
| 71 |
+
pts[idx] = (int(pt.x * img_w), int(pt.y * img_h))
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# 【TODO 2】計算兩眼外角距離 (做為基準比例尺)
|
| 75 |
+
p_lx, p_ly = pts[LEFT_EYE_OUTER]
|
| 76 |
+
p_rx, p_ry = pts[RIGHT_EYE_OUTER]
|
| 77 |
+
base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
if base_dist > 0:
|
| 81 |
+
# 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」
|
| 82 |
+
p_top_x, p_top_y = pts[LIP_TOP]
|
| 83 |
+
p_bot_x, p_bot_y = pts[LIP_BOTTOM]
|
| 84 |
+
mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y)
|
| 85 |
+
mouth_ratio = mouth_dist / base_dist
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線
|
| 89 |
+
# 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色)
|
| 90 |
+
is_open = mouth_ratio > THRESHOLD
|
| 91 |
+
cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線
|
| 92 |
+
cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# 畫出特徵點小黃圓圈
|
| 96 |
+
for idx, pos in pts.items():
|
| 97 |
+
cv2.circle(img, pos, 5, (0, 255, 255), -1)
|
| 98 |
+
|
| 99 |
+
# 顯示比例與開合狀態文字
|
| 100 |
+
cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
|
| 101 |
+
status_text = "OPEN" if is_open else "CLOSED"
|
| 102 |
+
status_color = (0, 255, 0) if is_open else (0, 0, 255)
|
| 103 |
+
cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2)
|
| 104 |
+
else:
|
| 105 |
+
cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
|
| 106 |
+
|
| 107 |
+
cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img)
|
| 108 |
+
|
| 109 |
+
if cv2.waitKey(1) & 0xFF == ord('q'):
|
| 110 |
+
break
|
| 111 |
+
|
| 112 |
+
cap.release()
|
| 113 |
+
cv2.destroyAllWindows()
|
projects/2026_AI營1/劉宗穎/{天气资料图表 → 天氣資料圖表}/d1_3_visualization.py
RENAMED
|
@@ -1,87 +1,117 @@
|
|
| 1 |
-
|
| 2 |
-
#
|
| 3 |
-
#
|
| 4 |
-
#
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
import
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
print("
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import matplotlib
|
| 2 |
+
matplotlib.use('Agg') # Telearn hub headless
|
| 3 |
+
# d1_3_visualization.py
|
| 4 |
+
# Day 1: 資料科學與數據視覺化 - Matplotlib 繪圖基礎(學員填空)
|
| 5 |
+
# cd examples/Day1/student && python3 d1_3_visualization.py
|
| 6 |
+
# 參考答案:../Teacher/d1_3_visualization.py
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import matplotlib.pyplot as plt
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _telearn_plt_show(*_a, **_k):
|
| 14 |
+
"""Hub-friendly plt.show: save current figure and display via OpenCV."""
|
| 15 |
+
import os
|
| 16 |
+
import matplotlib.pyplot as plt
|
| 17 |
+
try:
|
| 18 |
+
import cv2
|
| 19 |
+
except ImportError:
|
| 20 |
+
plt.savefig("_telearn_last_plot.png", dpi=150, bbox_inches="tight")
|
| 21 |
+
print("[Telearn] 已儲存 _telearn_last_plot.png(無 cv2 可顯示)")
|
| 22 |
+
return
|
| 23 |
+
path = os.path.join(os.getcwd(), "_telearn_last_plot.png")
|
| 24 |
+
plt.savefig(path, dpi=150, bbox_inches="tight")
|
| 25 |
+
img = cv2.imread(path)
|
| 26 |
+
if img is not None:
|
| 27 |
+
# fit to screen-ish width
|
| 28 |
+
h, w = img.shape[:2]
|
| 29 |
+
max_w = 960
|
| 30 |
+
if w > max_w:
|
| 31 |
+
scale = max_w / float(w)
|
| 32 |
+
img = cv2.resize(img, (int(w * scale), int(h * scale)))
|
| 33 |
+
cv2.imshow("Chart (press any key)", img)
|
| 34 |
+
print("[Telearn] 圖表已顯示,按任意鍵關閉…")
|
| 35 |
+
cv2.waitKey(0)
|
| 36 |
+
cv2.destroyAllWindows()
|
| 37 |
+
else:
|
| 38 |
+
print(f"[Telearn] 圖表已儲存: {path}")
|
| 39 |
+
|
| 40 |
+
def load_csv(filename="taipei_weather_2026.csv"):
|
| 41 |
+
"""安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題"""
|
| 42 |
+
# === 【學員填空練習點 1:CSV 檔案安全讀取】 ===
|
| 43 |
+
# TODO: 1. 使用 os.path.join 將當前腳本所在的目錄與檔案名稱拼接成絕對路徑
|
| 44 |
+
# 提示: 當前腳本目錄為 os.path.dirname(__file__)
|
| 45 |
+
# 2. 使用 os.path.exists 判斷路徑是否存在,若存在則用 pd.read_csv 讀取並回傳
|
| 46 |
+
path = os.path.join(os.path.dirname(__file__), filename)
|
| 47 |
+
|
| 48 |
+
if os.path.exists(path):
|
| 49 |
+
return pd.read_csv(path)
|
| 50 |
+
else:
|
| 51 |
+
print("error: no files found")
|
| 52 |
+
|
| 53 |
+
df = load_csv()
|
| 54 |
+
|
| 55 |
+
# 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊
|
| 56 |
+
plt.rcParams['font.sans-serif'] = [
|
| 57 |
+
'PingFang TC', 'Heiti TC', 'Arial Unicode MS',
|
| 58 |
+
'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei',
|
| 59 |
+
]
|
| 60 |
+
plt.rcParams['axes.unicode_minus'] = False
|
| 61 |
+
|
| 62 |
+
# === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 ===
|
| 63 |
+
# TODO: 使用 plt.subplots 建立一個 2 列 1 行 (2x1) 的子圖畫布,尺寸大小為寬 10 英吋、高 8 英吋
|
| 64 |
+
# 並指派給 fig 與 (ax1, ax2)
|
| 65 |
+
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
|
| 66 |
+
|
| 67 |
+
# 1. 繪製折線圖 (Line Plot) - 每日溫度
|
| 68 |
+
# === 【學員填空練習點 3:繪製每日溫度折線圖】 ===
|
| 69 |
+
# TODO: 1. 在 ax1 畫布上以 ax1.plot() 繪製折線圖,X軸傳入 df['Date'],Y軸傳入 df['Temperature']
|
| 70 |
+
# 設定參數:帶有圓點 marker='o',顏色 color='crimson',圖例 label='溫度 (°C)',線寬 linewidth=2
|
| 71 |
+
# 2. 使用 ax1.set_title 設定標題 "台北觀測站每日溫度趨勢圖"
|
| 72 |
+
# 3. 設定 ax1.set_xlabel("日期") 與 ax1.set_ylabel("數值")
|
| 73 |
+
# 4. 設定網格 ax1.grid(True, linestyle='--', alpha=0.5)
|
| 74 |
+
# 5. 呼叫 ax1.legend() 顯示圖例
|
| 75 |
+
# 6. 設定 X軸刻度旋轉 ax1.tick_params(axis='x', rotation=45)
|
| 76 |
+
#
|
| 77 |
+
# 請在下方完成 ax1 的溫度折線圖繪製:
|
| 78 |
+
ax1.plot(df['Date'], df['Temperature'], marker='o', color= 'crimson',label='温度 (°C)', linewidth=2)
|
| 79 |
+
ax1.set_title("台北观测站每日温度趋势图", fontsize=14, fontweight='bold')
|
| 80 |
+
ax1.set_xlabel("日期")
|
| 81 |
+
|
| 82 |
+
ax1.set_ylabel("数值")
|
| 83 |
+
ax1.grid(True,linestyle="--", alpha=0.5)
|
| 84 |
+
ax1.legend()
|
| 85 |
+
ax1.tick_params(axis='x', rotation=45)
|
| 86 |
+
ax1.plot(df['Date'], df['Humidity'], marker='o', color='lime', label='湿度 (%)', linewidth=2)
|
| 87 |
+
ax1.legend()
|
| 88 |
+
|
| 89 |
+
# 2. 繪製條形圖 (Bar Plot) - 每日降雨量
|
| 90 |
+
# === 【學員填空練習點 4:繪製每日降雨量長條圖】 ===
|
| 91 |
+
# TODO: 1. 在 ax2 畫布上以 ax2.bar() 繪製長條圖,X軸傳入 df['Date'],Y軸傳入 df['Rainfall']
|
| 92 |
+
# 設定參數:顏色 color='skyblue',外框顏色 edgecolor='royalblue',圖例 label='降雨量 (mm)'
|
| 93 |
+
# 2. 使用 ax2.set_title 設定標題 "台北觀測站每日降雨量分佈"
|
| 94 |
+
# 3. 設定 ax2.set_xlabel("日期") 與 ax2.set_ylabel("降雨量 (mm)")
|
| 95 |
+
# 4. 設定 Y 軸網格 ax2.grid(True, axis='y', linestyle='--', alpha=0.5)
|
| 96 |
+
# 5. 呼叫 ax2.legend() 顯示圖例
|
| 97 |
+
# 6. 設定 X軸刻度旋轉 ax2.tick_params(axis='x', rotation=45)
|
| 98 |
+
#
|
| 99 |
+
# 請在下方完成 ax2 的降雨量長條圖繪製:
|
| 100 |
+
ax2.bar(df['Date'], df['Rainfall'], color='turquoise',edgecolor = 'turquoise',label='降雨量 (mm)')
|
| 101 |
+
ax2.set_title("台北每天降雨量长条图", fontsize=14, fontweight='bold')
|
| 102 |
+
ax2.set_xlabel("日期")
|
| 103 |
+
ax2.set_ylabel("降雨量")
|
| 104 |
+
ax2.grid(True, axis='y', linestyle='--', alpha=0.5)
|
| 105 |
+
ax2.legend()
|
| 106 |
+
ax2.tick_params(axis='x', rotation=45)
|
| 107 |
+
|
| 108 |
+
plt.tight_layout() # 自動調整間距,避免文字重疊
|
| 109 |
+
|
| 110 |
+
# === 【學員填空練習點 5:圖表自動儲存與顯示】 ===
|
| 111 |
+
# TODO: 1. 呼叫 plt.savefig 將畫布儲存為檔名 "taipei_weather_2026.png",並設定高畫質 dpi=300
|
| 112 |
+
# (⚠️ 注意:savefig 必須在 show() 之前呼叫)
|
| 113 |
+
# 2. 呼叫 _telearn_plt_show() 將圖表視窗顯示在畫面上
|
| 114 |
+
#
|
| 115 |
+
# 請在下方完成圖表存檔與顯示:
|
| 116 |
+
plt.savefig("taipei_weather_2026.png", dpi=300)
|
| 117 |
+
_telearn_plt_show()
|
projects/2026_AI營1/劉宗穎/天氣資料圖表/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d1_3_visualization.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd1_3_visualization.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/天氣資料圖表/onepiece(2).jpeg
ADDED
|
Git LFS Details
|
projects/2026_AI營1/劉宗穎/{天气资料图表 → 天氣資料圖表}/taipei_weather_2026.csv
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/{影像处理 → 影像處理}/d3_1_opencv_basic.py
RENAMED
|
@@ -1,70 +1,110 @@
|
|
| 1 |
-
# d3_1_opencv_basic.py
|
| 2 |
-
# Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版)
|
| 3 |
-
# 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。
|
| 4 |
-
|
| 5 |
-
import cv2
|
| 6 |
-
import numpy as np
|
| 7 |
-
|
| 8 |
-
# 1. 影像讀取 (Read)
|
| 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 |
-
cv2.
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
cv2.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# d3_1_opencv_basic.py
|
| 2 |
+
# Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版)
|
| 3 |
+
# 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。
|
| 4 |
+
|
| 5 |
+
import cv2
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
# 1. 影像讀取 (Read)
|
| 9 |
+
|
| 10 |
+
def telearn_resolve_image(name):
|
| 11 |
+
"""Resolve image path relative to script, cwd, or student image/ folders."""
|
| 12 |
+
import os
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
here = Path(__file__).resolve().parent
|
| 15 |
+
candidates = [
|
| 16 |
+
Path(name),
|
| 17 |
+
here / name,
|
| 18 |
+
here / "image" / name,
|
| 19 |
+
here / "照片" / name,
|
| 20 |
+
here.parent / "image" / name,
|
| 21 |
+
here.parent / "照片" / name,
|
| 22 |
+
here.parent / "image_create" / name,
|
| 23 |
+
]
|
| 24 |
+
# also try basename-only search one level
|
| 25 |
+
for base in [here, here.parent]:
|
| 26 |
+
for folder in ["image", "照片", "image_create", "处理过的照片"]:
|
| 27 |
+
p = base / folder
|
| 28 |
+
if p.is_dir():
|
| 29 |
+
direct = p / name
|
| 30 |
+
candidates.append(direct)
|
| 31 |
+
# case-insensitive match
|
| 32 |
+
low = name.lower()
|
| 33 |
+
try:
|
| 34 |
+
for f in p.iterdir():
|
| 35 |
+
if f.is_file() and f.name.lower() == low:
|
| 36 |
+
candidates.append(f)
|
| 37 |
+
except OSError:
|
| 38 |
+
pass
|
| 39 |
+
for c in candidates:
|
| 40 |
+
try:
|
| 41 |
+
if c.is_file():
|
| 42 |
+
print(f"[Telearn] 讀取影像: {c}")
|
| 43 |
+
return str(c)
|
| 44 |
+
except OSError:
|
| 45 |
+
pass
|
| 46 |
+
print(f"[Telearn] 找不到影像 {name},搜尋過: {[str(c) for c in candidates[:8]]}…")
|
| 47 |
+
return name
|
| 48 |
+
|
| 49 |
+
img = cv2.imread(telearn_resolve_image("onepiece(2).jpeg"))
|
| 50 |
+
if img is None:
|
| 51 |
+
print("[錯誤] 無法讀取 jpg")
|
| 52 |
+
exit(1)
|
| 53 |
+
|
| 54 |
+
# 預設灰階影像為原始彩色影像,避免未解開註釋時程式出錯
|
| 55 |
+
gray = img
|
| 56 |
+
|
| 57 |
+
# === 【重要觀念 TODO 1:灰階轉換 (Grayscale Conversion)】 ===
|
| 58 |
+
# 觀念重點:大多數電腦視覺演算法在處理單通道(黑白)影像時速度更快。
|
| 59 |
+
# 將 BGR 彩色圖像轉換為單通道灰階圖像,可以減少 2/3 的資料量,且能過濾掉無用的顏色雜訊。
|
| 60 |
+
# 請解開下方註釋 (刪除行首的 #) 並執行,觀察變為灰階後的影像。
|
| 61 |
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| 62 |
+
|
| 63 |
+
# 預設模糊影像為灰階影像,避免未解開註釋時程式出錯
|
| 64 |
+
blurred = gray
|
| 65 |
+
|
| 66 |
+
# === 【重要觀念 TODO 2:高斯模糊平均來平 (Gaussian Blur)】 ===
|
| 67 |
+
# 觀念重點:高斯模糊是透過像素加權滑影像,主要目的是「消除噪點和細微反光」。
|
| 68 |
+
# 這可以避免後續邊緣偵測時把細微的噪輪廓,讓偵測出來的邊緣更加平滑。
|
| 69 |
+
# 請解開下方註釋 (刪除行首的 #) 並執行,觀察影像邊緣的柔焦效果。
|
| 70 |
+
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
|
| 71 |
+
|
| 72 |
+
# 預設邊緣影像為全黑畫布,避免未解開註釋時點誤判為程式出錯
|
| 73 |
+
edges = np.zeros(img.shape[:2], dtype=np.uint8)
|
| 74 |
+
|
| 75 |
+
# === 【重要觀念 TODO 3:Canny 邊緣偵測 (Canny Edge Detection)】 ===
|
| 76 |
+
# 觀念重點:Canny 演算法會找出影像中明暗變化劇烈的分界線。它使用雙閥值法:
|
| 77 |
+
# 高於高閥值(150)的 100% 判定為邊緣;介於高低閥值(50~150)之間的若連接強邊緣則保留;其餘捨棄。
|
| 78 |
+
# 請解開下方註釋 (刪除行首的 #) 並執行,觀察提取出的白色線條輪廓。
|
| 79 |
+
edges = cv2.Canny(blurred, 10, 80)
|
| 80 |
+
|
| 81 |
+
# 預設無偵測到輪廓,且繪製圖像為原始圖像,避免未解開註釋時程式出錯
|
| 82 |
+
contours = []
|
| 83 |
+
contour_img = img.copy()
|
| 84 |
+
|
| 85 |
+
# === 【重要觀念 TODO 4:尋找與繪製輪廓 (Find & Draw Contours)】 ===
|
| 86 |
+
# 觀念重點:`findContours` 會在二值化(邊緣)影像中尋找相連的邊界,並建立幾何結構。
|
| 87 |
+
# `drawContours` 則會將這些幾何結構繪製到指定的影像上(此處使用綠色框繪製於原始彩色影像)。
|
| 88 |
+
# 請解開下方所有註釋 (刪除行首的 #) 並執行,觀察圖形外框被綠線框起來的結果。
|
| 89 |
+
contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 90 |
+
print(f"在影像中偵測到 {len(contours)} 個獨立輪廓特徵。")
|
| 91 |
+
contour_img = img.copy()
|
| 92 |
+
cv2.drawContours(contour_img, contours, -1, (173,216,203), 1)
|
| 93 |
+
alpha=1
|
| 94 |
+
|
| 95 |
+
# === 【重要觀念 TODO 5:視窗顯示與釋放 (Window Display)】 ===
|
| 96 |
+
# 觀念重點:使用 cv2.imshow 顯示各步驟處理結果,並用 waitKey(0) 讓程式停下等待按鍵,
|
| 97 |
+
# 最後使用 destroyAllWindows 釋放視窗資源。
|
| 98 |
+
# 請解開下方所有註釋 (刪除行首的 #),以觀看影像處理的成果!
|
| 99 |
+
print("\n正在顯示影像處理結果(請點擊影像視窗並按下任意鍵關閉)...")
|
| 100 |
+
cv2.imshow("Original Shapes", img)
|
| 101 |
+
cv2.imshow("Grayscale", gray)
|
| 102 |
+
cv2.imshow("Canny Edges", edges)
|
| 103 |
+
cv2.imshow("Contours Output", contour_img)
|
| 104 |
+
|
| 105 |
+
success_gray = cv2.imwrite("output_gray.jpg", gray)
|
| 106 |
+
success_edges = cv2.imwrite("output_edges.jpg", edges)
|
| 107 |
+
success_contour = cv2.imwrite("output_contours.jpg", contour_img)
|
| 108 |
+
|
| 109 |
+
cv2.waitKey(0)
|
| 110 |
+
cv2.destroyAllWindows()
|
projects/2026_AI營1/劉宗穎/影像處理/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d3_1_opencv_basic.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd3_1_opencv_basic.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/影像處理/onepiece(2).jpeg
ADDED
|
Git LFS Details
|
projects/2026_AI營1/劉宗穎/{手指画画游戏 → 手指畫畫遊戲}/draw_game_student.py
RENAMED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import cv2
|
| 2 |
import numpy as np
|
| 3 |
import mediapipe as mp
|
|
@@ -14,7 +15,7 @@ from mediapipe.tasks.python import vision
|
|
| 14 |
# 2. HandLandmarkerOptions 用於設定手部偵測的細項設定,例如 num_hands=1 代表最多偵測一隻手。
|
| 15 |
# 3. 最後透過 vision.HandLandmarker.create_from_options 建立手部偵測器物件。
|
| 16 |
# 請解開下方三行程式碼的註釋:
|
| 17 |
-
base_options = python.BaseOptions(model_asset_path='hand_landmarker.task')
|
| 18 |
options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
|
| 19 |
detector = vision.HandLandmarker.create_from_options(options)
|
| 20 |
|
|
|
|
| 1 |
+
import os
|
| 2 |
import cv2
|
| 3 |
import numpy as np
|
| 4 |
import mediapipe as mp
|
|
|
|
| 15 |
# 2. HandLandmarkerOptions 用於設定手部偵測的細項設定,例如 num_hands=1 代表最多偵測一隻手。
|
| 16 |
# 3. 最後透過 vision.HandLandmarker.create_from_options 建立手部偵測器物件。
|
| 17 |
# 請解開下方三行程式碼的註釋:
|
| 18 |
+
base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hand_landmarker.task'))
|
| 19 |
options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1)
|
| 20 |
detector = vision.HandLandmarker.create_from_options(options)
|
| 21 |
|
projects/2026_AI營1/劉宗穎/手指畫畫遊戲/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: draw_game_student.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'draw_game_student.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/手指畫畫遊戲/onepiece(2).jpeg
ADDED
|
Git LFS Details
|
projects/2026_AI營1/劉宗穎/{爬虫分析 → 爬蟲分析}/d4_1_ex1_books.py
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/{爬虫分析 → 爬蟲分析}/d4_1_ex2_ptt.py
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/{爬虫分析 → 爬蟲分析}/d4_1_ex3_ithelp.py
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/爬蟲分析/main.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""多練習選單入口(自動產生)"""
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import os
|
| 5 |
+
import runpy
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
_ROOT = Path(__file__).resolve().parent
|
| 9 |
+
os.chdir(_ROOT)
|
| 10 |
+
sys.path.insert(0, str(_ROOT))
|
| 11 |
+
|
| 12 |
+
CHOICES = {
|
| 13 |
+
"1": 'd4_1_ex1_books.py',
|
| 14 |
+
"2": 'd4_1_ex2_ptt.py',
|
| 15 |
+
"3": 'd4_1_ex3_ithelp.py',
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
print("=== 本資料夾含多個練習,請選擇 ===")
|
| 19 |
+
print(' 1. d4_1_ex1_books.py')
|
| 20 |
+
print(' 2. d4_1_ex2_ptt.py')
|
| 21 |
+
print(' 3. d4_1_ex3_ithelp.py')
|
| 22 |
+
print(" (直接 Enter 預設 1)")
|
| 23 |
+
sel = input("編號: ").strip() or "1"
|
| 24 |
+
name = CHOICES.get(sel) or CHOICES.get("1")
|
| 25 |
+
print(f"執行: {name}")
|
| 26 |
+
runpy.run_path(str(_ROOT / name), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/爬蟲分析_PTT/d4_1_ex2_ptt.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# d4_1_ex2_ptt.py — Day 4 學員填空
|
| 2 |
+
# cd examples/Day4/student && python d4_1_ex2_ptt.py
|
| 3 |
+
# 參考答案:../d4_1_ex2_ptt.py
|
| 4 |
+
|
| 5 |
+
import requests
|
| 6 |
+
from bs4 import BeautifulSoup
|
| 7 |
+
from urllib.parse import urljoin
|
| 8 |
+
|
| 9 |
+
# 目標網站:PTT Python 討論板首頁
|
| 10 |
+
URL = "https://www.ptt.cc/bbs/Python/index.html"
|
| 11 |
+
|
| 12 |
+
# === 【學員填空練習點 1:模擬瀏覽器 Header】 ===
|
| 13 |
+
# TODO: 填入瀏覽器的 User-Agent,否則 PTT 會拒絕請求並回傳 403 錯誤
|
| 14 |
+
HEADERS = {
|
| 15 |
+
"User-Agent": (
|
| 16 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
| 17 |
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
| 18 |
+
"Chrome/120.0.0.0 Safari/537.36"
|
| 19 |
+
)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
print(f"正在抓取 PTT Python 板最新文章: {URL} ...")
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
response = requests.get(URL, headers=HEADERS, timeout=10)
|
| 26 |
+
response.raise_for_status()
|
| 27 |
+
|
| 28 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
| 29 |
+
|
| 30 |
+
# === 【學員填空練習點 2:定位文章區塊】 ===
|
| 31 |
+
# TODO: 觀察網頁 HTML 結構,找出所有包裹文章資訊的 <div> 標籤(其 class 名稱為何?)
|
| 32 |
+
posts = soup.find_all("div", class_="r-ent")
|
| 33 |
+
|
| 34 |
+
print(f"\n成功取得 {len(posts)} 筆文章項目(含已刪除與置底公告):")
|
| 35 |
+
print("=" * 80)
|
| 36 |
+
|
| 37 |
+
for idx, post in enumerate(posts, 1):
|
| 38 |
+
# === 【學員填空練習點 3:提取文章標題與連結並防錯 (Null Check)】 ===
|
| 39 |
+
# TODO:
|
| 40 |
+
# 1. 尋找 class 為 "title" 的 <div> 標籤
|
| 41 |
+
# 2. 在該 <div> 底下尋找 <a> 標籤 (即文章連結)
|
| 42 |
+
# 3. ⚠️ 重要思考:若文章已刪除,將不會有 <a> 標籤。請撰寫條件判斷:
|
| 43 |
+
# - 若 <a> 標籤存在:提取其文字,並使用 urljoin 拼接 href 屬性得到完整網址。
|
| 44 |
+
# - 若 <a> 標籤不存在:直接將 title_div 的文字作為標題,並將網址設為 "無 (文章已刪除)"。
|
| 45 |
+
title_div = post.find("div", class_="title")
|
| 46 |
+
title_link = title_div.find("a") if title_div else None
|
| 47 |
+
|
| 48 |
+
if title_link:
|
| 49 |
+
title = title_link.get("title")
|
| 50 |
+
relative_url = title_link.get("href")
|
| 51 |
+
post_url = urljoin(URL, relative_url)
|
| 52 |
+
else:
|
| 53 |
+
# 文章已刪除
|
| 54 |
+
title_div = post.find("div", class_="title")
|
| 55 |
+
title = title_div.get_text().strip() if title_div else "(已刪除)"
|
| 56 |
+
post_url = "無 (文章已刪除)"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# === 【學員填空練習點 4:提取推文數】 ===
|
| 60 |
+
# TODO: 尋找 class 為 "nrec" 的 <div> 標籤。
|
| 61 |
+
# 技巧:使用 .get_text().strip() 取得文字。若無文字(0 推),請將變數預設為 "0"。
|
| 62 |
+
nrec_div = post.find("div", class_="nrec")
|
| 63 |
+
nrec = nrec_div.get_text().strip() if nrec_div else "0"
|
| 64 |
+
|
| 65 |
+
# === 【學員填空練習點 5:提取作者與發文日期】 ===
|
| 66 |
+
# TODO: 分別尋找 class 為 "author" 與 "date" 的 <div> 標籤,並提取純文字(記得使用 .strip() 去除空白)
|
| 67 |
+
author = post.find("div", class_="author").get_text().strip()
|
| 68 |
+
date = post.find("div", class_="date").get_text().strip()
|
| 69 |
+
|
| 70 |
+
# 輸出單篇文章資訊
|
| 71 |
+
print(f"[{idx}] 推文數: {nrec:3s} | 發文日期: {date} | 作者: {author}")
|
| 72 |
+
print(f" 標題: {title}")
|
| 73 |
+
print(f" 網址: {post_url}")
|
| 74 |
+
print("-" * 80)
|
| 75 |
+
|
| 76 |
+
except requests.exceptions.HTTPError as http_err:
|
| 77 |
+
print(f"[錯誤] PTT 伺服器拒絕或找不到網頁:{http_err}")
|
| 78 |
+
except requests.exceptions.RequestException as e:
|
| 79 |
+
print(f"[錯誤] 網路請求失敗:{e}")
|
| 80 |
+
except Exception as ex:
|
| 81 |
+
print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}")
|
projects/2026_AI營1/劉宗穎/爬蟲分析_PTT/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d4_1_ex2_ptt.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd4_1_ex2_ptt.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/爬蟲分析_books/d4_1_ex1_books.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# d4_1_ex1_books.py — Day 4 學員填空
|
| 2 |
+
# cd examples/Day4/student && python d4_1_ex1_books.py
|
| 3 |
+
# 參考答案:../d4_1_ex1_books.py
|
| 4 |
+
|
| 5 |
+
import requests
|
| 6 |
+
from bs4 import BeautifulSoup
|
| 7 |
+
from urllib.parse import urljoin
|
| 8 |
+
|
| 9 |
+
# 目標網站:專供爬蟲練習的圖書商城沙盒
|
| 10 |
+
URL = "http://books.toscrape.com/"
|
| 11 |
+
|
| 12 |
+
# === 【學員填空練習點 1:模擬瀏覽器 Header】 ===
|
| 13 |
+
# TODO: 建立 headers 字典,填入 "User-Agent" 來模擬真實瀏覽器(降低被簡易防爬機制拒絕的機率)
|
| 14 |
+
HEADERS = {
|
| 15 |
+
"User-Agent": (
|
| 16 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
| 17 |
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
| 18 |
+
"Chrome/120.0.0.0 Safari/537.36"
|
| 19 |
+
)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
print(f"正在連線至圖書沙盒網站: {URL} ...")
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
# 發送 GET 請求取得網頁 HTML
|
| 26 |
+
response = requests.get(URL, headers=HEADERS, timeout=10)
|
| 27 |
+
|
| 28 |
+
# 檢查 HTTP 狀態碼是否為 200 (OK),非 200 時引發 HTTPError 異常
|
| 29 |
+
response.raise_for_status()
|
| 30 |
+
|
| 31 |
+
# 解決編碼問題:強制將解碼設定為 utf-8 (避免 requests 預設 ISO-8859-1 解碼導致英鎊符號亂碼且在 Windows 印出時崩潰)
|
| 32 |
+
response.encoding = response.apparent_encoding
|
| 33 |
+
|
| 34 |
+
print("連線成功!開始解析書籍資料...\n")
|
| 35 |
+
|
| 36 |
+
# 初始化 BeautifulSoup 解析器
|
| 37 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
| 38 |
+
|
| 39 |
+
# === 【學員填空練習點 2:定位 HTML 標籤區塊】 ===
|
| 40 |
+
# TODO: 找出網頁中所有書籍的容器。每本書都被包在一個 <article> 標籤中,且該標籤的 class 固定為 "product_pod"
|
| 41 |
+
books = soup.find_all("article", class_="product_pod")
|
| 42 |
+
|
| 43 |
+
print(f"成功在本頁找到 {len(books)} 本書籍:")
|
| 44 |
+
print("=" * 70)
|
| 45 |
+
|
| 46 |
+
for idx, book in enumerate(books, 1):
|
| 47 |
+
# === 【學員填空練習點 3:提取書籍名稱與連結】 ===
|
| 48 |
+
# TODO:
|
| 49 |
+
# 1. 尋找 <h3> 底下的 <a> 標籤
|
| 50 |
+
# 2. 取得 <a> 的 "title" 屬性(因為畫面上長度限制,.get_text() 可能會被 ellipsis 截斷)
|
| 51 |
+
# 3. 取得 <a> 的 "href" 屬性並使用 urljoin 拼接成完整網址
|
| 52 |
+
title_link = book.find("h3").find("a")
|
| 53 |
+
title = title_link.get("title") # 取得屬性 'title' 的值,避免拿到被截斷的文字
|
| 54 |
+
|
| 55 |
+
relative_url = title_link.get("href")
|
| 56 |
+
detail_url = urljoin(URL, relative_url)
|
| 57 |
+
|
| 58 |
+
# === 【學員填空練習點 4:提取書籍價格】 ===
|
| 59 |
+
# 提示:為了避免 Windows (CP950) 終端機不支援英鎊符號 '£' 而編碼崩潰,請加上 .replace("£", "GBP") 將其替換
|
| 60 |
+
# 觀察:價格存放在 class 為 "price_color" 的 <p> 標籤中,格式如 "£51.77"
|
| 61 |
+
# 技巧:將英鎊符號 '£' 替換為 'GBP',避免在 Windows (CP950) 終端機印出時因不支援該字元而崩潰
|
| 62 |
+
price_text = book.find("p", class_="price_color").get_text().replace("£", "GBP")
|
| 63 |
+
|
| 64 |
+
# === 【學員填空練習點 5:提取評價星等 (Rating)】 ===
|
| 65 |
+
# TODO:
|
| 66 |
+
# 1. 尋找 class 為 "star-rating" 的 <p> 標籤
|
| 67 |
+
# 2. 取得該標籤的 "class" 屬性值(這會是一個串列,如 ['star-rating', 'Three'])
|
| 68 |
+
# 3. 提取該串列的第二個元素作為星等字串 (若存在)
|
| 69 |
+
|
| 70 |
+
rating_tag = book.find("p", class_="star-rating")
|
| 71 |
+
rating_classes = rating_tag.get("class")
|
| 72 |
+
rating = "無"
|
| 73 |
+
if len(rating_classes) > 1:
|
| 74 |
+
rating = rating_classes[1] # 提取例如 'Three' (代表三顆星)
|
| 75 |
+
|
| 76 |
+
# === 【學員填空練習點 6:提取庫存狀態】 ===
|
| 77 |
+
# TODO: 尋找 class 為 "instock availability" 的 <p> 標籤,取得文字後去除首尾空白 (.strip())
|
| 78 |
+
stock_text = book.find("p", class_="instock availability").get_text().strip()
|
| 79 |
+
|
| 80 |
+
# 印出解析結果
|
| 81 |
+
print(f"【編號 {idx}】")
|
| 82 |
+
print(f"[書名] {title}")
|
| 83 |
+
print(f"[價格] {price_text}")
|
| 84 |
+
print(f"[評價] {rating} 星")
|
| 85 |
+
print(f"[狀態] {stock_text}")
|
| 86 |
+
print(f"[連結] {detail_url}")
|
| 87 |
+
print("-" * 70)
|
| 88 |
+
|
| 89 |
+
except requests.exceptions.HTTPError as http_err:
|
| 90 |
+
print(f"[錯誤] HTTP 連線異常:{http_err}")
|
| 91 |
+
except requests.exceptions.RequestException as e:
|
| 92 |
+
print(f"[錯誤] 網路請求失敗:{e}")
|
| 93 |
+
except Exception as ex:
|
| 94 |
+
print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}")
|
projects/2026_AI營1/劉宗穎/爬蟲分析_books/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d4_1_ex1_books.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd4_1_ex1_books.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/爬蟲分析_ithelp/d4_1_ex3_ithelp.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# d4_1_ex3_ithelp.py — Day 4 學員填空
|
| 2 |
+
# cd examples/Day4/student && python d4_1_ex3_ithelp.py
|
| 3 |
+
# 參考答案:../d4_1_ex3_ithelp.py
|
| 4 |
+
|
| 5 |
+
import requests
|
| 6 |
+
from bs4 import BeautifulSoup
|
| 7 |
+
|
| 8 |
+
# 目標網站:iT 邦幫忙技術文章首頁
|
| 9 |
+
URL = "https://ithelp.ithome.com.tw/articles?tab=tech"
|
| 10 |
+
|
| 11 |
+
# === 【學員填空練習點 1:模擬瀏覽器 Header】 ===
|
| 12 |
+
# TODO: 填入瀏覽器的 User-Agent,否則可能被伺服器拒絕連線
|
| 13 |
+
HEADERS = {
|
| 14 |
+
"User-Agent": (
|
| 15 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
| 16 |
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
| 17 |
+
"Chrome/120.0.0.0 Safari/537.36"
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
print(f"正在連線至 iT 邦幫忙並抓取技術文章: {URL} ...")
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
response = requests.get(URL, headers=HEADERS, timeout=10)
|
| 25 |
+
response.raise_for_status()
|
| 26 |
+
|
| 27 |
+
print("連線成功!解析文章列表中...\n")
|
| 28 |
+
soup = BeautifulSoup(response.text, "html.parser")
|
| 29 |
+
|
| 30 |
+
# === 【學員填空練習點 2:定位文章卡片區塊】 ===
|
| 31 |
+
# TODO: 尋找所有包裹文章資訊的 <div> 標籤(其 class 為 "qa-list")
|
| 32 |
+
articles = soup.find_all("div", class_="qa-list")
|
| 33 |
+
|
| 34 |
+
print(f"成功取得前 {len(articles)} 篇熱門技術文章:")
|
| 35 |
+
print("=" * 80)
|
| 36 |
+
|
| 37 |
+
for idx, art in enumerate(articles, 1):
|
| 38 |
+
# === 【學員填空練習點 3:提取標題與超連結】 ===
|
| 39 |
+
# TODO:
|
| 40 |
+
# 1. 在 art 中尋找 class 為 "qa-list__title-link" 的 <a> 標籤
|
| 41 |
+
# 2. 提取其純文字內容並去除首尾空白 (.strip()),作為標題
|
| 42 |
+
# 3. 提取其 "href" 屬性,作為文章連結
|
| 43 |
+
title_link = art.find("a", class_="qa-list__title-link")
|
| 44 |
+
if not title_link:
|
| 45 |
+
continue
|
| 46 |
+
title = title_link.get_text().strip()
|
| 47 |
+
post_url = title_link.get("href").strip()
|
| 48 |
+
|
| 49 |
+
# === 【學員填空練習點 4:提取作者名稱】 ===
|
| 50 |
+
# TODO:
|
| 51 |
+
# 1. 尋找 class 為 "qa-list__info" 的 <div> 標籤
|
| 52 |
+
# 2. 在該 <div> 下尋找 class 為 "qa-list__info-link" 的 <a> 標籤
|
| 53 |
+
# 3. 提取其文字內容作為作者名稱
|
| 54 |
+
info_div = art.find("div", class_="qa-list__info")
|
| 55 |
+
author = "未知作者"
|
| 56 |
+
if info_div:
|
| 57 |
+
author_link = info_div.find("a", class_="qa-list__info-link")
|
| 58 |
+
if author_link:
|
| 59 |
+
author = author_link.get_text().strip()
|
| 60 |
+
|
| 61 |
+
# === 【學員填空練習點 5:提取發文時間】 ===
|
| 62 |
+
# TODO: 尋找 class 為 "qa-list__info-time" 的 <a> 標籤,並提取其 "title" 屬性值(因 title 屬性中包含完整時間秒數)
|
| 63 |
+
time_link = art.find("div", class_="qa-list__info-time")
|
| 64 |
+
post_time = time_link.get("title") if time_link else "未知時間"
|
| 65 |
+
|
| 66 |
+
# === 【學員填空練習點 6:利用 List 索引提取多項數據】 ===
|
| 67 |
+
# TODO:
|
| 68 |
+
# 1. 使用 find_all 找出卡片中所有 class 為 "qa-condition" 的 <a> 標籤
|
| 69 |
+
# 2. 當抓取到的數量大於等於 3 時,利用 List 索引取得對應的資訊:
|
| 70 |
+
# - 索引 0 的項目底下的 "qa-condition__count" <span>:Like 數
|
| 71 |
+
# - 索引 1 的項目底下的 "qa-condition__count" <span>:回應數
|
| 72 |
+
# - 索引 2 的項目底下的 "qa-condition__count" <span>:瀏覽數
|
| 73 |
+
conditions = art.find_all("a", class_="qa-condition")
|
| 74 |
+
|
| 75 |
+
likes = "0"
|
| 76 |
+
comments = "0"
|
| 77 |
+
views = "0"
|
| 78 |
+
|
| 79 |
+
if len(conditions) >= 3:
|
| 80 |
+
# 請在此處填入利用 conditions[0]、conditions[1]、conditions[2] 尋找 span 的代碼
|
| 81 |
+
likes = conditions[0].find("span", class_="qa-condition__count").get_text().strip()
|
| 82 |
+
comments = conditions[1].find("span", class_="qa-condition__count").get_text().strip()
|
| 83 |
+
views = conditions[2].find("span", class_="qa-condition__count").get_text().strip()
|
| 84 |
+
|
| 85 |
+
# 格式化輸出
|
| 86 |
+
print(f"【排名 {idx}】")
|
| 87 |
+
print(f"[標題] {title}")
|
| 88 |
+
print(f"[作者] {author} | [時間] {post_time}")
|
| 89 |
+
print(f"[數據] Like: {likes} | 回應: {comments} | 瀏覽/訂閱: {views}")
|
| 90 |
+
print(f"[連結] {post_url}")
|
| 91 |
+
print("-" * 80)
|
| 92 |
+
|
| 93 |
+
except requests.exceptions.HTTPError as http_err:
|
| 94 |
+
print(f"[錯誤] 伺服器回應異常:{http_err}")
|
| 95 |
+
except requests.exceptions.RequestException as e:
|
| 96 |
+
print(f"[錯誤] 網路請求失敗:{e}")
|
| 97 |
+
except Exception as ex:
|
| 98 |
+
print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}")
|
projects/2026_AI營1/劉宗穎/爬蟲分析_ithelp/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d4_1_ex3_ithelp.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd4_1_ex3_ithelp.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/{爬虫分析与AI报告 → 爬蟲分析與AI報告}/d4_1_web_scraping.py
RENAMED
|
File without changes
|
projects/2026_AI營1/劉宗穎/{爬虫分析与AI报告 → 爬蟲分析與AI報告}/d4_2_agent_task_chain.py
RENAMED
|
@@ -7,8 +7,39 @@ import os
|
|
| 7 |
import requests
|
| 8 |
from bs4 import BeautifulSoup
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite")
|
| 11 |
-
api_key =
|
| 12 |
|
| 13 |
HEADERS = {
|
| 14 |
"User-Agent": (
|
|
@@ -96,7 +127,7 @@ def run_task_chain(topic):
|
|
| 96 |
f"- 模擬數據:\"Love is all, love is new.\" -- John Lennon\n\n"
|
| 97 |
f"## 二、 核心思想分析\n"
|
| 98 |
f"本機模擬分析指出,此主題的語錄主要著眼於人與人之間的連結與心靈啟發。\n"
|
| 99 |
-
f"*(注意:此為 Mock 離線生成報告,
|
| 100 |
)
|
| 101 |
else:
|
| 102 |
print(f"\n[步驟 2] 正在調用 Gemini ({GEMINI_MODEL}) 進行深度分析與排版...")
|
|
|
|
| 7 |
import requests
|
| 8 |
from bs4 import BeautifulSoup
|
| 9 |
|
| 10 |
+
|
| 11 |
+
def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str:
|
| 12 |
+
"""終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。"""
|
| 13 |
+
import os
|
| 14 |
+
key = (os.environ.get("GEMINI_API_KEY") or "").strip()
|
| 15 |
+
if key:
|
| 16 |
+
print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY")
|
| 17 |
+
return key
|
| 18 |
+
print()
|
| 19 |
+
print("=" * 54)
|
| 20 |
+
print(" 本專案需要 Google Gemini API Key")
|
| 21 |
+
print(" 取得金鑰:https://aistudio.google.com/apikey")
|
| 22 |
+
print(" 請在下方貼上 Key 後按 Enter")
|
| 23 |
+
print(" (僅本次執行使用,不會寫入任何檔案)")
|
| 24 |
+
if allow_skip:
|
| 25 |
+
print(" 若要改用離線 Mock 模式:直接按 Enter 略過")
|
| 26 |
+
print("=" * 54)
|
| 27 |
+
try:
|
| 28 |
+
key = input("請輸入 GEMINI_API_KEY: ").strip()
|
| 29 |
+
except EOFError:
|
| 30 |
+
key = ""
|
| 31 |
+
if key:
|
| 32 |
+
os.environ["GEMINI_API_KEY"] = key
|
| 33 |
+
print("[Telearn] 已接收 API Key,開始連線…")
|
| 34 |
+
return key
|
| 35 |
+
if allow_skip:
|
| 36 |
+
print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能")
|
| 37 |
+
return ""
|
| 38 |
+
print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。")
|
| 39 |
+
raise SystemExit(0)
|
| 40 |
+
|
| 41 |
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite")
|
| 42 |
+
api_key = _telearn_require_gemini_api_key(allow_skip=True)
|
| 43 |
|
| 44 |
HEADERS = {
|
| 45 |
"User-Agent": (
|
|
|
|
| 127 |
f"- 模擬數據:\"Love is all, love is new.\" -- John Lennon\n\n"
|
| 128 |
f"## 二、 核心思想分析\n"
|
| 129 |
f"本機模擬分析指出,此主題的語錄主要著眼於人與人之間的連結與心靈啟發。\n"
|
| 130 |
+
f"*(注意:此為 Mock 離線生成報告,於啟動時輸入 GEMINI_API_KEY 以啟用 Gemini 真實生成。)*\n"
|
| 131 |
)
|
| 132 |
else:
|
| 133 |
print(f"\n[步驟 2] 正在調用 Gemini ({GEMINI_MODEL}) 進行深度分析與排版...")
|
projects/2026_AI營1/劉宗穎/{爬虫分析与AI报告 → 爬蟲分析與AI報告}/d4_2_ex2_crawler_ai.py
RENAMED
|
@@ -9,8 +9,39 @@ from urllib.parse import urljoin
|
|
| 9 |
from google import genai
|
| 10 |
|
| 11 |
# 設定預設使用的 Gemini 模型
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite")
|
| 13 |
-
api_key =
|
| 14 |
|
| 15 |
HEADERS = {
|
| 16 |
"User-Agent": (
|
|
@@ -176,7 +207,7 @@ def run_analysis_chain(source_type):
|
|
| 176 |
|
| 177 |
# 步驟 2: AI 分析 (或 Mock 降級防禦)
|
| 178 |
if not api_key:
|
| 179 |
-
print("\n[步驟 2] 未
|
| 180 |
# === 【學員填空練習點 5:Mock 降級防衛機制】 ===
|
| 181 |
# TODO 5: 請設計一段模擬的 Markdown 格式分析報告字串,指派給 report_content。
|
| 182 |
# 報告內容至少需要包含標題與數據摘要,避免程式在無 Key 時報錯中斷。
|
|
|
|
| 9 |
from google import genai
|
| 10 |
|
| 11 |
# 設定預設使用的 Gemini 模型
|
| 12 |
+
|
| 13 |
+
def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str:
|
| 14 |
+
"""終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。"""
|
| 15 |
+
import os
|
| 16 |
+
key = (os.environ.get("GEMINI_API_KEY") or "").strip()
|
| 17 |
+
if key:
|
| 18 |
+
print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY")
|
| 19 |
+
return key
|
| 20 |
+
print()
|
| 21 |
+
print("=" * 54)
|
| 22 |
+
print(" 本專案需要 Google Gemini API Key")
|
| 23 |
+
print(" 取得金鑰:https://aistudio.google.com/apikey")
|
| 24 |
+
print(" 請在下方貼上 Key 後按 Enter")
|
| 25 |
+
print(" (僅本次執行使用,不會寫入任何檔案)")
|
| 26 |
+
if allow_skip:
|
| 27 |
+
print(" 若要改用離線 Mock 模式:直接按 Enter 略過")
|
| 28 |
+
print("=" * 54)
|
| 29 |
+
try:
|
| 30 |
+
key = input("請輸入 GEMINI_API_KEY: ").strip()
|
| 31 |
+
except EOFError:
|
| 32 |
+
key = ""
|
| 33 |
+
if key:
|
| 34 |
+
os.environ["GEMINI_API_KEY"] = key
|
| 35 |
+
print("[Telearn] 已接收 API Key,開始連線…")
|
| 36 |
+
return key
|
| 37 |
+
if allow_skip:
|
| 38 |
+
print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能")
|
| 39 |
+
return ""
|
| 40 |
+
print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。")
|
| 41 |
+
raise SystemExit(0)
|
| 42 |
+
|
| 43 |
GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite")
|
| 44 |
+
api_key = _telearn_require_gemini_api_key(allow_skip=True)
|
| 45 |
|
| 46 |
HEADERS = {
|
| 47 |
"User-Agent": (
|
|
|
|
| 207 |
|
| 208 |
# 步驟 2: AI 分析 (或 Mock 降級防禦)
|
| 209 |
if not api_key:
|
| 210 |
+
print("\n[步驟 2] 未輸入 GEMINI_API_KEY,啟動 Mock 離線降級機制...")
|
| 211 |
# === 【學員填空練習點 5:Mock 降級防衛機制】 ===
|
| 212 |
# TODO 5: 請設計一段模擬的 Markdown 格式分析報告字串,指派給 report_content。
|
| 213 |
# 報告內容至少需要包含標題與數據摘要,避免程式在無 Key 時報錯中斷。
|
projects/2026_AI營1/劉宗穎/爬蟲分析與AI報告/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: d4_2_ex2_crawler_ai.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'd4_2_ex2_crawler_ai.py'), run_name="__main__")
|
projects/2026_AI營1/劉宗穎/眼睛距離偵測/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Telearn Arcade Hub 入口檔(自動產生)
|
| 4 |
+
實際執行: step0_eyeguard_template.py
|
| 5 |
+
規格: projects/{班級}/{學生}/{專案}/main.py
|
| 6 |
+
"""
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import os
|
| 9 |
+
import runpy
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
_ROOT = Path(__file__).resolve().parent
|
| 13 |
+
os.chdir(_ROOT)
|
| 14 |
+
if str(_ROOT) not in sys.path:
|
| 15 |
+
sys.path.insert(0, str(_ROOT))
|
| 16 |
+
|
| 17 |
+
# 執行原始學員腳本
|
| 18 |
+
runpy.run_path(str(_ROOT / 'step0_eyeguard_template.py'), run_name="__main__")
|