diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\273\203\344\271\240/d1_1_pandas_intro.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" similarity index 90% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\273\203\344\271\240/d1_1_pandas_intro.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" index 8498f11a8b54d718d72b66f1c86f0ddb12979161..c8d0425f2a153860cb90a5da6989c7c637560f3b 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\273\203\344\271\240/d1_1_pandas_intro.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" @@ -1,37 +1,38 @@ -# d1_1_pandas_intro.py -# Day 1 學員填空:Pandas 基礎(完成 TODO 後執行) -# cd examples/Day1/student && python3 d1_1_pandas_intro.py -# 參考答案:../Teacher/d1_1_pandas_intro.py - -import pandas as pd - -df = pd.read_csv("taipei_weather_2026.csv") -print("=== 數據載入成功! ===") - -print("\n--- 1. head ---") -print(df.head()) - -print("\n--- 2. info ---") -df.info() - -print("\n--- 3. describe ---") -print(df.describe()) - -print("\n--- 4. Temperature 前 5 筆 ---") -print(df['Temperature'].head()) - -print("\n--- 5. Date 與 Rainfall ---") -print(df[['Rainfall', 'Date']].head()) - -print("\n--- 挑戰:最高溫與平均濕度 ---") -max_temp = df['Temperature'].max() -avg_humidity = df['Humidity'].mean() -min_rainfall = df['Rainfall'].min() -print(f"最高溫度:{max_temp}°C") -print(f"平均濕度:{avg_humidity:.2f}%") -print(f"最低降雨:{min_rainfall:.2f}%") - -print("\n--- whether it is good for egyptians") - -egy = df[(df['Temperature'] > 27) & (df['Rainfall'] == 0)] +import os +# d1_1_pandas_intro.py +# Day 1 學員填空:Pandas 基礎(完成 TODO 後執行) +# cd examples/Day1/student && python3 d1_1_pandas_intro.py +# 參考答案:../Teacher/d1_1_pandas_intro.py + +import pandas as pd + +df = pd.read_csv(os.path.join(os.path.dirname(__file__), "taipei_weather_2026.csv")) +print("=== 數據載入成功! ===") + +print("\n--- 1. head ---") +print(df.head()) + +print("\n--- 2. info ---") +df.info() + +print("\n--- 3. describe ---") +print(df.describe()) + +print("\n--- 4. Temperature 前 5 筆 ---") +print(df['Temperature'].head()) + +print("\n--- 5. Date 與 Rainfall ---") +print(df[['Rainfall', 'Date']].head()) + +print("\n--- 挑戰:最高溫與平均濕度 ---") +max_temp = df['Temperature'].max() +avg_humidity = df['Humidity'].mean() +min_rainfall = df['Rainfall'].min() +print(f"最高溫度:{max_temp}°C") +print(f"平均濕度:{avg_humidity:.2f}%") +print(f"最低降雨:{min_rainfall:.2f}%") + +print("\n--- whether it is good for egyptians") + +egy = df[(df['Temperature'] > 27) & (df['Rainfall'] == 0)] print(egy) \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..19029aca2c352f92bc7a6ace63c7161bf8b10e90 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_1_pandas_intro.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_1_pandas_intro.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\273\203\344\271\240/taipei_weather_2026.csv" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/taipei_weather_2026.csv" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\273\203\344\271\240/taipei_weather_2026.csv" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/taipei_weather_2026.csv" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/ai_api\344\270\262\346\216\245\347\273\203\344\271\240/d2_1_gemini.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" similarity index 55% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/ai_api\344\270\262\346\216\245\347\273\203\344\271\240/d2_1_gemini.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" index 7f56d7320a3e87a1fe7221075233544b5f31dec1..e70b84af1726248b15e9010e751c6614ef9bcfa6 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/ai_api\344\270\262\346\216\245\347\273\203\344\271\240/d2_1_gemini.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" @@ -1,41 +1,63 @@ -# d2_1_gemini.py — Day2 學員填空 -# cd examples/Day2/Student && python3 d2_1_gemini.py -# 參考答案:../Teacher/d2_1_gemini.py - -import os -from google import genai - -# === 【學員填空練習點 1:設置與載入 API Key】 === -# TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。 -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6LgriztRWiwQOdkK1wXmmfs4AzlZKSQsP2X0cnjzaMCCg") - -if not api_key: - print("[提示] 未檢測到環境變數 GEMINI_API_KEY。") - print(" 請先設定:export GEMINI_API_KEY=\"your_key\"") - print(" 或於下方手動輸入(僅供本機測試,勿寫死在檔案中上傳):") - api_key = input("API Key: ").strip() - if not api_key: - print("[結束] 未提供 API Key。可改練習 d2_1_ollama_local.py。") - exit(0) - -# === 【學員填空練習點 2:設定模型名稱】 === -# TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite') -model_name = "gemini-3.1-flash-lite" - -print(f"正在連線至 Google Cloud Model ({model_name})...") - -# === 【學員填空練習點 3:初始化 Gemini Client】 === -# TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數 -client = genai.Client(api_key=api_key) - -# === 【學員填空練習點 4:發送內容生成請求】 === -# TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞 -# 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。 -response = client.models.generate_content(model=model_name, contents="请用中文解释为什么泡面很好吃,字数一百字以内") -# === 【學員填空練習點 5:讀取並印出回應】 === -# TODO: 從 response 物件中取得 text 欄位內容,並列印出來。 -print("\n=== AI 回應結果 ===") -# 請在下方實作列印邏輯: -print(response.text) -# for i in range(10): +# d2_1_gemini.py — Day2 學員填空 +# cd examples/Day2/Student && python3 d2_1_gemini.py +# 參考答案:../Teacher/d2_1_gemini.py + +import os +from google import genai + +# === 【學員填空練習點 1:設置與載入 API Key】 === +# TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +api_key = _telearn_require_gemini_api_key(allow_skip=False) + +# === 【學員填空練習點 2:設定模型名稱】 === +# TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite') +model_name = "gemini-3.1-flash-lite" + +print(f"正在連線至 Google Cloud Model ({model_name})...") + +# === 【學員填空練習點 3:初始化 Gemini Client】 === +# TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數 +client = genai.Client(api_key=api_key) + +# === 【學員填空練習點 4:發送內容生成請求】 === +# TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞 +# 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。 +response = client.models.generate_content(model=model_name, contents="请用中文解释为什么泡面很好吃,字数一百字以内") +# === 【學員填空練習點 5:讀取並印出回應】 === +# TODO: 從 response 物件中取得 text 欄位內容,並列印出來。 +print("\n=== AI 回應結果 ===") +# 請在下方實作列印邏輯: +print(response.text) +# for i in range(10): # print(response.text) \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..9c7cdcc41dc129b0848a190aa8fb9c425c2c376c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_1_gemini.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_1_gemini.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\345\257\206\345\256\244\351\200\203\350\204\261\346\270\270\346\210\217/d2_3_chatbot_memory_game_template.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/d2_3_chatbot_memory_game_template.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\345\257\206\345\256\244\351\200\203\350\204\261\346\270\270\346\210\217/d2_3_chatbot_memory_game_template.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/d2_3_chatbot_memory_game_template.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..2d6d70db2b75c24d81bbc745f45d31d25e02825f --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_3_chatbot_memory_game_template.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_3_chatbot_memory_game_template.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\345\205\263\351\224\256\350\257\215\346\212\223\345\217\226/d2_3_chatbot_memory_mock.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/d2_3_chatbot_memory_mock.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\345\205\263\351\224\256\350\257\215\346\212\223\345\217\226/d2_3_chatbot_memory_mock.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/d2_3_chatbot_memory_mock.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a7ff00b684e34d0c68bab0adcd53df7f1e52fef4 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_3_chatbot_memory_mock.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_3_chatbot_memory_mock.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock&AI_api\345\217\214\346\250\241\345\274\217/d2_3_chatbot_memory.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_3_chatbot_memory.py" similarity index 69% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock&AI_api\345\217\214\346\250\241\345\274\217/d2_3_chatbot_memory.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_3_chatbot_memory.py" index 32ebc5bf66fa42e8ea1e31329897c8fe436f73c5..c460a9906e36c30690fcf917363a7226944cdf80 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock&AI_api\345\217\214\346\250\241\345\274\217/d2_3_chatbot_memory.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_3_chatbot_memory.py" @@ -1,95 +1,124 @@ -# d2_3_chatbot_memory.py -# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模) - -import os -from google import genai - -# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite -GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get( - "GEMINI_API_KEY", "") - - -def local_mock_response(user_input, history): - user_input_lower = user_input.lower() - - # TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句 - if "營業時間" in user_input_lower or "時間" in user_input_lower: - return "我們觀測站的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" - # elif "XX" in user_input_lower or "XX" in user_input_lower: - # return "XXXXXXXXXXXXXXXXX" - - - else: - turns = len(history) // 2 + 1 - return ( - f"(模擬模式)收到您的訊息:『{user_input}』。" - f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?" - ) - - -def main(): - print("=" * 50) - print(" 得能 AI 智慧客服對話系統 (支援對話上下文記憶)") - print("=" * 50) - - use_mock = False - chat_session = None - - if not api_key: - print("[提示] 未檢測到環境變數 GEMINI_API_KEY,將自動啟用「離線模擬模式 (Local Mock Mode)」。") - print(" (如需連線雲端模型,請設定 GEMINI_API_KEY 環境變數)") - use_mock = True - else: - try: - print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...") - client = genai.Client(api_key=api_key) - # TODO: 設定系統指令 (System Instruction / System Prompt) - # system_instruction = ( - # "你是一位專業的得能教育客服人員,語氣溫和客氣," - # "XXXXXXXXXXXXXXXXXXXXXXX" - # ) - - chat_session = client.chats.create( - model=GEMINI_MODEL, - config={'system_instruction': system_instruction} - ) - print("雲端客服系統已就緒!") - except Exception as e: - print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...") - use_mock = True - - local_history = [] - - print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):") - print("-" * 50) - - while True: - try: - user_input = input("\n[您]: ").strip() - if not user_input: - continue - - if user_input.lower() in ["quit", "exit"]: - print("對話結束,感謝您的使用!") - break - - if use_mock: - ai_reply = local_mock_response(user_input, local_history) - local_history.append({"role": "user", "text": user_input}) - local_history.append({"role": "model", "text": ai_reply}) - else: - response = chat_session.send_message(user_input) - ai_reply = response.text - - print(f"[AI 客服]: {ai_reply}") - - except KeyboardInterrupt: - print("\n對話強行終止。再見!") - break - except Exception as e: - print(f"\n[錯誤] 發生異常:{e}") - - -if __name__ == "__main__": - main() +# d2_3_chatbot_memory.py +# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模) + +import os +from google import genai + +# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") +api_key = _telearn_require_gemini_api_key(allow_skip=True) + + +def local_mock_response(user_input, history): + user_input_lower = user_input.lower() + + # TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句 + if "營業時間" in user_input_lower or "時間" in user_input_lower: + return "我們觀測站的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" + # elif "XX" in user_input_lower or "XX" in user_input_lower: + # return "XXXXXXXXXXXXXXXXX" + + + else: + turns = len(history) // 2 + 1 + return ( + f"(模擬模式)收到您的訊息:『{user_input}』。" + f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?" + ) + + +def main(): + print("=" * 50) + print(" 得能 AI 智慧客服對話系統 (支援對話上下文記憶)") + print("=" * 50) + + use_mock = False + chat_session = None + + if not api_key: + print("[提示] 未輸入 GEMINI_API_KEY,改用「離線模擬模式 (Local Mock Mode)」。") + use_mock = True + else: + try: + print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...") + client = genai.Client(api_key=api_key) + # TODO: 設定系統指令 (System Instruction / System Prompt) + # system_instruction = ( + # "你是一位專業的得能教育客服人員,語氣溫和客氣," + # "XXXXXXXXXXXXXXXXXXXXXXX" + # ) + + chat_session = client.chats.create( + model=GEMINI_MODEL, + config={'system_instruction': system_instruction} + ) + print("雲端客服系統已就緒!") + except Exception as e: + print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...") + use_mock = True + + local_history = [] + + print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):") + print("-" * 50) + + while True: + try: + user_input = input("\n[您]: ").strip() + if not user_input: + continue + + if user_input.lower() in ["quit", "exit"]: + print("對話結束,感謝您的使用!") + break + + if use_mock: + ai_reply = local_mock_response(user_input, local_history) + local_history.append({"role": "user", "text": user_input}) + local_history.append({"role": "model", "text": ai_reply}) + else: + response = chat_session.send_message(user_input) + ai_reply = response.text + + print(f"[AI 客服]: {ai_reply}") + + except KeyboardInterrupt: + print("\n對話強行終止。再見!") + break + except Exception as e: + print(f"\n[錯誤] 發生異常:{e}") + + +if __name__ == "__main__": + main() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock&AI_api\345\217\214\346\250\241\345\274\217/d2_4_chatbot_memory.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" similarity index 85% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock&AI_api\345\217\214\346\250\241\345\274\217/d2_4_chatbot_memory.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" index 20ff7a1187c6062f5d0f30f195ecabbbd1bb3e85..326c8f1196b926c931f4689ede381cd1dee6b82b 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock&AI_api\345\217\214\346\250\241\345\274\217/d2_4_chatbot_memory.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" @@ -1,129 +1,158 @@ -# d2_3_chatbot_memory.py -# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模) - -import os -from google import genai - -# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite -GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get( - "GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") - - -def local_mock_response(user_input, history): - user_input_lower = user_input.lower() - - if "營業時間" in user_input_lower or "時間" in user_input_lower: - return "我们冰淇淋店的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" - elif "地址" in user_input_lower or "地点" in user_input_lower or "地方" in user_input_lower: - return "我们的店在求毛星球毛笔城市QQ镇喜喜街167号一楼。" - elif "冰淇淋的口味" in user_input_lower or "口味" in user_input_lower: - return "我们的冰淇淋的口味有草莓,芒果,巧克力,咖啡,蓝莓,汽水和芭乐。" - elif "冰淇淋的价格" in user_input_lower or "价格" in user_input_lower or "冰淇淋怎么卖" in user_input_lower: - return "一球100元,两球170元,三球250元,五球400元,十球750元。" - 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: - return "我们有和吃鸡炸鸡店合作,去那里消费满1000元即可换一球冰淇淋。" - elif "help" in user_input_lower or "帮助" in user_input_lower: - return "\n輸入「營業時間、時間」即可知道營業時間\n輸入「地址、地點、地方」即可知道地址\n輸入「冰淇淋的口味、口味」即可知道口味\n輸入「冰淇淋的價格、價格、冰淇淋怎麼賣」即可知道價格\n輸入「有打折嗎、有優惠嗎、有進行促銷活動嗎、有活動嗎、折扣」即可知道優惠資訊\n輸入「停車場、停車、附近有沒有停車場」即可知道停車資訊\n輸入「可以客製化嗎、客製化的服務、客製化」即可知道客製化服務\n輸入「客製化的價錢、客製化要多少錢」即可知道客製化價格\n輸入「推薦、招牌」即可知道推薦口味\n輸入「外帶、內用」即可知道用餐方式\n輸入「付款、支付、可以刷卡」即可知道付款方式。" - elif "你好" in user_input_lower or "hello" in user_input_lower or "hi" in user_input_lower or "Hi" in user_input_lower: - return "你好!请问您需要什么帮助吗?" - elif "停车场" in user_input_lower or "停车" in user_input_lower or "附近有没有停车场" in user_input_lower: - return "我们的店附近有三个停车场,步行三分钟即可到达我们的店。" - elif "可以克制化吗" in user_input_lower or "客制化的服务" in user_input_lower or "客制化" in user_input_lower: - return "我们的店可以克制化冰淇淋,可以选择彩糖、珍珠、草莓酱、巧克力酱、布丁、奶油和炼乳。" - elif "客制化的价钱" in user_input_lower or "客制化要多少钱" in user_input_lower or "客制化要多少钱?" in user_input_lower: - return "客制化每一种材料都要再加15元。" - elif "推薦" in user_input_lower or "招牌" in user_input_lower: - return "推薦您試試草莓、巧克力和芭樂口味,都是熱門選擇。" - elif "外帶" in user_input_lower or "內用" in user_input_lower: - return "我們提供內用及外帶服務。" - elif "付款" in user_input_lower or "支付" in user_input_lower or "可以刷卡" in user_input_lower: - return "我們接受現金、信用卡及行動支付。" - elif "谢谢" in user_input_lower or "Thank you" in user_input_lower: - return "不客气!还需要什么帮助吗?" - - else: - # 計算目前是第幾輪對話(一輪包含使用者輸入與客服回覆,共 2 個元素) - # Why: 藉由傳入的 history 串列長度除以 2,即可動態得知目前互動的輪數,模擬記憶狀態。 - turns = len(history) // 2 + 1 - return ( - f"(模擬模式)收到您的訊息:『{user_input}』。" - f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?" - ) - - -def main(): - print("=" * 50) - print(" 吃吃吃冰淇淋店客服對話系統 (支援對話上下文記憶)") - print("=" * 50) - - use_mock = False - chat_session = None - - if not api_key: - print("[提示] 未檢測到環境變數 GEMINI_API_KEY,將自動啟用「離線模擬模式 (Local Mock Mode)」。") - print(" (如需連線雲端模型,請設定 GEMINI_API_KEY 環境變數)") - use_mock = True - else: - try: - print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...") - client = genai.Client(api_key=api_key) - # TODO: 設定系統指令 (System Instruction / System Prompt) - system_instruction = ( - "你是一位冰淇淋店的客服,负责提供资讯" - "我们冰淇淋店的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" - "我们的店在求毛星球毛笔城市QQ镇喜喜街167号一楼。" - "冰淇淋的价格一球100元,两球170元,三球250元,五球400元,十球750元。" - "我们的店有进行优惠,有和吃鸡炸鸡店合作,去那里消费满1000元即可换一球冰淇淋。" - "这是我们的资讯:輸入「營業時間、時間」即可知道營業時間,輸入「地址、地點、地方」即可知道地址,輸入「冰淇淋的口味、口味」即可知道口味,輸入「冰淇淋的價格、價格、冰淇淋怎麼賣」即可知道價格,輸入「有打折嗎、有優惠嗎、有進行促銷活動嗎、有活動嗎、折扣」即可知道優惠資訊,輸入「停車場、停車、附近有沒有停車場」即可知道停車資訊,輸入「可以客製化嗎、客製化的服務、客製化」即可知道客製化服務,輸入「客製化的價錢、客製化要多少錢」即可知道客製化價格,輸入「推薦、招牌」即可知道推薦口味,輸入「外帶、內用」即可知道用餐方式,輸入「付款、支付、可以刷卡」即可知道付款方式。" - "你应该亲切的向客户问好。" - "我们的店附近有三个停车场,步行三分钟即可到达我们的店。" - "我们的店可以克制化冰淇淋,可以选择彩糖、珍珠、草莓酱、巧克力酱、布丁、奶油和炼乳。" - "客制化每一种材料都要再加15元。" - "我們提供內用及外帶服務。" - "我們接受現金、信用卡及行動支付。" - ) - - chat_session = client.chats.create( - model=GEMINI_MODEL, - config={'system_instruction': system_instruction} - ) - print("雲端客服系統已就緒!") - except Exception as e: - print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...") - use_mock = True - - local_history = [] - - print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):") - print("-" * 50) - - while True: - try: - user_input = input("\n[您]: ").strip() - if not user_input: - continue - - if user_input.lower() in ["quit", "exit"]: - print("對話結束,感謝您的使用!") - break - - if use_mock: - ai_reply = local_mock_response(user_input, local_history) - local_history.append({"role": "user", "text": user_input}) - local_history.append({"role": "model", "text": ai_reply}) - else: - response = chat_session.send_message(user_input) - ai_reply = response.text - - print(f"[AI 客服]: {ai_reply}") - - except KeyboardInterrupt: - print("\n對話強行終止。再見!") - break - except Exception as e: - print(f"\n[錯誤] 發生異常:{e}") - - -if __name__ == "__main__": - main() +# d2_3_chatbot_memory.py +# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模) + +import os +from google import genai + +# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") +api_key = _telearn_require_gemini_api_key(allow_skip=True) + + +def local_mock_response(user_input, history): + user_input_lower = user_input.lower() + + if "營業時間" in user_input_lower or "時間" in user_input_lower: + return "我们冰淇淋店的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" + elif "地址" in user_input_lower or "地点" in user_input_lower or "地方" in user_input_lower: + return "我们的店在求毛星球毛笔城市QQ镇喜喜街167号一楼。" + elif "冰淇淋的口味" in user_input_lower or "口味" in user_input_lower: + return "我们的冰淇淋的口味有草莓,芒果,巧克力,咖啡,蓝莓,汽水和芭乐。" + elif "冰淇淋的价格" in user_input_lower or "价格" in user_input_lower or "冰淇淋怎么卖" in user_input_lower: + return "一球100元,两球170元,三球250元,五球400元,十球750元。" + 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: + return "我们有和吃鸡炸鸡店合作,去那里消费满1000元即可换一球冰淇淋。" + elif "help" in user_input_lower or "帮助" in user_input_lower: + return "\n輸入「營業時間、時間」即可知道營業時間\n輸入「地址、地點、地方」即可知道地址\n輸入「冰淇淋的口味、口味」即可知道口味\n輸入「冰淇淋的價格、價格、冰淇淋怎麼賣」即可知道價格\n輸入「有打折嗎、有優惠嗎、有進行促銷活動嗎、有活動嗎、折扣」即可知道優惠資訊\n輸入「停車場、停車、附近有沒有停車場」即可知道停車資訊\n輸入「可以客製化嗎、客製化的服務、客製化」即可知道客製化服務\n輸入「客製化的價錢、客製化要多少錢」即可知道客製化價格\n輸入「推薦、招牌」即可知道推薦口味\n輸入「外帶、內用」即可知道用餐方式\n輸入「付款、支付、可以刷卡」即可知道付款方式。" + elif "你好" in user_input_lower or "hello" in user_input_lower or "hi" in user_input_lower or "Hi" in user_input_lower: + return "你好!请问您需要什么帮助吗?" + elif "停车场" in user_input_lower or "停车" in user_input_lower or "附近有没有停车场" in user_input_lower: + return "我们的店附近有三个停车场,步行三分钟即可到达我们的店。" + elif "可以克制化吗" in user_input_lower or "客制化的服务" in user_input_lower or "客制化" in user_input_lower: + return "我们的店可以克制化冰淇淋,可以选择彩糖、珍珠、草莓酱、巧克力酱、布丁、奶油和炼乳。" + elif "客制化的价钱" in user_input_lower or "客制化要多少钱" in user_input_lower or "客制化要多少钱?" in user_input_lower: + return "客制化每一种材料都要再加15元。" + elif "推薦" in user_input_lower or "招牌" in user_input_lower: + return "推薦您試試草莓、巧克力和芭樂口味,都是熱門選擇。" + elif "外帶" in user_input_lower or "內用" in user_input_lower: + return "我們提供內用及外帶服務。" + elif "付款" in user_input_lower or "支付" in user_input_lower or "可以刷卡" in user_input_lower: + return "我們接受現金、信用卡及行動支付。" + elif "谢谢" in user_input_lower or "Thank you" in user_input_lower: + return "不客气!还需要什么帮助吗?" + + else: + # 計算目前是第幾輪對話(一輪包含使用者輸入與客服回覆,共 2 個元素) + # Why: 藉由傳入的 history 串列長度除以 2,即可動態得知目前互動的輪數,模擬記憶狀態。 + turns = len(history) // 2 + 1 + return ( + f"(模擬模式)收到您的訊息:『{user_input}』。" + f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?" + ) + + +def main(): + print("=" * 50) + print(" 吃吃吃冰淇淋店客服對話系統 (支援對話上下文記憶)") + print("=" * 50) + + use_mock = False + chat_session = None + + if not api_key: + print("[提示] 未輸入 GEMINI_API_KEY,改用「離線模擬模式 (Local Mock Mode)」。") + use_mock = True + else: + try: + print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...") + client = genai.Client(api_key=api_key) + # TODO: 設定系統指令 (System Instruction / System Prompt) + system_instruction = ( + "你是一位冰淇淋店的客服,负责提供资讯" + "我们冰淇淋店的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" + "我们的店在求毛星球毛笔城市QQ镇喜喜街167号一楼。" + "冰淇淋的价格一球100元,两球170元,三球250元,五球400元,十球750元。" + "我们的店有进行优惠,有和吃鸡炸鸡店合作,去那里消费满1000元即可换一球冰淇淋。" + "这是我们的资讯:輸入「營業時間、時間」即可知道營業時間,輸入「地址、地點、地方」即可知道地址,輸入「冰淇淋的口味、口味」即可知道口味,輸入「冰淇淋的價格、價格、冰淇淋怎麼賣」即可知道價格,輸入「有打折嗎、有優惠嗎、有進行促銷活動嗎、有活動嗎、折扣」即可知道優惠資訊,輸入「停車場、停車、附近有沒有停車場」即可知道停車資訊,輸入「可以客製化嗎、客製化的服務、客製化」即可知道客製化服務,輸入「客製化的價錢、客製化要多少錢」即可知道客製化價格,輸入「推薦、招牌」即可知道推薦口味,輸入「外帶、內用」即可知道用餐方式,輸入「付款、支付、可以刷卡」即可知道付款方式。" + "你应该亲切的向客户问好。" + "我们的店附近有三个停车场,步行三分钟即可到达我们的店。" + "我们的店可以克制化冰淇淋,可以选择彩糖、珍珠、草莓酱、巧克力酱、布丁、奶油和炼乳。" + "客制化每一种材料都要再加15元。" + "我們提供內用及外帶服務。" + "我們接受現金、信用卡及行動支付。" + ) + + chat_session = client.chats.create( + model=GEMINI_MODEL, + config={'system_instruction': system_instruction} + ) + print("雲端客服系統已就緒!") + except Exception as e: + print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...") + use_mock = True + + local_history = [] + + print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):") + print("-" * 50) + + while True: + try: + user_input = input("\n[您]: ").strip() + if not user_input: + continue + + if user_input.lower() in ["quit", "exit"]: + print("對話結束,感謝您的使用!") + break + + if use_mock: + ai_reply = local_mock_response(user_input, local_history) + local_history.append({"role": "user", "text": user_input}) + local_history.append({"role": "model", "text": ai_reply}) + else: + response = chat_session.send_message(user_input) + ai_reply = response.text + + print(f"[AI 客服]: {ai_reply}") + + except KeyboardInterrupt: + print("\n對話強行終止。再見!") + break + except Exception as e: + print(f"\n[錯誤] 發生異常:{e}") + + +if __name__ == "__main__": + main() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..4946a0a52b48301e190fe096df87684bd366261c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_4_chatbot_memory.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_4_chatbot_memory.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\270\211\346\214\207\346\215\217\346\270\270\346\210\217/d5_pinch_challenge_solution.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" similarity index 97% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\270\211\346\214\207\346\215\217\346\270\270\346\210\217/d5_pinch_challenge_solution.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" index 4fab835be0c692d02f761eedc81c00545d94e078..ed78a86e2c79e10b0071bc09c684966033022873 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\270\211\346\214\207\346\215\217\346\270\270\346\210\217/d5_pinch_challenge_solution.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" @@ -1,217 +1,216 @@ -""" -Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版) -================================================================== -本週挑戰目標: - 1. 實作「三指捏合 (Three-Finger Pinch)」: - 必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」 - 以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch - 2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊 - 3. 使用邊緣觸發 (Edge Detection) 防止重複觸發 - 4. 新增 30 秒限時機制,時間到顯示最終得分 -""" -import pygame -import cv2 -import mediapipe as mp -import math -import sys -import os -import random - -# ============================================ -# 1. 初始化 -# ============================================ -pygame.init() -WIDTH, HEIGHT = 800, 600 -screen = pygame.display.set_mode((WIDTH, HEIGHT)) -pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ") -clock = pygame.time.Clock() -from mediapipe.tasks import python -from mediapipe.tasks.python import vision - -# 攝影機 -cap = cv2.VideoCapture(0) - -# 【TODO 1】初始化 MediaPipe 手勢偵測器 (Hand Landmarker) -\ - -try: - from mediapipe.tasks import python - from mediapipe.tasks.python import vision - base_options = python.BaseOptions(model_asset_path=os.path.join('hand_landmarker.task')) - options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) - detector = vision.HandLandmarker.create_from_options(options) -except Exception as e: - print(f"Error loading model: {e}") - pygame.quit() - sys.exit() - -# ============================================ -# 2. 遊戲變數 -# ============================================ -# 手指座標 -finger_x, finger_y = 400, 300 # 食指 -thumb_x, thumb_y = 400, 300 # 大拇指 -middle_x, middle_y = 400, 300 # 中指 - -# 距離變數 -dist_thumb_index = 0.0 -dist_thumb_middle = 0.0 - -# Pinch 相關變數 -is_pinching = False -was_pinching = False # 邊緣觸發 -PINCH_THRESHOLD = 50 # 距離臨界值 - -# 按鈕設定 -button_rect = pygame.Rect(300, 250, 200, 100) -button_colors = [(100, 100, 220), (220, 100, 100), - (100, 220, 100), (220, 180, 50)] -current_color_index = 0 - -# 點擊計數器 -click_count = 0 - -# --- 限時機制變數 --- -START_TIME = pygame.time.get_ticks() -GAME_DURATION = 30 # 遊戲限制時間(秒) -game_state = "PLAYING" # 狀態機 - -# ============================================ -# 3. 主迴圈 -# ============================================ -running = True -while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - # --- 讀取攝影機與手勢偵測 --- - success, img = cap.read() - is_pinching = False # 每幀重設狀態,防止手離屏時卡死 - - if success: - img = cv2.flip(img, 1) # 鏡像翻轉 - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - results = detector.detect(mp_image) - - # === 【TODO 2:挑戰關卡三之手部安全偵測】 === - # 1. 判斷手部是否在畫面中 (results.hand_landmarks 是否有資料) - # 2. 如果在畫面中,取得食指 (8)、大拇指 (4)、中指 (12) 的像素座標 - # 3. 如果手不在畫面中,將 is_pinching 設為 False - # - hand_in_frame = False - if results.hand_landmarks: - hand_in_frame = True - for hand_lms in results.hand_landmarks: - # 食指 (8) - lm8 = hand_lms[8] - finger_x = int(lm8.x * WIDTH) - finger_y = int(lm8.y * HEIGHT) - - # 大拇指 (4) - lm4 = hand_lms[4] - thumb_x = int(lm4.x * WIDTH) - thumb_y = int(lm4.y * HEIGHT) - - # 中指 (12) - lm12 = hand_lms[12] - middle_x = int(lm12.x * WIDTH) - middle_y = int(lm12.y * HEIGHT) - - # === 【TODO 3:挑戰關卡一之三指捏合判定】 === - # 提示:計算食指與大拇指距離,以及中指與大拇指距離 - # 兩組距離皆小於 PINCH_THRESHOLD 時,is_pinching 才為 True - dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y) - dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y) - is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD) - - dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y) - dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y) - is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD) - - # --- 狀態機邏輯處理 --- - if game_state == "PLAYING": - # 1. 計算剩餘時間 - current_ticks = pygame.time.get_ticks() - elapsed_seconds = (current_ticks - START_TIME) / 1000 - remaining_time = max(0, GAME_DURATION - elapsed_seconds) - - # 2. 檢查時間是否到了 - if remaining_time <= 0: - game_state = "GAME_OVER" - - # 3. 邊緣觸發點擊判斷 - if is_pinching and not was_pinching: - finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) - if finger_rect.colliderect(button_rect): - click_count += 1 - current_color_index = (current_color_index + 1) % len(button_colors) - - # === 【TODO 4:挑戰關卡二之按鈕隨機移動】 === - # 提示:當點擊發生時,讓按鈕的 x 和 y 座標隨機分配在畫面內 - - button_rect.x = random.randint(0, WIDTH - button_rect.width) - button_rect.y = random.randint(0, HEIGHT - button_rect.height) - print(f" 魔法三指點擊!第 {click_count} 次") - - # 更新上一幀狀態 - was_pinching = is_pinching - - # --- 繪製畫面 --- - screen.fill((20, 20, 35)) - font = pygame.font.SysFont(None, 32) - large_font = pygame.font.SysFont(None, 64) - - if game_state == "PLAYING": - # 繪製按鈕 - btn_color = button_colors[current_color_index] - pygame.draw.rect(screen, btn_color, button_rect, border_radius=16) - - btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255)) - screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2, - button_rect.centery - btn_text.get_height() // 2)) - - # 顯示倒數計時 - time_text = font.render(f"Time Left: {remaining_time:.1f}s", True, (255, 220, 100)) - screen.blit(time_text, (WIDTH - time_text.get_width() - 20, 10)) - - # 畫手指游標 - cursor_color = (50, 255, 50) if is_pinching else (255, 50, 50) - pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) # 食指 - pygame.draw.circle(screen, (255, 255, 255), (thumb_x, thumb_y), 12) # 大拇指 - pygame.draw.circle(screen, (100, 150, 255), (middle_x, middle_y), 12) # 中指 - - # === 【TODO 5:挑戰關卡三之畫面提示】 === - # 提示:當手不在畫面中時,在畫面上方顯示警告文字提醒學生將手部放回 - - if not hand_in_frame: - warning_text = font.render("OUT OF FRAME - PLEASE PUT HANDS BACK", True, (255, 100, 100)) - screen.blit(warning_text, (WIDTH // 2 - warning_text.get_width() // 2, 100)) - - # 顯示調試數值 - small_font = pygame.font.SysFont(None, 24) - status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150)) - screen.blit(status, (10, 10)) - - dist_text = small_font.render(f"Dist Thumb-Index: {dist_thumb_index:.0f} px", True, (150, 150, 200)) - screen.blit(dist_text, (10, 35)) - - dist_text2 = small_font.render(f"Dist Thumb-Middle: {dist_thumb_middle:.0f} px", True, (150, 150, 200)) - screen.blit(dist_text2, (10, 60)) - - elif game_state == "GAME_OVER": - # 繪製遊戲結束畫面 - over_text = large_font.render("GAME OVER", True, (255, 50, 50)) - score_text = font.render(f"Total Clicks: {click_count} times", True, (255, 255, 255)) - - screen.blit(over_text, (WIDTH // 2 - over_text.get_width() // 2, HEIGHT // 2 - 50)) - screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, HEIGHT // 2 + 20)) - - pygame.display.update() - clock.tick(30) - -cap.release() -pygame.quit() -sys.exit() +""" +Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版) +================================================================== +本週挑戰目標: + 1. 實作「三指捏合 (Three-Finger Pinch)」: + 必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」 + 以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch + 2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊 + 3. 使用邊緣觸發 (Edge Detection) 防止重複觸發 + 4. 新增 30 秒限時機制,時間到顯示最終得分 +""" +import pygame +import cv2 +import mediapipe as mp +import math +import sys +import os +import random + +# ============================================ +# 1. 初始化 +# ============================================ +pygame.init() +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ") +clock = pygame.time.Clock() +from mediapipe.tasks import python +from mediapipe.tasks.python import vision + +# 攝影機 +cap = cv2.VideoCapture(0) + +# 【TODO 1】初始化 MediaPipe 手勢偵測器 (Hand Landmarker) + +try: + from mediapipe.tasks import python + from mediapipe.tasks.python import vision + base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hand_landmarker.task')) + options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) + detector = vision.HandLandmarker.create_from_options(options) +except Exception as e: + print(f"Error loading model: {e}") + pygame.quit() + sys.exit() + +# ============================================ +# 2. 遊戲變數 +# ============================================ +# 手指座標 +finger_x, finger_y = 400, 300 # 食指 +thumb_x, thumb_y = 400, 300 # 大拇指 +middle_x, middle_y = 400, 300 # 中指 + +# 距離變數 +dist_thumb_index = 0.0 +dist_thumb_middle = 0.0 + +# Pinch 相關變數 +is_pinching = False +was_pinching = False # 邊緣觸發 +PINCH_THRESHOLD = 50 # 距離臨界值 + +# 按鈕設定 +button_rect = pygame.Rect(300, 250, 200, 100) +button_colors = [(100, 100, 220), (220, 100, 100), + (100, 220, 100), (220, 180, 50)] +current_color_index = 0 + +# 點擊計數器 +click_count = 0 + +# --- 限時機制變數 --- +START_TIME = pygame.time.get_ticks() +GAME_DURATION = 30 # 遊戲限制時間(秒) +game_state = "PLAYING" # 狀態機 + +# ============================================ +# 3. 主迴圈 +# ============================================ +running = True +while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # --- 讀取攝影機與手勢偵測 --- + success, img = cap.read() + is_pinching = False # 每幀重設狀態,防止手離屏時卡死 + + if success: + img = cv2.flip(img, 1) # 鏡像翻轉 + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + # === 【TODO 2:挑戰關卡三之手部安全偵測】 === + # 1. 判斷手部是否在畫面中 (results.hand_landmarks 是否有資料) + # 2. 如果在畫面中,取得食指 (8)、大拇指 (4)、中指 (12) 的像素座標 + # 3. 如果手不在畫面中,將 is_pinching 設為 False + # + hand_in_frame = False + if results.hand_landmarks: + hand_in_frame = True + for hand_lms in results.hand_landmarks: + # 食指 (8) + lm8 = hand_lms[8] + finger_x = int(lm8.x * WIDTH) + finger_y = int(lm8.y * HEIGHT) + + # 大拇指 (4) + lm4 = hand_lms[4] + thumb_x = int(lm4.x * WIDTH) + thumb_y = int(lm4.y * HEIGHT) + + # 中指 (12) + lm12 = hand_lms[12] + middle_x = int(lm12.x * WIDTH) + middle_y = int(lm12.y * HEIGHT) + + # === 【TODO 3:挑戰關卡一之三指捏合判定】 === + # 提示:計算食指與大拇指距離,以及中指與大拇指距離 + # 兩組距離皆小於 PINCH_THRESHOLD 時,is_pinching 才為 True + dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y) + dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y) + is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD) + + dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y) + dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y) + is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD) + + # --- 狀態機邏輯處理 --- + if game_state == "PLAYING": + # 1. 計算剩餘時間 + current_ticks = pygame.time.get_ticks() + elapsed_seconds = (current_ticks - START_TIME) / 1000 + remaining_time = max(0, GAME_DURATION - elapsed_seconds) + + # 2. 檢查時間是否到了 + if remaining_time <= 0: + game_state = "GAME_OVER" + + # 3. 邊緣觸發點擊判斷 + if is_pinching and not was_pinching: + finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) + if finger_rect.colliderect(button_rect): + click_count += 1 + current_color_index = (current_color_index + 1) % len(button_colors) + + # === 【TODO 4:挑戰關卡二之按鈕隨機移動】 === + # 提示:當點擊發生時,讓按鈕的 x 和 y 座標隨機分配在畫面內 + + button_rect.x = random.randint(0, WIDTH - button_rect.width) + button_rect.y = random.randint(0, HEIGHT - button_rect.height) + print(f" 魔法三指點擊!第 {click_count} 次") + + # 更新上一幀狀態 + was_pinching = is_pinching + + # --- 繪製畫面 --- + screen.fill((20, 20, 35)) + font = pygame.font.SysFont(None, 32) + large_font = pygame.font.SysFont(None, 64) + + if game_state == "PLAYING": + # 繪製按鈕 + btn_color = button_colors[current_color_index] + pygame.draw.rect(screen, btn_color, button_rect, border_radius=16) + + btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255)) + screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2, + button_rect.centery - btn_text.get_height() // 2)) + + # 顯示倒數計時 + time_text = font.render(f"Time Left: {remaining_time:.1f}s", True, (255, 220, 100)) + screen.blit(time_text, (WIDTH - time_text.get_width() - 20, 10)) + + # 畫手指游標 + cursor_color = (50, 255, 50) if is_pinching else (255, 50, 50) + pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) # 食指 + pygame.draw.circle(screen, (255, 255, 255), (thumb_x, thumb_y), 12) # 大拇指 + pygame.draw.circle(screen, (100, 150, 255), (middle_x, middle_y), 12) # 中指 + + # === 【TODO 5:挑戰關卡三之畫面提示】 === + # 提示:當手不在畫面中時,在畫面上方顯示警告文字提醒學生將手部放回 + + if not hand_in_frame: + warning_text = font.render("OUT OF FRAME - PLEASE PUT HANDS BACK", True, (255, 100, 100)) + screen.blit(warning_text, (WIDTH // 2 - warning_text.get_width() // 2, 100)) + + # 顯示調試數值 + small_font = pygame.font.SysFont(None, 24) + status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150)) + screen.blit(status, (10, 10)) + + dist_text = small_font.render(f"Dist Thumb-Index: {dist_thumb_index:.0f} px", True, (150, 150, 200)) + screen.blit(dist_text, (10, 35)) + + dist_text2 = small_font.render(f"Dist Thumb-Middle: {dist_thumb_middle:.0f} px", True, (150, 150, 200)) + screen.blit(dist_text2, (10, 60)) + + elif game_state == "GAME_OVER": + # 繪製遊戲結束畫面 + over_text = large_font.render("GAME OVER", True, (255, 50, 50)) + score_text = font.render(f"Total Clicks: {click_count} times", True, (255, 255, 255)) + + screen.blit(over_text, (WIDTH // 2 - over_text.get_width() // 2, HEIGHT // 2 - 50)) + screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, HEIGHT // 2 + 20)) + + pygame.display.update() + clock.tick(30) + +cap.release() +pygame.quit() +sys.exit() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..fcb42c4bf1d6f4dbdfca16288a1aa1e349c298ef --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d5_pinch_challenge_solution.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd5_pinch_challenge_solution.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\204\270\350\276\250\350\257\206/d3_2_face_detection.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\207\211\350\276\250\350\255\230/d3_2_face_detection.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\204\270\350\276\250\350\257\206/d3_2_face_detection.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\207\211\350\276\250\350\255\230/d3_2_face_detection.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\204\270\350\276\250\350\257\206/d3_2_face_detection2.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\207\211\350\276\250\350\255\230/d3_2_face_detection2.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\204\270\350\276\250\350\257\206/d3_2_face_detection2.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\207\211\350\276\250\350\255\230/d3_2_face_detection2.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..72b1075a69ea1ceeabd526fd9dbbcf8a32934405 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d3_2_face_detection2.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd3_2_face_detection2.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\207\211\350\276\250\350\255\230/onepiece(2).jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\207\211\350\276\250\350\255\230/onepiece(2).jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..d462e2442bb100a26a13c0dd9f1f539222ec45a8 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\344\272\272\350\207\211\350\276\250\350\255\230/onepiece(2).jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16063edf453095349df30308f927b018b8b6eaf10cdcff3fd64dbe3e5c6c0cf6 +size 80213 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\233\276\350\241\250/coffee_sales_analysis.png" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\234\226\350\241\250/coffee_sales_analysis.png" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\233\276\350\241\250/coffee_sales_analysis.png" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\234\226\350\241\250/coffee_sales_analysis.png" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\233\276\350\241\250/coffee_sales_data.csv" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\234\226\350\241\250/coffee_sales_data.csv" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\233\276\350\241\250/coffee_sales_data.csv" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\234\226\350\241\250/coffee_sales_data.csv" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\233\276\350\241\250/d1_4_visualization_coffee.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\234\226\350\241\250/d1_4_visualization_coffee.py" similarity index 84% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\233\276\350\241\250/d1_4_visualization_coffee.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\234\226\350\241\250/d1_4_visualization_coffee.py" index 21bdf330eca1a6a8ef832bb30de0041f76adc79c..4b452920826fdfc6c7833866a31feed8ba239be0 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\233\276\350\241\250/d1_4_visualization_coffee.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\234\226\350\241\250/d1_4_visualization_coffee.py" @@ -1,3 +1,5 @@ +import matplotlib +matplotlib.use('Agg') # Telearn hub headless # d1_4_visualization_coffee.py # Day 1: 資料科學與數據視覺化 - 咖啡廳營運數據進階分析(學員填空) # cd examples/Day1/student && python3 d1_4_visualization_coffee.py @@ -8,6 +10,34 @@ import pandas as pd import matplotlib.pyplot as plt + +def _telearn_plt_show(*_a, **_k): + """Hub-friendly plt.show: save current figure and display via OpenCV.""" + import os + import matplotlib.pyplot as plt + try: + import cv2 + except ImportError: + plt.savefig("_telearn_last_plot.png", dpi=150, bbox_inches="tight") + print("[Telearn] 已儲存 _telearn_last_plot.png(無 cv2 可顯示)") + return + path = os.path.join(os.getcwd(), "_telearn_last_plot.png") + plt.savefig(path, dpi=150, bbox_inches="tight") + img = cv2.imread(path) + if img is not None: + # fit to screen-ish width + h, w = img.shape[:2] + max_w = 960 + if w > max_w: + scale = max_w / float(w) + img = cv2.resize(img, (int(w * scale), int(h * scale))) + cv2.imshow("Chart (press any key)", img) + print("[Telearn] 圖表已顯示,按任意鍵關閉…") + cv2.waitKey(0) + cv2.destroyAllWindows() + else: + print(f"[Telearn] 圖表已儲存: {path}") + def load_csv(filename="coffee_sales_data.csv"): """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" # === 【學員填空練習點 1:CSV 檔案安全讀取】 === @@ -112,4 +142,4 @@ plt.tight_layout() # 自動調整間距,避免文字重疊 output_image = "coffee_sales_analysis.png" # 請在下方完成圖表存檔與顯示: plt.savefig(output_image, dpi=300) -plt.show() \ No newline at end of file +_telearn_plt_show() \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\234\226\350\241\250/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\234\226\350\241\250/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..1f104b92c6250f22879c19e0c0198fe1a72e3ba3 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\222\226\345\225\241\345\272\227\345\210\206\346\236\220\345\234\226\350\241\250/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_4_visualization_coffee.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_4_visualization_coffee.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\345\201\265\346\270\254/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\345\201\265\346\270\254/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..4f7550b344cc508f552737ddcfc49fc178221251 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\345\201\265\346\270\254/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step3_mouth_ratio.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step3_mouth_ratio.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\345\201\265\346\270\254/onepiece(2).jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\345\201\265\346\270\254/onepiece(2).jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..d462e2442bb100a26a13c0dd9f1f539222ec45a8 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\345\201\265\346\270\254/onepiece(2).jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16063edf453095349df30308f927b018b8b6eaf10cdcff3fd64dbe3e5c6c0cf6 +size 80213 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\344\276\246\346\265\213/step3_mouth_ratio.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\345\201\265\346\270\254/step3_mouth_ratio.py" similarity index 96% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\344\276\246\346\265\213/step3_mouth_ratio.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\345\201\265\346\270\254/step3_mouth_ratio.py" index 5ed81565194a1bf3b44ffcd1b9c2c35f48a7acad..a404dd119baaf4ab29df541281846a1bd275d0eb 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\344\276\246\346\265\213/step3_mouth_ratio.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\230\264\345\267\264\345\201\265\346\270\254/step3_mouth_ratio.py" @@ -1,113 +1,113 @@ -""" -Week 11 - Step 2: Mouth Opening Ratio Detection (Template 填空版) -============================================================= -本步驟目標: - 1. 找出特定的臉部特徵點:眼角(33, 263)與嘴唇(13, 14) - 2. 計算兩眼距離,當作臉寬基準比例尺,防止人靠近或遠離鏡頭造成的誤判 (歸一化) - 3. 計算嘴巴張開比例:嘴巴距離 / 兩眼距離 - 4. 在 OpenCV 畫面上即時顯示比例與嘴巴開合狀態 (OPEN / CLOSED) -""" -import cv2 -import mediapipe as mp -import math -import sys -import os - -# 1. 初始化攝影機 -cap = cv2.VideoCapture(0) -if not cap.isOpened(): - print("Error: Could not open camera.") - sys.exit() - -# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) -model_path = os.path.join('face_landmarker.task') -if not os.path.exists(model_path): - print(f"Error: Model file not found at {model_path}") - sys.exit() - -try: - from mediapipe.tasks import python - from mediapipe.tasks.python import vision - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) - detector = vision.FaceLandmarker.create_from_options(options) - print("Face Landmarker initialized successfully.") -except Exception as e: - print(f"Error: Failed to initialize detector: {e}") - sys.exit() - -# 特徵點索引定義 -LEFT_EYE_OUTER = 33 -RIGHT_EYE_OUTER = 263 -LIP_TOP = 13 -LIP_BOTTOM = 14 - -# 嘴巴張開的比例閾值 (超過代表張開) -THRESHOLD = 0.25 - -print("Press 'q' in the window to exit.") - -# 3. 主迴圈 -while True: - success, img = cap.read() - if not success: - break - - img = cv2.flip(img, 1) - img_h, img_w, _ = img.shape - - # 影像處理 - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - results = detector.detect(mp_image) - - if results.face_landmarks: - face_lms = results.face_landmarks[0] - - # 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中 - pts = {} - for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]: - pt = face_lms[idx] - pts[idx] = (int(pt.x * img_w), int(pt.y * img_h)) - - - # 【TODO 2】計算兩眼外角距離 (做為基準比例尺) - p_lx, p_ly = pts[LEFT_EYE_OUTER] - p_rx, p_ry = pts[RIGHT_EYE_OUTER] - base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly) - - - if base_dist > 0: - # 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」 - p_top_x, p_top_y = pts[LIP_TOP] - p_bot_x, p_bot_y = pts[LIP_BOTTOM] - mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y) - mouth_ratio = mouth_dist / base_dist - - - # 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線 - # 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色) - is_open = mouth_ratio > THRESHOLD - cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線 - cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線 - - - # 畫出特徵點小黃圓圈 - for idx, pos in pts.items(): - cv2.circle(img, pos, 5, (0, 255, 255), -1) - - # 顯示比例與開合狀態文字 - cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) - status_text = "OPEN" if is_open else "CLOSED" - status_color = (0, 255, 0) if is_open else (0, 0, 255) - cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2) - else: - cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) - - cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img) - - if cv2.waitKey(1) & 0xFF == ord('q'): - break - -cap.release() -cv2.destroyAllWindows() +""" +Week 11 - Step 2: Mouth Opening Ratio Detection (Template 填空版) +============================================================= +本步驟目標: + 1. 找出特定的臉部特徵點:眼角(33, 263)與嘴唇(13, 14) + 2. 計算兩眼距離,當作臉寬基準比例尺,防止人靠近或遠離鏡頭造成的誤判 (歸一化) + 3. 計算嘴巴張開比例:嘴巴距離 / 兩眼距離 + 4. 在 OpenCV 畫面上即時顯示比例與嘴巴開合狀態 (OPEN / CLOSED) +""" +import cv2 +import mediapipe as mp +import math +import sys +import os + +# 1. 初始化攝影機 +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("Error: Could not open camera.") + sys.exit() + +# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) +model_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task') +if not os.path.exists(model_path): + print(f"Error: Model file not found at {model_path}") + sys.exit() + +try: + from mediapipe.tasks import python + from mediapipe.tasks.python import vision + base_options = python.BaseOptions(model_asset_path=model_path) + options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) + detector = vision.FaceLandmarker.create_from_options(options) + print("Face Landmarker initialized successfully.") +except Exception as e: + print(f"Error: Failed to initialize detector: {e}") + sys.exit() + +# 特徵點索引定義 +LEFT_EYE_OUTER = 33 +RIGHT_EYE_OUTER = 263 +LIP_TOP = 13 +LIP_BOTTOM = 14 + +# 嘴巴張開的比例閾值 (超過代表張開) +THRESHOLD = 0.25 + +print("Press 'q' in the window to exit.") + +# 3. 主迴圈 +while True: + success, img = cap.read() + if not success: + break + + img = cv2.flip(img, 1) + img_h, img_w, _ = img.shape + + # 影像處理 + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + if results.face_landmarks: + face_lms = results.face_landmarks[0] + + # 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中 + pts = {} + for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]: + pt = face_lms[idx] + pts[idx] = (int(pt.x * img_w), int(pt.y * img_h)) + + + # 【TODO 2】計算兩眼外角距離 (做為基準比例尺) + p_lx, p_ly = pts[LEFT_EYE_OUTER] + p_rx, p_ry = pts[RIGHT_EYE_OUTER] + base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly) + + + if base_dist > 0: + # 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」 + p_top_x, p_top_y = pts[LIP_TOP] + p_bot_x, p_bot_y = pts[LIP_BOTTOM] + mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y) + mouth_ratio = mouth_dist / base_dist + + + # 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線 + # 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色) + is_open = mouth_ratio > THRESHOLD + cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線 + cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線 + + + # 畫出特徵點小黃圓圈 + for idx, pos in pts.items(): + cv2.circle(img, pos, 5, (0, 255, 255), -1) + + # 顯示比例與開合狀態文字 + cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) + status_text = "OPEN" if is_open else "CLOSED" + status_color = (0, 255, 0) if is_open else (0, 0, 255) + cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2) + else: + cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) + + cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + +cap.release() +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\224\350\265\204\346\226\231\345\233\276\350\241\250/d1_3_visualization.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/d1_3_visualization.py" similarity index 78% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\224\350\265\204\346\226\231\345\233\276\350\241\250/d1_3_visualization.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/d1_3_visualization.py" index 081acf6e7fe9a40f490bb83cafbc1a57c9164ae7..5a13718cdde36e8c6737d98f3345c5c918f4a290 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\224\350\265\204\346\226\231\345\233\276\350\241\250/d1_3_visualization.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/d1_3_visualization.py" @@ -1,87 +1,117 @@ -# d1_3_visualization.py -# Day 1: 資料科學與數據視覺化 - Matplotlib 繪圖基礎(學員填空) -# cd examples/Day1/student && python3 d1_3_visualization.py -# 參考答案:../Teacher/d1_3_visualization.py - -import os -import pandas as pd -import matplotlib.pyplot as plt - -def load_csv(filename="taipei_weather_2026.csv"): - """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" - # === 【學員填空練習點 1:CSV 檔案安全讀取】 === - # TODO: 1. 使用 os.path.join 將當前腳本所在的目錄與檔案名稱拼接成絕對路徑 - # 提示: 當前腳本目錄為 os.path.dirname(__file__) - # 2. 使用 os.path.exists 判斷路徑是否存在,若存在則用 pd.read_csv 讀取並回傳 - path = os.path.join(os.path.dirname(__file__), filename) - - if os.path.exists(path): - return pd.read_csv(path) - else: - print("error: no files found") - -df = load_csv() - -# 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊 -plt.rcParams['font.sans-serif'] = [ - 'PingFang TC', 'Heiti TC', 'Arial Unicode MS', - 'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei', -] -plt.rcParams['axes.unicode_minus'] = False - -# === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 === -# TODO: 使用 plt.subplots 建立一個 2 列 1 行 (2x1) 的子圖畫布,尺寸大小為寬 10 英吋、高 8 英吋 -# 並指派給 fig 與 (ax1, ax2) -fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) - -# 1. 繪製折線圖 (Line Plot) - 每日溫度 -# === 【學員填空練習點 3:繪製每日溫度折線圖】 === -# TODO: 1. 在 ax1 畫布上以 ax1.plot() 繪製折線圖,X軸傳入 df['Date'],Y軸傳入 df['Temperature'] -# 設定參數:帶有圓點 marker='o',顏色 color='crimson',圖例 label='溫度 (°C)',線寬 linewidth=2 -# 2. 使用 ax1.set_title 設定標題 "台北觀測站每日溫度趨勢圖" -# 3. 設定 ax1.set_xlabel("日期") 與 ax1.set_ylabel("數值") -# 4. 設定網格 ax1.grid(True, linestyle='--', alpha=0.5) -# 5. 呼叫 ax1.legend() 顯示圖例 -# 6. 設定 X軸刻度旋轉 ax1.tick_params(axis='x', rotation=45) -# -# 請在下方完成 ax1 的溫度折線圖繪製: -ax1.plot(df['Date'], df['Temperature'], marker='o', color= 'crimson',label='温度 (°C)', linewidth=2) -ax1.set_title("台北观测站每日温度趋势图", fontsize=14, fontweight='bold') -ax1.set_xlabel("日期") - -ax1.set_ylabel("数值") -ax1.grid(True,linestyle="--", alpha=0.5) -ax1.legend() -ax1.tick_params(axis='x', rotation=45) -ax1.plot(df['Date'], df['Humidity'], marker='o', color='lime', label='湿度 (%)', linewidth=2) -ax1.legend() - -# 2. 繪製條形圖 (Bar Plot) - 每日降雨量 -# === 【學員填空練習點 4:繪製每日降雨量長條圖】 === -# TODO: 1. 在 ax2 畫布上以 ax2.bar() 繪製長條圖,X軸傳入 df['Date'],Y軸傳入 df['Rainfall'] -# 設定參數:顏色 color='skyblue',外框顏色 edgecolor='royalblue',圖例 label='降雨量 (mm)' -# 2. 使用 ax2.set_title 設定標題 "台北觀測站每日降雨量分佈" -# 3. 設定 ax2.set_xlabel("日期") 與 ax2.set_ylabel("降雨量 (mm)") -# 4. 設定 Y 軸網格 ax2.grid(True, axis='y', linestyle='--', alpha=0.5) -# 5. 呼叫 ax2.legend() 顯示圖例 -# 6. 設定 X軸刻度旋轉 ax2.tick_params(axis='x', rotation=45) -# -# 請在下方完成 ax2 的降雨量長條圖繪製: -ax2.bar(df['Date'], df['Rainfall'], color='turquoise',edgecolor = 'turquoise',label='降雨量 (mm)') -ax2.set_title("台北每天降雨量长条图", fontsize=14, fontweight='bold') -ax2.set_xlabel("日期") -ax2.set_ylabel("降雨量") -ax2.grid(True, axis='y', linestyle='--', alpha=0.5) -ax2.legend() -ax2.tick_params(axis='x', rotation=45) - -plt.tight_layout() # 自動調整間距,避免文字重疊 - -# === 【學員填空練習點 5:圖表自動儲存與顯示】 === -# TODO: 1. 呼叫 plt.savefig 將畫布儲存為檔名 "taipei_weather_2026.png",並設定高畫質 dpi=300 -# (⚠️ 注意:savefig 必須在 show() 之前呼叫) -# 2. 呼叫 plt.show() 將圖表視窗顯示在畫面上 -# -# 請在下方完成圖表存檔與顯示: -plt.savefig("taipei_weather_2026.png", dpi=300) -plt.show() \ No newline at end of file +import matplotlib +matplotlib.use('Agg') # Telearn hub headless +# d1_3_visualization.py +# Day 1: 資料科學與數據視覺化 - Matplotlib 繪圖基礎(學員填空) +# cd examples/Day1/student && python3 d1_3_visualization.py +# 參考答案:../Teacher/d1_3_visualization.py + +import os +import pandas as pd +import matplotlib.pyplot as plt + + +def _telearn_plt_show(*_a, **_k): + """Hub-friendly plt.show: save current figure and display via OpenCV.""" + import os + import matplotlib.pyplot as plt + try: + import cv2 + except ImportError: + plt.savefig("_telearn_last_plot.png", dpi=150, bbox_inches="tight") + print("[Telearn] 已儲存 _telearn_last_plot.png(無 cv2 可顯示)") + return + path = os.path.join(os.getcwd(), "_telearn_last_plot.png") + plt.savefig(path, dpi=150, bbox_inches="tight") + img = cv2.imread(path) + if img is not None: + # fit to screen-ish width + h, w = img.shape[:2] + max_w = 960 + if w > max_w: + scale = max_w / float(w) + img = cv2.resize(img, (int(w * scale), int(h * scale))) + cv2.imshow("Chart (press any key)", img) + print("[Telearn] 圖表已顯示,按任意鍵關閉…") + cv2.waitKey(0) + cv2.destroyAllWindows() + else: + print(f"[Telearn] 圖表已儲存: {path}") + +def load_csv(filename="taipei_weather_2026.csv"): + """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" + # === 【學員填空練習點 1:CSV 檔案安全讀取】 === + # TODO: 1. 使用 os.path.join 將當前腳本所在的目錄與檔案名稱拼接成絕對路徑 + # 提示: 當前腳本目錄為 os.path.dirname(__file__) + # 2. 使用 os.path.exists 判斷路徑是否存在,若存在則用 pd.read_csv 讀取並回傳 + path = os.path.join(os.path.dirname(__file__), filename) + + if os.path.exists(path): + return pd.read_csv(path) + else: + print("error: no files found") + +df = load_csv() + +# 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊 +plt.rcParams['font.sans-serif'] = [ + 'PingFang TC', 'Heiti TC', 'Arial Unicode MS', + 'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei', +] +plt.rcParams['axes.unicode_minus'] = False + +# === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 === +# TODO: 使用 plt.subplots 建立一個 2 列 1 行 (2x1) 的子圖畫布,尺寸大小為寬 10 英吋、高 8 英吋 +# 並指派給 fig 與 (ax1, ax2) +fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + +# 1. 繪製折線圖 (Line Plot) - 每日溫度 +# === 【學員填空練習點 3:繪製每日溫度折線圖】 === +# TODO: 1. 在 ax1 畫布上以 ax1.plot() 繪製折線圖,X軸傳入 df['Date'],Y軸傳入 df['Temperature'] +# 設定參數:帶有圓點 marker='o',顏色 color='crimson',圖例 label='溫度 (°C)',線寬 linewidth=2 +# 2. 使用 ax1.set_title 設定標題 "台北觀測站每日溫度趨勢圖" +# 3. 設定 ax1.set_xlabel("日期") 與 ax1.set_ylabel("數值") +# 4. 設定網格 ax1.grid(True, linestyle='--', alpha=0.5) +# 5. 呼叫 ax1.legend() 顯示圖例 +# 6. 設定 X軸刻度旋轉 ax1.tick_params(axis='x', rotation=45) +# +# 請在下方完成 ax1 的溫度折線圖繪製: +ax1.plot(df['Date'], df['Temperature'], marker='o', color= 'crimson',label='温度 (°C)', linewidth=2) +ax1.set_title("台北观测站每日温度趋势图", fontsize=14, fontweight='bold') +ax1.set_xlabel("日期") + +ax1.set_ylabel("数值") +ax1.grid(True,linestyle="--", alpha=0.5) +ax1.legend() +ax1.tick_params(axis='x', rotation=45) +ax1.plot(df['Date'], df['Humidity'], marker='o', color='lime', label='湿度 (%)', linewidth=2) +ax1.legend() + +# 2. 繪製條形圖 (Bar Plot) - 每日降雨量 +# === 【學員填空練習點 4:繪製每日降雨量長條圖】 === +# TODO: 1. 在 ax2 畫布上以 ax2.bar() 繪製長條圖,X軸傳入 df['Date'],Y軸傳入 df['Rainfall'] +# 設定參數:顏色 color='skyblue',外框顏色 edgecolor='royalblue',圖例 label='降雨量 (mm)' +# 2. 使用 ax2.set_title 設定標題 "台北觀測站每日降雨量分佈" +# 3. 設定 ax2.set_xlabel("日期") 與 ax2.set_ylabel("降雨量 (mm)") +# 4. 設定 Y 軸網格 ax2.grid(True, axis='y', linestyle='--', alpha=0.5) +# 5. 呼叫 ax2.legend() 顯示圖例 +# 6. 設定 X軸刻度旋轉 ax2.tick_params(axis='x', rotation=45) +# +# 請在下方完成 ax2 的降雨量長條圖繪製: +ax2.bar(df['Date'], df['Rainfall'], color='turquoise',edgecolor = 'turquoise',label='降雨量 (mm)') +ax2.set_title("台北每天降雨量长条图", fontsize=14, fontweight='bold') +ax2.set_xlabel("日期") +ax2.set_ylabel("降雨量") +ax2.grid(True, axis='y', linestyle='--', alpha=0.5) +ax2.legend() +ax2.tick_params(axis='x', rotation=45) + +plt.tight_layout() # 自動調整間距,避免文字重疊 + +# === 【學員填空練習點 5:圖表自動儲存與顯示】 === +# TODO: 1. 呼叫 plt.savefig 將畫布儲存為檔名 "taipei_weather_2026.png",並設定高畫質 dpi=300 +# (⚠️ 注意:savefig 必須在 show() 之前呼叫) +# 2. 呼叫 _telearn_plt_show() 將圖表視窗顯示在畫面上 +# +# 請在下方完成圖表存檔與顯示: +plt.savefig("taipei_weather_2026.png", dpi=300) +_telearn_plt_show() \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a4fd3671a6f3d6b1d49df8a77568c765d630e26d --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_3_visualization.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_3_visualization.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/onepiece(2).jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/onepiece(2).jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..d462e2442bb100a26a13c0dd9f1f539222ec45a8 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/onepiece(2).jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16063edf453095349df30308f927b018b8b6eaf10cdcff3fd64dbe3e5c6c0cf6 +size 80213 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\224\350\265\204\346\226\231\345\233\276\350\241\250/taipei_weather_2026.csv" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/taipei_weather_2026.csv" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\224\350\265\204\346\226\231\345\233\276\350\241\250/taipei_weather_2026.csv" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/taipei_weather_2026.csv" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\345\244\204\347\220\206/d3_1_opencv_basic.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" similarity index 72% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\345\244\204\347\220\206/d3_1_opencv_basic.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" index ca5eb71bb0d5daecfb2c765b5bee8521a8a65dc4..f981d8e3dfee8b9c76cb899abc007ce4b6f88181 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\345\244\204\347\220\206/d3_1_opencv_basic.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" @@ -1,70 +1,110 @@ -# d3_1_opencv_basic.py -# Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版) -# 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。 - -import cv2 -import numpy as np - -# 1. 影像讀取 (Read) -img = cv2.imread("onepiece(2).jpeg") -if img is None: - print("[錯誤] 無法讀取 jpg") - exit(1) - -# 預設灰階影像為原始彩色影像,避免未解開註釋時程式出錯 -gray = img - -# === 【重要觀念 TODO 1:灰階轉換 (Grayscale Conversion)】 === -# 觀念重點:大多數電腦視覺演算法在處理單通道(黑白)影像時速度更快。 -# 將 BGR 彩色圖像轉換為單通道灰階圖像,可以減少 2/3 的資料量,且能過濾掉無用的顏色雜訊。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察變為灰階後的影像。 -gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - -# 預設模糊影像為灰階影像,避免未解開註釋時程式出錯 -blurred = gray - -# === 【重要觀念 TODO 2:高斯模糊平均來平 (Gaussian Blur)】 === -# 觀念重點:高斯模糊是透過像素加權滑影像,主要目的是「消除噪點和細微反光」。 -# 這可以避免後續邊緣偵測時把細微的噪輪廓,讓偵測出來的邊緣更加平滑。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察影像邊緣的柔焦效果。 -blurred = cv2.GaussianBlur(gray, (5, 5), 0) - -# 預設邊緣影像為全黑畫布,避免未解開註釋時點誤判為程式出錯 -edges = np.zeros(img.shape[:2], dtype=np.uint8) - -# === 【重要觀念 TODO 3:Canny 邊緣偵測 (Canny Edge Detection)】 === -# 觀念重點:Canny 演算法會找出影像中明暗變化劇烈的分界線。它使用雙閥值法: -# 高於高閥值(150)的 100% 判定為邊緣;介於高低閥值(50~150)之間的若連接強邊緣則保留;其餘捨棄。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察提取出的白色線條輪廓。 -edges = cv2.Canny(blurred, 10, 80) - -# 預設無偵測到輪廓,且繪製圖像為原始圖像,避免未解開註釋時程式出錯 -contours = [] -contour_img = img.copy() - -# === 【重要觀念 TODO 4:尋找與繪製輪廓 (Find & Draw Contours)】 === -# 觀念重點:`findContours` 會在二值化(邊緣)影像中尋找相連的邊界,並建立幾何結構。 -# `drawContours` 則會將這些幾何結構繪製到指定的影像上(此處使用綠色框繪製於原始彩色影像)。 -# 請解開下方所有註釋 (刪除行首的 #) 並執行,觀察圖形外框被綠線框起來的結果。 -contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) -print(f"在影像中偵測到 {len(contours)} 個獨立輪廓特徵。") -contour_img = img.copy() -cv2.drawContours(contour_img, contours, -1, (173,216,203), 1) -alpha=1 - -# === 【重要觀念 TODO 5:視窗顯示與釋放 (Window Display)】 === -# 觀念重點:使用 cv2.imshow 顯示各步驟處理結果,並用 waitKey(0) 讓程式停下等待按鍵, -# 最後使用 destroyAllWindows 釋放視窗資源。 -# 請解開下方所有註釋 (刪除行首的 #),以觀看影像處理的成果! -print("\n正在顯示影像處理結果(請點擊影像視窗並按下任意鍵關閉)...") -cv2.imshow("Original Shapes", img) -cv2.imshow("Grayscale", gray) -cv2.imshow("Canny Edges", edges) -cv2.imshow("Contours Output", contour_img) - -success_gray = cv2.imwrite("output_gray.jpg", gray) -success_edges = cv2.imwrite("output_edges.jpg", edges) -success_contour = cv2.imwrite("output_contours.jpg", contour_img) - -cv2.waitKey(0) -cv2.destroyAllWindows() +# d3_1_opencv_basic.py +# Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版) +# 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。 + +import cv2 +import numpy as np + +# 1. 影像讀取 (Read) + +def telearn_resolve_image(name): + """Resolve image path relative to script, cwd, or student image/ folders.""" + import os + from pathlib import Path + here = Path(__file__).resolve().parent + candidates = [ + Path(name), + here / name, + here / "image" / name, + here / "照片" / name, + here.parent / "image" / name, + here.parent / "照片" / name, + here.parent / "image_create" / name, + ] + # also try basename-only search one level + for base in [here, here.parent]: + for folder in ["image", "照片", "image_create", "处理过的照片"]: + p = base / folder + if p.is_dir(): + direct = p / name + candidates.append(direct) + # case-insensitive match + low = name.lower() + try: + for f in p.iterdir(): + if f.is_file() and f.name.lower() == low: + candidates.append(f) + except OSError: + pass + for c in candidates: + try: + if c.is_file(): + print(f"[Telearn] 讀取影像: {c}") + return str(c) + except OSError: + pass + print(f"[Telearn] 找不到影像 {name},搜尋過: {[str(c) for c in candidates[:8]]}…") + return name + +img = cv2.imread(telearn_resolve_image("onepiece(2).jpeg")) +if img is None: + print("[錯誤] 無法讀取 jpg") + exit(1) + +# 預設灰階影像為原始彩色影像,避免未解開註釋時程式出錯 +gray = img + +# === 【重要觀念 TODO 1:灰階轉換 (Grayscale Conversion)】 === +# 觀念重點:大多數電腦視覺演算法在處理單通道(黑白)影像時速度更快。 +# 將 BGR 彩色圖像轉換為單通道灰階圖像,可以減少 2/3 的資料量,且能過濾掉無用的顏色雜訊。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察變為灰階後的影像。 +gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + +# 預設模糊影像為灰階影像,避免未解開註釋時程式出錯 +blurred = gray + +# === 【重要觀念 TODO 2:高斯模糊平均來平 (Gaussian Blur)】 === +# 觀念重點:高斯模糊是透過像素加權滑影像,主要目的是「消除噪點和細微反光」。 +# 這可以避免後續邊緣偵測時把細微的噪輪廓,讓偵測出來的邊緣更加平滑。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察影像邊緣的柔焦效果。 +blurred = cv2.GaussianBlur(gray, (5, 5), 0) + +# 預設邊緣影像為全黑畫布,避免未解開註釋時點誤判為程式出錯 +edges = np.zeros(img.shape[:2], dtype=np.uint8) + +# === 【重要觀念 TODO 3:Canny 邊緣偵測 (Canny Edge Detection)】 === +# 觀念重點:Canny 演算法會找出影像中明暗變化劇烈的分界線。它使用雙閥值法: +# 高於高閥值(150)的 100% 判定為邊緣;介於高低閥值(50~150)之間的若連接強邊緣則保留;其餘捨棄。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察提取出的白色線條輪廓。 +edges = cv2.Canny(blurred, 10, 80) + +# 預設無偵測到輪廓,且繪製圖像為原始圖像,避免未解開註釋時程式出錯 +contours = [] +contour_img = img.copy() + +# === 【重要觀念 TODO 4:尋找與繪製輪廓 (Find & Draw Contours)】 === +# 觀念重點:`findContours` 會在二值化(邊緣)影像中尋找相連的邊界,並建立幾何結構。 +# `drawContours` 則會將這些幾何結構繪製到指定的影像上(此處使用綠色框繪製於原始彩色影像)。 +# 請解開下方所有註釋 (刪除行首的 #) 並執行,觀察圖形外框被綠線框起來的結果。 +contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) +print(f"在影像中偵測到 {len(contours)} 個獨立輪廓特徵。") +contour_img = img.copy() +cv2.drawContours(contour_img, contours, -1, (173,216,203), 1) +alpha=1 + +# === 【重要觀念 TODO 5:視窗顯示與釋放 (Window Display)】 === +# 觀念重點:使用 cv2.imshow 顯示各步驟處理結果,並用 waitKey(0) 讓程式停下等待按鍵, +# 最後使用 destroyAllWindows 釋放視窗資源。 +# 請解開下方所有註釋 (刪除行首的 #),以觀看影像處理的成果! +print("\n正在顯示影像處理結果(請點擊影像視窗並按下任意鍵關閉)...") +cv2.imshow("Original Shapes", img) +cv2.imshow("Grayscale", gray) +cv2.imshow("Canny Edges", edges) +cv2.imshow("Contours Output", contour_img) + +success_gray = cv2.imwrite("output_gray.jpg", gray) +success_edges = cv2.imwrite("output_edges.jpg", edges) +success_contour = cv2.imwrite("output_contours.jpg", contour_img) + +cv2.waitKey(0) +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..b223ab342e5fe7b1cc69da34d1bfed35520f5217 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d3_1_opencv_basic.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd3_1_opencv_basic.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\350\231\225\347\220\206/onepiece(2).jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\350\231\225\347\220\206/onepiece(2).jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..d462e2442bb100a26a13c0dd9f1f539222ec45a8 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\275\261\345\203\217\350\231\225\347\220\206/onepiece(2).jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16063edf453095349df30308f927b018b8b6eaf10cdcff3fd64dbe3e5c6c0cf6 +size 80213 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\224\273\347\224\273\346\270\270\346\210\217/draw_game_student.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" similarity index 97% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\224\273\347\224\273\346\270\270\346\210\217/draw_game_student.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" index 96c0eff596ce08501d575376830004b6d0501690..1861f41de4eef59c3077e3bc55c06a3fdf49d4ce 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\224\273\347\224\273\346\270\270\346\210\217/draw_game_student.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" @@ -1,3 +1,4 @@ +import os import cv2 import numpy as np import mediapipe as mp @@ -14,7 +15,7 @@ from mediapipe.tasks.python import vision # 2. HandLandmarkerOptions 用於設定手部偵測的細項設定,例如 num_hands=1 代表最多偵測一隻手。 # 3. 最後透過 vision.HandLandmarker.create_from_options 建立手部偵測器物件。 # 請解開下方三行程式碼的註釋: -base_options = python.BaseOptions(model_asset_path='hand_landmarker.task') +base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hand_landmarker.task')) options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) detector = vision.HandLandmarker.create_from_options(options) diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..b4b6ab0db6832a42e6763bb130c2aa53c4003caa --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: draw_game_student.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'draw_game_student.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/onepiece(2).jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/onepiece(2).jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..d462e2442bb100a26a13c0dd9f1f539222ec45a8 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/onepiece(2).jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16063edf453095349df30308f927b018b8b6eaf10cdcff3fd64dbe3e5c6c0cf6 +size 80213 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220/d4_1_ex1_books.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220/d4_1_ex1_books.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220/d4_1_ex1_books.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220/d4_1_ex1_books.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220/d4_1_ex2_ptt.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220/d4_1_ex2_ptt.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220/d4_1_ex2_ptt.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220/d4_1_ex2_ptt.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220/d4_1_ex3_ithelp.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220/d4_1_ex3_ithelp.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220/d4_1_ex3_ithelp.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220/d4_1_ex3_ithelp.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..6b65190bcacb1447bfebdd455bd8880f07b3a67a --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220/main.py" @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""多練習選單入口(自動產生)""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +sys.path.insert(0, str(_ROOT)) + +CHOICES = { + "1": 'd4_1_ex1_books.py', + "2": 'd4_1_ex2_ptt.py', + "3": 'd4_1_ex3_ithelp.py', +} + +print("=== 本資料夾含多個練習,請選擇 ===") +print(' 1. d4_1_ex1_books.py') +print(' 2. d4_1_ex2_ptt.py') +print(' 3. d4_1_ex3_ithelp.py') +print(" (直接 Enter 預設 1)") +sel = input("編號: ").strip() or "1" +name = CHOICES.get(sel) or CHOICES.get("1") +print(f"執行: {name}") +runpy.run_path(str(_ROOT / name), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/d4_1_ex2_ptt.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/d4_1_ex2_ptt.py" new file mode 100644 index 0000000000000000000000000000000000000000..4c0ad885893936a77642ecaba8a3ac6c53c8e88a --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/d4_1_ex2_ptt.py" @@ -0,0 +1,81 @@ +# d4_1_ex2_ptt.py — Day 4 學員填空 +# cd examples/Day4/student && python d4_1_ex2_ptt.py +# 參考答案:../d4_1_ex2_ptt.py + +import requests +from bs4 import BeautifulSoup +from urllib.parse import urljoin + +# 目標網站:PTT Python 討論板首頁 +URL = "https://www.ptt.cc/bbs/Python/index.html" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 填入瀏覽器的 User-Agent,否則 PTT 會拒絕請求並回傳 403 錯誤 +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在抓取 PTT Python 板最新文章: {URL} ...") + +try: + response = requests.get(URL, headers=HEADERS, timeout=10) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 2:定位文章區塊】 === + # TODO: 觀察網頁 HTML 結構,找出所有包裹文章資訊的
標籤(其 class 名稱為何?) + posts = soup.find_all("div", class_="r-ent") + + print(f"\n成功取得 {len(posts)} 筆文章項目(含已刪除與置底公告):") + print("=" * 80) + + for idx, post in enumerate(posts, 1): + # === 【學員填空練習點 3:提取文章標題與連結並防錯 (Null Check)】 === + # TODO: + # 1. 尋找 class 為 "title" 的
標籤 + # 2. 在該
底下尋找 標籤 (即文章連結) + # 3. ⚠️ 重要思考:若文章已刪除,將不會有 標籤。請撰寫條件判斷: + # - 若 標籤存在:提取其文字,並使用 urljoin 拼接 href 屬性得到完整網址。 + # - 若 標籤不存在:直接將 title_div 的文字作為標題,並將網址設為 "無 (文章已刪除)"。 + title_div = post.find("div", class_="title") + title_link = title_div.find("a") if title_div else None + + if title_link: + title = title_link.get("title") + relative_url = title_link.get("href") + post_url = urljoin(URL, relative_url) + else: + # 文章已刪除 + title_div = post.find("div", class_="title") + title = title_div.get_text().strip() if title_div else "(已刪除)" + post_url = "無 (文章已刪除)" + + + # === 【學員填空練習點 4:提取推文數】 === + # TODO: 尋找 class 為 "nrec" 的
標籤。 + # 技巧:使用 .get_text().strip() 取得文字。若無文字(0 推),請將變數預設為 "0"。 + nrec_div = post.find("div", class_="nrec") + nrec = nrec_div.get_text().strip() if nrec_div else "0" + + # === 【學員填空練習點 5:提取作者與發文日期】 === + # TODO: 分別尋找 class 為 "author" 與 "date" 的
標籤,並提取純文字(記得使用 .strip() 去除空白) + author = post.find("div", class_="author").get_text().strip() + date = post.find("div", class_="date").get_text().strip() + + # 輸出單篇文章資訊 + print(f"[{idx}] 推文數: {nrec:3s} | 發文日期: {date} | 作者: {author}") + print(f" 標題: {title}") + print(f" 網址: {post_url}") + print("-" * 80) + +except requests.exceptions.HTTPError as http_err: + print(f"[錯誤] PTT 伺服器拒絕或找不到網頁:{http_err}") +except requests.exceptions.RequestException as e: + print(f"[錯誤] 網路請求失敗:{e}") +except Exception as ex: + print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..e271a2a8c0d74bb4e05e2cc91e7720a53098f5cf --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex2_ptt.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex2_ptt.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_books/d4_1_ex1_books.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_books/d4_1_ex1_books.py" new file mode 100644 index 0000000000000000000000000000000000000000..7b20bb8c1ce86a4d012d8b072db8a037636431d9 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_books/d4_1_ex1_books.py" @@ -0,0 +1,94 @@ +# d4_1_ex1_books.py — Day 4 學員填空 +# cd examples/Day4/student && python d4_1_ex1_books.py +# 參考答案:../d4_1_ex1_books.py + +import requests +from bs4 import BeautifulSoup +from urllib.parse import urljoin + +# 目標網站:專供爬蟲練習的圖書商城沙盒 +URL = "http://books.toscrape.com/" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 建立 headers 字典,填入 "User-Agent" 來模擬真實瀏覽器(降低被簡易防爬機制拒絕的機率) +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在連線至圖書沙盒網站: {URL} ...") + +try: + # 發送 GET 請求取得網頁 HTML + response = requests.get(URL, headers=HEADERS, timeout=10) + + # 檢查 HTTP 狀態碼是否為 200 (OK),非 200 時引發 HTTPError 異常 + response.raise_for_status() + + # 解決編碼問題:強制將解碼設定為 utf-8 (避免 requests 預設 ISO-8859-1 解碼導致英鎊符號亂碼且在 Windows 印出時崩潰) + response.encoding = response.apparent_encoding + + print("連線成功!開始解析書籍資料...\n") + + # 初始化 BeautifulSoup 解析器 + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 2:定位 HTML 標籤區塊】 === + # TODO: 找出網頁中所有書籍的容器。每本書都被包在一個
標籤中,且該標籤的 class 固定為 "product_pod" + books = soup.find_all("article", class_="product_pod") + + print(f"成功在本頁找到 {len(books)} 本書籍:") + print("=" * 70) + + for idx, book in enumerate(books, 1): + # === 【學員填空練習點 3:提取書籍名稱與連結】 === + # TODO: + # 1. 尋找

底下的 標籤 + # 2. 取得 的 "title" 屬性(因為畫面上長度限制,.get_text() 可能會被 ellipsis 截斷) + # 3. 取得 的 "href" 屬性並使用 urljoin 拼接成完整網址 + title_link = book.find("h3").find("a") + title = title_link.get("title") # 取得屬性 'title' 的值,避免拿到被截斷的文字 + + relative_url = title_link.get("href") + detail_url = urljoin(URL, relative_url) + + # === 【學員填空練習點 4:提取書籍價格】 === + # 提示:為了避免 Windows (CP950) 終端機不支援英鎊符號 '£' 而編碼崩潰,請加上 .replace("£", "GBP") 將其替換 + # 觀察:價格存放在 class 為 "price_color" 的

標籤中,格式如 "£51.77" + # 技巧:將英鎊符號 '£' 替換為 'GBP',避免在 Windows (CP950) 終端機印出時因不支援該字元而崩潰 + price_text = book.find("p", class_="price_color").get_text().replace("£", "GBP") + + # === 【學員填空練習點 5:提取評價星等 (Rating)】 === + # TODO: + # 1. 尋找 class 為 "star-rating" 的

標籤 + # 2. 取得該標籤的 "class" 屬性值(這會是一個串列,如 ['star-rating', 'Three']) + # 3. 提取該串列的第二個元素作為星等字串 (若存在) + + rating_tag = book.find("p", class_="star-rating") + rating_classes = rating_tag.get("class") + rating = "無" + if len(rating_classes) > 1: + rating = rating_classes[1] # 提取例如 'Three' (代表三顆星) + + # === 【學員填空練習點 6:提取庫存狀態】 === + # TODO: 尋找 class 為 "instock availability" 的

標籤,取得文字後去除首尾空白 (.strip()) + stock_text = book.find("p", class_="instock availability").get_text().strip() + + # 印出解析結果 + print(f"【編號 {idx}】") + print(f"[書名] {title}") + print(f"[價格] {price_text}") + print(f"[評價] {rating} 星") + print(f"[狀態] {stock_text}") + print(f"[連結] {detail_url}") + print("-" * 70) + +except requests.exceptions.HTTPError as http_err: + print(f"[錯誤] HTTP 連線異常:{http_err}") +except requests.exceptions.RequestException as e: + print(f"[錯誤] 網路請求失敗:{e}") +except Exception as ex: + print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..afd501d3f608ba4c046ff4983d27e9763690b3b7 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex1_books.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex1_books.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/d4_1_ex3_ithelp.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/d4_1_ex3_ithelp.py" new file mode 100644 index 0000000000000000000000000000000000000000..c0299624ce527f07bab03c30f85184640b1ba40c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/d4_1_ex3_ithelp.py" @@ -0,0 +1,98 @@ +# d4_1_ex3_ithelp.py — Day 4 學員填空 +# cd examples/Day4/student && python d4_1_ex3_ithelp.py +# 參考答案:../d4_1_ex3_ithelp.py + +import requests +from bs4 import BeautifulSoup + +# 目標網站:iT 邦幫忙技術文章首頁 +URL = "https://ithelp.ithome.com.tw/articles?tab=tech" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 填入瀏覽器的 User-Agent,否則可能被伺服器拒絕連線 +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在連線至 iT 邦幫忙並抓取技術文章: {URL} ...") + +try: + response = requests.get(URL, headers=HEADERS, timeout=10) + response.raise_for_status() + + print("連線成功!解析文章列表中...\n") + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 2:定位文章卡片區塊】 === + # TODO: 尋找所有包裹文章資訊的

標籤(其 class 為 "qa-list") + articles = soup.find_all("div", class_="qa-list") + + print(f"成功取得前 {len(articles)} 篇熱門技術文章:") + print("=" * 80) + + for idx, art in enumerate(articles, 1): + # === 【學員填空練習點 3:提取標題與超連結】 === + # TODO: + # 1. 在 art 中尋找 class 為 "qa-list__title-link" 的 標籤 + # 2. 提取其純文字內容並去除首尾空白 (.strip()),作為標題 + # 3. 提取其 "href" 屬性,作為文章連結 + title_link = art.find("a", class_="qa-list__title-link") + if not title_link: + continue + title = title_link.get_text().strip() + post_url = title_link.get("href").strip() + + # === 【學員填空練習點 4:提取作者名稱】 === + # TODO: + # 1. 尋找 class 為 "qa-list__info" 的
標籤 + # 2. 在該
下尋找 class 為 "qa-list__info-link" 的 標籤 + # 3. 提取其文字內容作為作者名稱 + info_div = art.find("div", class_="qa-list__info") + author = "未知作者" + if info_div: + author_link = info_div.find("a", class_="qa-list__info-link") + if author_link: + author = author_link.get_text().strip() + + # === 【學員填空練習點 5:提取發文時間】 === + # TODO: 尋找 class 為 "qa-list__info-time" 的 標籤,並提取其 "title" 屬性值(因 title 屬性中包含完整時間秒數) + time_link = art.find("div", class_="qa-list__info-time") + post_time = time_link.get("title") if time_link else "未知時間" + + # === 【學員填空練習點 6:利用 List 索引提取多項數據】 === + # TODO: + # 1. 使用 find_all 找出卡片中所有 class 為 "qa-condition" 的 標籤 + # 2. 當抓取到的數量大於等於 3 時,利用 List 索引取得對應的資訊: + # - 索引 0 的項目底下的 "qa-condition__count" :Like 數 + # - 索引 1 的項目底下的 "qa-condition__count" :回應數 + # - 索引 2 的項目底下的 "qa-condition__count" :瀏覽數 + conditions = art.find_all("a", class_="qa-condition") + + likes = "0" + comments = "0" + views = "0" + + if len(conditions) >= 3: + # 請在此處填入利用 conditions[0]、conditions[1]、conditions[2] 尋找 span 的代碼 + likes = conditions[0].find("span", class_="qa-condition__count").get_text().strip() + comments = conditions[1].find("span", class_="qa-condition__count").get_text().strip() + views = conditions[2].find("span", class_="qa-condition__count").get_text().strip() + + # 格式化輸出 + print(f"【排名 {idx}】") + print(f"[標題] {title}") + print(f"[作者] {author} | [時間] {post_time}") + print(f"[數據] Like: {likes} | 回應: {comments} | 瀏覽/訂閱: {views}") + print(f"[連結] {post_url}") + print("-" * 80) + +except requests.exceptions.HTTPError as http_err: + print(f"[錯誤] 伺服器回應異常:{http_err}") +except requests.exceptions.RequestException as e: + print(f"[錯誤] 網路請求失敗:{e}") +except Exception as ex: + print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..2ee124121c9acf2163ab18a3f956886b02cea663 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex3_ithelp.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex3_ithelp.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220\344\270\216AI\346\212\245\345\221\212/d4_1_web_scraping.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_1_web_scraping.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220\344\270\216AI\346\212\245\345\221\212/d4_1_web_scraping.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_1_web_scraping.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220\344\270\216AI\346\212\245\345\221\212/d4_2_agent_task_chain.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_agent_task_chain.py" similarity index 80% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220\344\270\216AI\346\212\245\345\221\212/d4_2_agent_task_chain.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_agent_task_chain.py" index 00097ba707ea6242699529b0269de4855352fbff..ba5abb52511b3278d2dfb38e2aea1466f443fe7b 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220\344\270\216AI\346\212\245\345\221\212/d4_2_agent_task_chain.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_agent_task_chain.py" @@ -7,8 +7,39 @@ import os import requests from bs4 import BeautifulSoup + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") +api_key = _telearn_require_gemini_api_key(allow_skip=True) HEADERS = { "User-Agent": ( @@ -96,7 +127,7 @@ def run_task_chain(topic): f"- 模擬數據:\"Love is all, love is new.\" -- John Lennon\n\n" f"## 二、 核心思想分析\n" f"本機模擬分析指出,此主題的語錄主要著眼於人與人之間的連結與心靈啟發。\n" - f"*(注意:此為 Mock 離線生成報告,設定 GEMINI_API_KEY 以啟用 Gemini 真實生成。)*\n" + f"*(注意:此為 Mock 離線生成報告,於啟動時輸入 GEMINI_API_KEY 以啟用 Gemini 真實生成。)*\n" ) else: print(f"\n[步驟 2] 正在調用 Gemini ({GEMINI_MODEL}) 進行深度分析與排版...") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220\344\270\216AI\346\212\245\345\221\212/d4_2_ex2_crawler_ai.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" similarity index 88% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220\344\270\216AI\346\212\245\345\221\212/d4_2_ex2_crawler_ai.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" index f4e46c1409927f15a2eef9bdae4d30ab738be660..c31122af949b01a60504a43c5a461644fed933ab 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\231\253\345\210\206\346\236\220\344\270\216AI\346\212\245\345\221\212/d4_2_ex2_crawler_ai.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" @@ -9,8 +9,39 @@ from urllib.parse import urljoin from google import genai # 設定預設使用的 Gemini 模型 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") +api_key = _telearn_require_gemini_api_key(allow_skip=True) HEADERS = { "User-Agent": ( @@ -176,7 +207,7 @@ def run_analysis_chain(source_type): # 步驟 2: AI 分析 (或 Mock 降級防禦) if not api_key: - print("\n[步驟 2] 未偵測到環境變數 GEMINI_API_KEY,自動啟動 Mock 離線降級機制...") + print("\n[步驟 2] 未輸入 GEMINI_API_KEY,啟動 Mock 離線降級機制...") # === 【學員填空練習點 5:Mock 降級防衛機制】 === # TODO 5: 請設計一段模擬的 Markdown 格式分析報告字串,指派給 report_content。 # 報告內容至少需要包含標題與數據摘要,避免程式在無 Key 時報錯中斷。 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a5ae937359f5ab6c3d6b6730ee4cf5c043ea685a --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_2_ex2_crawler_ai.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_2_ex2_crawler_ai.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..6d9f8e43cbdcde83518590b0a762af898547ad23 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step0_eyeguard_template.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step0_eyeguard_template.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\234\274\347\235\233\350\267\235\347\246\273\344\276\246\346\265\213/step0_eyeguard_template.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" similarity index 98% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\234\274\347\235\233\350\267\235\347\246\273\344\276\246\346\265\213/step0_eyeguard_template.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" index b21522bd482959dc1a7ba0aaa8bb3eb40382a5a7..ec50c1b4a141e47beed31169b5d29bc8740bd89f 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\234\274\347\235\233\350\267\235\347\246\273\344\276\246\346\265\213/step0_eyeguard_template.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" @@ -45,7 +45,7 @@ cap = cv2.VideoCapture(0) # 沒有攝影機就改成影片:cap = cv2.VideoCapture("backup_face_video.mp4") # 【TODO 1】初始化 MediaPipe 臉部偵測 (Face Landmarker) -base_options = python.BaseOptions(model_asset_path=os.path.join('face_landmarker.task')) +base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task')) options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) detector = vision.FaceLandmarker.create_from_options(options) diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\204\270\351\203\250\344\276\246\346\265\213/face_mesh_connections.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/face_mesh_connections.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\204\270\351\203\250\344\276\246\346\265\213/face_mesh_connections.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/face_mesh_connections.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..6e10ef440ce9cdd4de5bb4ee95fde52ce4fb20ce --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step2_facemesh_complete.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step2_facemesh_complete.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/onepiece(2).jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/onepiece(2).jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..d462e2442bb100a26a13c0dd9f1f539222ec45a8 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/onepiece(2).jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16063edf453095349df30308f927b018b8b6eaf10cdcff3fd64dbe3e5c6c0cf6 +size 80213 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\204\270\351\203\250\344\276\246\346\265\213/step1_facemesh.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/step1_facemesh.py" similarity index 96% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\204\270\351\203\250\344\276\246\346\265\213/step1_facemesh.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/step1_facemesh.py" index 33a62fd7dffe57091f640903108f175bac654a5b..4074dcccde2f51c5ce39f31fa90921ae9b31678e 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\204\270\351\203\250\344\276\246\346\265\213/step1_facemesh.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/step1_facemesh.py" @@ -1,111 +1,111 @@ -""" -Week 11 - Step 1: Face Mesh Tracking with Connections (Template 填空版) -=================================================================== -本步驟目標: - 1. 使用 OpenCV 開啟視訊鏡頭 - 2. 載入 MediaPipe 臉部特徵偵測器 (face_landmarker.task) - 3. 偵測人臉,並在臉上畫出綠色的 3D 特效網格線與黃色特徵點 -""" -import cv2 -import mediapipe as mp -import numpy as np -import sys -import os - -# 1. 初始化攝影機 -cap = cv2.VideoCapture(0) -if not cap.isOpened(): - print("Error: Could not open camera.") - sys.exit() - -# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) -model_path = os.path.join('face_landmarker.task') -if not os.path.exists(model_path): - print(f"Error: Model file not found at {model_path}") - print("Please make sure face_landmarker.task is in the examples folder.") - sys.exit() - -try: - from mediapipe.tasks import python - from mediapipe.tasks.python import vision - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) - detector = vision.FaceLandmarker.create_from_options(options) - print("Face Landmarker Tasks API initialized successfully.") -except Exception as e: - print(f"Error initializing detector: {e}") - sys.exit() - -# 匯入臉部網格連接點定義 -try: - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - from face_mesh_connections import FACEMESH_TESSELATION -except ImportError: - FACEMESH_TESSELATION = [] - -# 繪圖模式:0 = 僅畫網格線, 1 = 僅畫點點, 2 = 點線都畫 -draw_mode = 0 - -print("Press 'q' in the window to exit, or 'm' to toggle display modes.") - -# 3. 主迴圈 -while True: - success, img = cap.read() - if not success: - print("Error: Failed to read frame.") - break - - # 【TODO 1】畫面水平翻轉,並將 BGR 轉成 MediaPipe 需要的 RGB 格式 - img = cv2.flip(img, 1) - img_h, img_w, _ = img.shape - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - - - # 執行臉部偵測 - results = detector.detect(mp_image) - - # 【TODO 2】如果偵測到臉部,畫出 3D 特效網格線 - # 提示:取出 results.face_landmarks[0],並用迴圈巡覽 FACEMESH_TESSELATION - # 點的座標 (lm.x, lm.y) 是 0~1 的比例,需要乘上 img_w 和 img_h 換算成像素座標 - if results.face_landmarks: - face_lms = results.face_landmarks[0] - num_lms = len(face_lms) - - if draw_mode == 0 or draw_mode == 2: - for start_idx, end_idx in FACEMESH_TESSELATION: - if start_idx < num_lms and end_idx < num_lms: - lm1 = face_lms[start_idx] - lm2 = face_lms[end_idx] - pt1 = (int(lm1.x * img_w), int(lm1.y * img_h)) - pt2 = (int(lm2.x * img_w), int(lm2.y * img_h)) - cv2.line(img, pt1, pt2, (224, 224, 0), 1) # 青綠色線條 - - - # 【TODO 3】畫出臉部 468 個黃色特徵點點 - if draw_mode == 1 or draw_mode == 2: - for lm in face_lms: - cx = int(lm.x * img_w) - cy = int(lm.y * img_h) - cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) # 黃色小圓點 - - - - mode_str = "Mesh" if draw_mode == 0 else "Dots" if draw_mode == 1 else "Both" - cv2.putText(img, f"Face Detected | Mode: {mode_str}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) - else: - cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) - - # 顯示操作提示文字 - cv2.putText(img, "Press 'm' to toggle mode | 'q' to quit", (10, img_h - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1) - - cv2.imshow("Face Mesh Connections", img) - - key = cv2.waitKey(1) & 0xFF - if key == ord('q'): - break - elif key == ord('m'): - draw_mode = (draw_mode + 1) % 3 - -cap.release() -cv2.destroyAllWindows() +""" +Week 11 - Step 1: Face Mesh Tracking with Connections (Template 填空版) +=================================================================== +本步驟目標: + 1. 使用 OpenCV 開啟視訊鏡頭 + 2. 載入 MediaPipe 臉部特徵偵測器 (face_landmarker.task) + 3. 偵測人臉,並在臉上畫出綠色的 3D 特效網格線與黃色特徵點 +""" +import cv2 +import mediapipe as mp +import numpy as np +import sys +import os + +# 1. 初始化攝影機 +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("Error: Could not open camera.") + sys.exit() + +# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) +model_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task') +if not os.path.exists(model_path): + print(f"Error: Model file not found at {model_path}") + print("Please make sure face_landmarker.task is in the examples folder.") + sys.exit() + +try: + from mediapipe.tasks import python + from mediapipe.tasks.python import vision + base_options = python.BaseOptions(model_asset_path=model_path) + options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) + detector = vision.FaceLandmarker.create_from_options(options) + print("Face Landmarker Tasks API initialized successfully.") +except Exception as e: + print(f"Error initializing detector: {e}") + sys.exit() + +# 匯入臉部網格連接點定義 +try: + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + from face_mesh_connections import FACEMESH_TESSELATION +except ImportError: + FACEMESH_TESSELATION = [] + +# 繪圖模式:0 = 僅畫網格線, 1 = 僅畫點點, 2 = 點線都畫 +draw_mode = 0 + +print("Press 'q' in the window to exit, or 'm' to toggle display modes.") + +# 3. 主迴圈 +while True: + success, img = cap.read() + if not success: + print("Error: Failed to read frame.") + break + + # 【TODO 1】畫面水平翻轉,並將 BGR 轉成 MediaPipe 需要的 RGB 格式 + img = cv2.flip(img, 1) + img_h, img_w, _ = img.shape + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + + + # 執行臉部偵測 + results = detector.detect(mp_image) + + # 【TODO 2】如果偵測到臉部,畫出 3D 特效網格線 + # 提示:取出 results.face_landmarks[0],並用迴圈巡覽 FACEMESH_TESSELATION + # 點的座標 (lm.x, lm.y) 是 0~1 的比例,需要乘上 img_w 和 img_h 換算成像素座標 + if results.face_landmarks: + face_lms = results.face_landmarks[0] + num_lms = len(face_lms) + + if draw_mode == 0 or draw_mode == 2: + for start_idx, end_idx in FACEMESH_TESSELATION: + if start_idx < num_lms and end_idx < num_lms: + lm1 = face_lms[start_idx] + lm2 = face_lms[end_idx] + pt1 = (int(lm1.x * img_w), int(lm1.y * img_h)) + pt2 = (int(lm2.x * img_w), int(lm2.y * img_h)) + cv2.line(img, pt1, pt2, (224, 224, 0), 1) # 青綠色線條 + + + # 【TODO 3】畫出臉部 468 個黃色特徵點點 + if draw_mode == 1 or draw_mode == 2: + for lm in face_lms: + cx = int(lm.x * img_w) + cy = int(lm.y * img_h) + cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) # 黃色小圓點 + + + + mode_str = "Mesh" if draw_mode == 0 else "Dots" if draw_mode == 1 else "Both" + cv2.putText(img, f"Face Detected | Mode: {mode_str}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + else: + cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) + + # 顯示操作提示文字 + cv2.putText(img, "Press 'm' to toggle mode | 'q' to quit", (10, img_h - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1) + + cv2.imshow("Face Mesh Connections", img) + + key = cv2.waitKey(1) & 0xFF + if key == ord('q'): + break + elif key == ord('m'): + draw_mode = (draw_mode + 1) % 3 + +cap.release() +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\204\270\351\203\250\344\276\246\346\265\213/step2_facemesh_complete.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/step2_facemesh_complete.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\204\270\351\203\250\344\276\246\346\265\213/step2_facemesh_complete.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\207\211\351\203\250\345\201\265\346\270\254/step2_facemesh_complete.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\204\347\220\206\350\277\207\347\232\204\347\205\247\347\211\207/output_contours.jpg" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\231\225\347\220\206\351\201\216\347\232\204\347\205\247\347\211\207/output_contours.jpg" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\204\347\220\206\350\277\207\347\232\204\347\205\247\347\211\207/output_contours.jpg" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\231\225\347\220\206\351\201\216\347\232\204\347\205\247\347\211\207/output_contours.jpg" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\204\347\220\206\350\277\207\347\232\204\347\205\247\347\211\207/output_edges.jpg" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\231\225\347\220\206\351\201\216\347\232\204\347\205\247\347\211\207/output_edges.jpg" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\204\347\220\206\350\277\207\347\232\204\347\205\247\347\211\207/output_edges.jpg" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\231\225\347\220\206\351\201\216\347\232\204\347\205\247\347\211\207/output_edges.jpg" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\204\347\220\206\350\277\207\347\232\204\347\205\247\347\211\207/output_gray.jpg" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\231\225\347\220\206\351\201\216\347\232\204\347\205\247\347\211\207/output_gray.jpg" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\345\244\204\347\220\206\350\277\207\347\232\204\347\205\247\347\211\207/output_gray.jpg" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\231\225\347\220\206\351\201\216\347\232\204\347\205\247\347\211\207/output_gray.jpg" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\256\276\350\256\241AI\350\247\222\350\211\262&\345\234\272\346\231\257/d2_2_prompt_engineering.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" similarity index 55% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\256\276\350\256\241AI\350\247\222\350\211\262&\345\234\272\346\231\257/d2_2_prompt_engineering.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" index aa4416ca9cf3e31f75e5bd8dfccaf1726610946a..de012795ad16d290ceb11c9c0bcee5e4043d81f8 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\256\276\350\256\241AI\350\247\222\350\211\262&\345\234\272\346\231\257/d2_2_prompt_engineering.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" @@ -1,56 +1,84 @@ -# d2_2_prompt_engineering.py -# Day 2: 生成式 AI 串接實戰 - Prompt 工程 (系統指令與結構化輸出) -import os -from google import genai - -# 取得 API 金鑰 -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") -if not api_key: - print("[提示] 請輸入您的 Google AI Studio API 金鑰:") - api_key = input("API Key: ").strip() - -client = genai.Client(api_key=api_key) - -# 1. 設置系統指令 (System Instruction / System Prompt) -# TODO 1: -system_prompt = "你是AI國中英語老師,请输入你想要翻译的中文单字" - -print("正在初始化强制英語教學模型...") - -# 2. Few-shot (少樣本) 引導與對話演練 -# TODO 2: -print(f"\n[學生輸入]: ") -user_word = input().strip() -prompt = f"請幫我翻譯這個詞:'{user_word}'。" - -response = client.models.generate_content( - model='gemini-3.1-flash-lite', - contents=prompt, - config={'system_instruction': system_prompt} -) - -print("\n[AI 老師回應]:") -print(response.text) - - -# 3. 結構化 JSON 輸出 (Structured Output) 範例 -print("\n" + "="*40) -print("進階挑戰:要求模型回傳 JSON 格式") -print("="*40) - -# 我們可以在 prompt 中要求,並透過 configuration 設定 JSON 輸出格式 -# TODO 3: -json_prompt = ( - f"請將中文單字 '{user_word}' 的翻譯結果整理成 JSON 格式,包含以下三個欄位:\n" - "- 'english': 英文單字\n" - "- 'kk_phonetic': KK音標\n" - "- 'example_sentence': 適合國中生理解的英文例句與中文翻譯" -) - -response = client.models.generate_content( - model='gemini-3.1-flash-lite', - contents=json_prompt, - config={"response_mime_type": "application/json"} -) -print("\n[AI 回傳的原始 JSON]:") -print(response.text) +# d2_2_prompt_engineering.py +# Day 2: 生成式 AI 串接實戰 - Prompt 工程 (系統指令與結構化輸出) +import os +from google import genai + +# 取得 API 金鑰 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +api_key = _telearn_require_gemini_api_key(allow_skip=False) + +client = genai.Client(api_key=api_key) + +# 1. 設置系統指令 (System Instruction / System Prompt) +# TODO 1: +system_prompt = "你是AI國中英語老師,请输入你想要翻译的中文单字" + +print("正在初始化强制英語教學模型...") + +# 2. Few-shot (少樣本) 引導與對話演練 +# TODO 2: +print(f"\n[學生輸入]: ") +user_word = input().strip() +prompt = f"請幫我翻譯這個詞:'{user_word}'。" + +response = client.models.generate_content( + model='gemini-3.1-flash-lite', + contents=prompt, + config={'system_instruction': system_prompt} +) + +print("\n[AI 老師回應]:") +print(response.text) + + +# 3. 結構化 JSON 輸出 (Structured Output) 範例 +print("\n" + "="*40) +print("進階挑戰:要求模型回傳 JSON 格式") +print("="*40) + +# 我們可以在 prompt 中要求,並透過 configuration 設定 JSON 輸出格式 +# TODO 3: +json_prompt = ( + f"請將中文單字 '{user_word}' 的翻譯結果整理成 JSON 格式,包含以下三個欄位:\n" + "- 'english': 英文單字\n" + "- 'kk_phonetic': KK音標\n" + "- 'example_sentence': 適合國中生理解的英文例句與中文翻譯" +) + +response = client.models.generate_content( + model='gemini-3.1-flash-lite', + contents=json_prompt, + config={"response_mime_type": "application/json"} +) +print("\n[AI 回傳的原始 JSON]:") +print(response.text) diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..5f819ed5e0570c50eae23f77a85ad0660170f673 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_2_prompt_engineering.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_2_prompt_engineering.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\265\204\346\226\231\345\274\202\345\270\270\345\200\274\346\270\205\346\264\227/d1_2_data_cleaning.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/d1_2_data_cleaning.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\265\204\346\226\231\345\274\202\345\270\270\345\200\274\346\270\205\346\264\227/d1_2_data_cleaning.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/d1_2_data_cleaning.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..69cdb69578cc4ce89cad8c1538eb2191bd6e5200 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_2_data_cleaning.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_2_data_cleaning.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\265\204\346\226\231\345\274\202\345\270\270\345\200\274\346\270\205\346\264\227/taipei_weather_2026.csv" "b/projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/taipei_weather_2026.csv" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\265\204\346\226\231\345\274\202\345\270\270\345\200\274\346\270\205\346\264\227/taipei_weather_2026.csv" rename to "projects/2026_AI\347\207\2371/\345\212\211\345\256\227\347\251\216/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/taipei_weather_2026.csv" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" index ee76f4034a9e8f8b5ea7fc947f0c08f9ef7778ec..578c05fbcf944e36d81eb9fb59362e280e556077 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" @@ -1,34 +1,35 @@ -# d1_1_pandas_intro.py -# Day 1 學員填空:Pandas 基礎(完成 TODO 後執行) -# cd examples/Day1/student && python3 d1_1_pandas_intro.py -# 參考答案:../Teacher/d1_1_pandas_intro.py - -import pandas as pd - -df = pd.read_csv("taipei_weather_2026.csv") -print("=== 數據載入成功! ===") - -print("\n--- 1. head ---") -print(df.head()) - -print("\n--- 2. info ---") -df.info() - -print("\n--- 3. describe ---") -print(df.describe()) - -print("\n--- 4. Temperature 前 5 筆 ---") -print(df['Temperature'].head()) - -print("\n--- 5. Date 與 Rainfall ---") -print(df[['Date', 'Rainfall']].head()) - -print("\n--- 挑戰:最高溫與平均濕度 ---") -max_temp = df['Temperature'].max() -avg_humidity = df['Humidity'].mean() -print(f"最高溫度:{max_temp}°C") -print(f"平均濕度:{avg_humidity:.2f}%") -min_Rainfall = df['Rainfall'].min() -print(f"平均濕度:{min_Rainfall:.2f}%") -egyptian_good_weather =df[(df["Temperature"]>27) & (df["Rainfall"]==0)] +import os +# d1_1_pandas_intro.py +# Day 1 學員填空:Pandas 基礎(完成 TODO 後執行) +# cd examples/Day1/student && python3 d1_1_pandas_intro.py +# 參考答案:../Teacher/d1_1_pandas_intro.py + +import pandas as pd + +df = pd.read_csv(os.path.join(os.path.dirname(__file__), "taipei_weather_2026.csv")) +print("=== 數據載入成功! ===") + +print("\n--- 1. head ---") +print(df.head()) + +print("\n--- 2. info ---") +df.info() + +print("\n--- 3. describe ---") +print(df.describe()) + +print("\n--- 4. Temperature 前 5 筆 ---") +print(df['Temperature'].head()) + +print("\n--- 5. Date 與 Rainfall ---") +print(df[['Date', 'Rainfall']].head()) + +print("\n--- 挑戰:最高溫與平均濕度 ---") +max_temp = df['Temperature'].max() +avg_humidity = df['Humidity'].mean() +print(f"最高溫度:{max_temp}°C") +print(f"平均濕度:{avg_humidity:.2f}%") +min_Rainfall = df['Rainfall'].min() +print(f"平均濕度:{min_Rainfall:.2f}%") +egyptian_good_weather =df[(df["Temperature"]>27) & (df["Rainfall"]==0)] print(egyptian_good_weather) \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..19029aca2c352f92bc7a6ace63c7161bf8b10e90 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_1_pandas_intro.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_1_pandas_intro.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" index 04d8e32a00d4299d5a04b71cf3bed2e19e90df82..0fb90c000540dd1446bb36df34b5348c133ab248 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" @@ -1,41 +1,63 @@ -# d2_1_gemini.py — Day2 學員填空 -# cd examples/Day2/Student && python3 d2_1_gemini.py -# 參考答案:../Teacher/d2_1_gemini.py - -import os -from google import genai - -# === 【學員填空練習點 1:設置與載入 API Key】 === -# TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。 -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6J-S5dtl4YCuomoO9V4L5YAgTzLh9E8vJUuScN0c4VyKg") - -if not api_key: - print("[提示] 未檢測到環境變數 GEMINI_API_KEY。") - print(" 請先設定:export GEMINI_API_KEY=\"your_key\"") - print(" 或於下方手動輸入(僅供本機測試,勿寫死在檔案中上傳):") - api_key = input("API Key: ").strip() - if not api_key: - print("[結束] 未提供 API Key。可改練習 d2_1_ollama_local.py。") - exit(0) - -# === 【學員填空練習點 2:設定模型名稱】 === -# TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite') -model_name = "gemini-3.1-flash-lite" - -print(f"正在連線至 Google Cloud Model ({model_name})...") - -# === 【學員填空練習點 3:初始化 Gemini Client】 === -# TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數 -client = genai.Client(api_key=api_key) - -# === 【學員填空練習點 4:發送內容生成請求】 === -# TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞 -# 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。 -response = client.models.generate_content(model= model_name, contents="請以繁體中文解釋什麼是『AI』,字數約 100 字。") - - -# === 【學員填空練習點 5:讀取並印出回應】 === -# TODO: 從 response 物件中取得 text 欄位內容,並列印出來。 -print("\n=== AI 回應結果 ===") -# 請在下方實作列印邏輯: +# d2_1_gemini.py — Day2 學員填空 +# cd examples/Day2/Student && python3 d2_1_gemini.py +# 參考答案:../Teacher/d2_1_gemini.py + +import os +from google import genai + +# === 【學員填空練習點 1:設置與載入 API Key】 === +# TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +api_key = _telearn_require_gemini_api_key(allow_skip=False) + +# === 【學員填空練習點 2:設定模型名稱】 === +# TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite') +model_name = "gemini-3.1-flash-lite" + +print(f"正在連線至 Google Cloud Model ({model_name})...") + +# === 【學員填空練習點 3:初始化 Gemini Client】 === +# TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數 +client = genai.Client(api_key=api_key) + +# === 【學員填空練習點 4:發送內容生成請求】 === +# TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞 +# 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。 +response = client.models.generate_content(model= model_name, contents="請以繁體中文解釋什麼是『AI』,字數約 100 字。") + + +# === 【學員填空練習點 5:讀取並印出回應】 === +# TODO: 從 response 物件中取得 text 欄位內容,並列印出來。 +print("\n=== AI 回應結果 ===") +# 請在下方實作列印邏輯: print(response.text) \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..9c7cdcc41dc129b0848a190aa8fb9c425c2c376c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_1_gemini.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_1_gemini.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock_\345\205\263\351\224\256\350\251\236\346\212\223\345\217\226/d2_3_chatbot_memory_mock.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/d2_3_chatbot_memory_mock.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock_\345\205\263\351\224\256\350\251\236\346\212\223\345\217\226/d2_3_chatbot_memory_mock.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/d2_3_chatbot_memory_mock.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a7ff00b684e34d0c68bab0adcd53df7f1e52fef4 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_3_chatbot_memory_mock.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_3_chatbot_memory_mock.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock_game/d2_3_chatbot_memory_game_template.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/d2_3_chatbot_memory_game_template.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock_game/d2_3_chatbot_memory_game_template.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/d2_3_chatbot_memory_game_template.py" diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..2d6d70db2b75c24d81bbc745f45d31d25e02825f --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_3_chatbot_memory_game_template.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_3_chatbot_memory_game_template.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock&AI_\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\350\210\207AI_\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" similarity index 67% rename from "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock&AI_\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\350\210\207AI_\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" index e7e7201ede25510c0d19d22d893462c91882bcbc..b5c093255fe01c7265dd8524db3bd21780f898f2 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock&AI_\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\350\210\207AI_\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" @@ -1,113 +1,142 @@ -# d2_3_chatbot_memory.py -# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模) - -import os -from google import genai - -# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite -GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get( - "GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") - - -def local_mock_response(user_input, history): - user_input_lower = user_input.lower() - - # TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句 - if "在哪" in user_input_lower or "地点" in user_input_lower: - return "這起案件的地点在饭店,接下來去查查『地上的脚印』吧!" - elif "线索" in user_input_lower or "地上的脚印" in user_input_lower: - return "哇!地上的脚印指向了饭店的 'ROOM_238',請記好這串字,接下來該查明『小偷的是誰』了!" - elif "小偷的是誰" in user_input_lower or "是誰" in user_input_lower: - return "小偷居然是'David_8538',接下來我們該查明时间了!" - elif "时间" in user_input_lower: - return "下午3:30" - elif "提示" in user_input_lower: - return "你可以问我'地点'、'时间'、'小偷的動機'或是'犯人'。" - elif "你好" in user_input_lower: - return "大偵探!我们需要你幫忙找出小偷!" - elif "小偷的動機" in user_input_lower: - return "受害者其實是David以前的朋友,David對他懷恨在心,因為有一次她和David參加了一個游泳比賽,她為了得到第一名,拿了一杯在太陽下曬了兩個小時的奶茶給David喝,還騙他那是溫奶茶,害David一直拉肚子,沒辦法去參加游泳比賽,最後她成功拿到了第一名,而David卻一點機會也沒有。長大之後David就一直想要報復,但是一直找不到任何機會,這次是她第一次來David開的饭店,David就偷了她的東西,當作報復。" - else: - # 計算目前是第幾輪對話(一輪包含使用者輸入與客服回覆,共 2 個元素) - # Why: 藉由傳入的 history 串列長度除以 2,即可動態得知目前互動的輪數,模擬記憶狀態。 - turns = len(history) // 2 + 1 - return ( - f"(模擬模式)收到您的訊息:『{user_input}』。" - f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?" - ) - -def main(): - print("=" * 50) - print(" 得能 AI 智慧客服對話系統 (支援對話上下文記憶)") - print("=" * 50) - - use_mock = False - chat_session = None - - if not api_key: - print("[提示] 未檢測到環境變數 GEMINI_API_KEY,將自動啟用「離線模擬模式 (Local Mock Mode)」。") - print(" (如需連線雲端模型,請設定 GEMINI_API_KEY 環境變數)") - use_mock = True - else: - try: - print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...") - client = genai.Client(api_key=api_key) - # TODO: 設定系統指令 (System Instruction / System Prompt) - system_instruction = ( - "你是一位專業的侦探,語氣溫和客氣," - "小偷是David_8538" - "這起案件的地点在饭店" - "地上的脚印指向了饭店的 'ROOM_238'" - "时间是下午3:30" - "小偷跟受害者有仇" - "小偷是饭店经理" - "受害者其實是偷的老朋友" - "小偷趁著飯店清潔工的交班空檔,进到受害者的房間偷走他的東西" - "小偷偷走了她最喜歡的珍珠項鍊" - "ROOM_238剛好位於走廊盡頭,是唯一避開所有監視器的死角" - ) - - chat_session = client.chats.create( - model=GEMINI_MODEL, - config={'system_instruction': system_instruction} - ) - print("雲端客服系統已就緒!") - except Exception as e: - print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...") - use_mock = True - - local_history = [] - - print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):") - print("-" * 50) - - while True: - try: - user_input = input("\n[您]: ").strip() - if not user_input: - continue - - if user_input.lower() in ["quit", "exit"]: - print("對話結束,感謝您的使用!") - break - - if use_mock: - ai_reply = local_mock_response(user_input, local_history) - local_history.append({"role": "user", "text": user_input}) - local_history.append({"role": "model", "text": ai_reply}) - else: - response = chat_session.send_message(user_input) - ai_reply = response.text - - print(f"[AI 客服]: {ai_reply}") - - except KeyboardInterrupt: - print("\n對話強行終止。再見!") - break - except Exception as e: - print(f"\n[錯誤] 發生異常:{e}") - - -if __name__ == "__main__": - main() +# d2_3_chatbot_memory.py +# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模) + +import os +from google import genai + +# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") +api_key = _telearn_require_gemini_api_key(allow_skip=True) + + +def local_mock_response(user_input, history): + user_input_lower = user_input.lower() + + # TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句 + if "在哪" in user_input_lower or "地点" in user_input_lower: + return "這起案件的地点在饭店,接下來去查查『地上的脚印』吧!" + elif "线索" in user_input_lower or "地上的脚印" in user_input_lower: + return "哇!地上的脚印指向了饭店的 'ROOM_238',請記好這串字,接下來該查明『小偷的是誰』了!" + elif "小偷的是誰" in user_input_lower or "是誰" in user_input_lower: + return "小偷居然是'David_8538',接下來我們該查明时间了!" + elif "时间" in user_input_lower: + return "下午3:30" + elif "提示" in user_input_lower: + return "你可以问我'地点'、'时间'、'小偷的動機'或是'犯人'。" + elif "你好" in user_input_lower: + return "大偵探!我们需要你幫忙找出小偷!" + elif "小偷的動機" in user_input_lower: + return "受害者其實是David以前的朋友,David對他懷恨在心,因為有一次她和David參加了一個游泳比賽,她為了得到第一名,拿了一杯在太陽下曬了兩個小時的奶茶給David喝,還騙他那是溫奶茶,害David一直拉肚子,沒辦法去參加游泳比賽,最後她成功拿到了第一名,而David卻一點機會也沒有。長大之後David就一直想要報復,但是一直找不到任何機會,這次是她第一次來David開的饭店,David就偷了她的東西,當作報復。" + else: + # 計算目前是第幾輪對話(一輪包含使用者輸入與客服回覆,共 2 個元素) + # Why: 藉由傳入的 history 串列長度除以 2,即可動態得知目前互動的輪數,模擬記憶狀態。 + turns = len(history) // 2 + 1 + return ( + f"(模擬模式)收到您的訊息:『{user_input}』。" + f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?" + ) + +def main(): + print("=" * 50) + print(" 得能 AI 智慧客服對話系統 (支援對話上下文記憶)") + print("=" * 50) + + use_mock = False + chat_session = None + + if not api_key: + print("[提示] 未輸入 GEMINI_API_KEY,改用「離線模擬模式 (Local Mock Mode)」。") + use_mock = True + else: + try: + print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...") + client = genai.Client(api_key=api_key) + # TODO: 設定系統指令 (System Instruction / System Prompt) + system_instruction = ( + "你是一位專業的偵探,語氣溫和客氣。" + "小偷是David_8538。" + "這起案件的地點在飯店。" + "地上的腳印指向了飯店的 'ROOM_238'。" + "時間是下午3:30。" + "小偷跟受害者有仇。" + "小偷是飯店經理。" + "受害者其實是小偷的老朋友。" + "小偷趁著飯店清潔工的交班空檔,進到受害者的房間偷走他的東西。" + "小偷偷走了她最喜歡的珍珠項鍊。" + "ROOM_238剛好位於走廊盡頭,是唯一避開所有監視器的死角。" + ) + + chat_session = client.chats.create( + model=GEMINI_MODEL, + config={'system_instruction': system_instruction} + ) + print("雲端客服系統已就緒!") + except Exception as e: + print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...") + use_mock = True + + local_history = [] + + print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):") + print("-" * 50) + + while True: + try: + user_input = input("\n[您]: ").strip() + if not user_input: + continue + + if user_input.lower() in ["quit", "exit"]: + print("對話結束,感謝您的使用!") + break + + if use_mock: + ai_reply = local_mock_response(user_input, local_history) + local_history.append({"role": "user", "text": user_input}) + local_history.append({"role": "model", "text": ai_reply}) + else: + response = chat_session.send_message(user_input) + ai_reply = response.text + + print(f"[AI 客服]: {ai_reply}") + + except KeyboardInterrupt: + print("\n對話強行終止。再見!") + break + except Exception as e: + print(f"\n[錯誤] 發生異常:{e}") + + +if __name__ == "__main__": + main() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\350\210\207AI_\351\233\231\346\250\241\345\274\217/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\350\210\207AI_\351\233\231\346\250\241\345\274\217/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..4946a0a52b48301e190fe096df87684bd366261c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/mock\350\210\207AI_\351\233\231\346\250\241\345\274\217/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_4_chatbot_memory.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_4_chatbot_memory.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" index ff37e0664180dfbe9495fc794b6b619b1261973c..5c192f6e9fa241559d4257b2eaa0acf5cf1b1d40 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" @@ -1,205 +1,205 @@ -""" -Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版) -================================================================== -本週挑戰目標: - 1. 實作「三指捏合 (Three-Finger Pinch)」: - 必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」 - 以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch - 2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊 - 3. 使用邊緣觸發 (Edge Detection) 防止重複觸發 - 4. 新增 30 秒限時機制,時間到顯示最終得分 -""" -import pygame -import cv2 -import mediapipe as mp -import math -import sys -import os -import random -from mediapipe.tasks import python -from mediapipe.tasks.python import vision -# ============================================ -# 1. 初始化 -# ============================================ -pygame.init() -WIDTH, HEIGHT = 800, 600 -screen = pygame.display.set_mode((WIDTH, HEIGHT)) -pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ") -clock = pygame.time.Clock() - -# 攝影機 -cap = cv2.VideoCapture(0) - -try: - base_options = python.BaseOptions(model_asset_path='hand_landmarker.task') - options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) - detector = vision.HandLandmarker.create_from_options(options) -except Exception as e: - print(f"Error loading model: {e}") - pygame.quit() - sys.exit() - -# ============================================ -# 2. 遊戲變數 -# ============================================ -# 手指座標 -finger_x, finger_y = 400, 300 # 食指 -thumb_x, thumb_y = 400, 300 # 大拇指 -middle_x, middle_y = 400, 300 # 中指 - -# 距離變數 -dist_thumb_index = 0.0 -dist_thumb_middle = 0.0 - -# Pinch 相關變數 -is_pinching = False -was_pinching = False # 邊緣觸發 -PINCH_THRESHOLD =40 # 距離臨界值 - -# 按鈕設定 -button_rect = pygame.Rect(300, 250, 200, 100) -button_colors = [(247, 151, 236), (3, 252, 252), - (52, 244, 3), (255, 255, 255)] -current_color_index = 0 - -# 點擊計數器 -click_count = 0 - -# --- 限時機制變數 --- -START_TIME = pygame.time.get_ticks() -GAME_DURATION = 30 # 遊戲限制時間(秒) -game_state = "PLAYING" # 狀態機 - -# ============================================ -# 3. 主迴圈 -# ============================================ -running = True -while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - # --- 讀取攝影機與手勢偵測 --- - success, img = cap.read() - is_pinching = False # 每幀重設狀態,防止手離屏時卡死 - - if success: - img = cv2.flip(img, 1) # 鏡像翻轉 - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - results = detector.detect(mp_image) - - # === 【TODO 2:挑戰關卡三之手部安全偵測】 === - # 1. 判斷手部是否在畫面中 (results.hand_landmarks 是否有資料) - # 2. 如果在畫面中,取得食指 (8)、大拇指 (4)、中指 (12) 的像素座標 - # 3. 如果手不在畫面中,將 is_pinching 設為 False - # - hand_in_frame = False - if results.hand_landmarks: - hand_in_frame = True - for hand_lms in results.hand_landmarks: - # 食指 (8) - lm8 = hand_lms[8] - finger_x = int(lm8.x * WIDTH) - finger_y = int(lm8.y * HEIGHT) - - # 大拇指 (4) - lm4 = hand_lms[4] - thumb_x = int(lm4.x * WIDTH) - thumb_y = int(lm4.y * HEIGHT) - - # 中指 (12) - lm12 = hand_lms[12] - middle_x = int(lm12.x * WIDTH) - middle_y = int(lm12.y * HEIGHT) - - # === 【TODO 3:挑戰關卡一之三指捏合判定】 === - # 提示:計算食指與大拇指距離,以及中指與大拇指距離 - # 兩組距離皆小於 PINCH_THRESHOLD 時,is_pinching 才為 True - dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y) - dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y) - is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD) - - # --- 狀態機邏輯處理 --- - if game_state == "PLAYING": - # 1. 計算剩餘時間 - current_ticks = pygame.time.get_ticks() - elapsed_seconds = (current_ticks - START_TIME) / 1000 - remaining_time = max(0, GAME_DURATION - elapsed_seconds) - - # 2. 檢查時間是否到了 - if remaining_time <= 0: - game_state = "GAME_OVER" - - # 3. 邊緣觸發點擊判斷 - if is_pinching and not was_pinching: - finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) - if finger_rect.colliderect(button_rect): - click_count += 1 - current_color_index = (current_color_index + 1) % len(button_colors) - - # === 【TODO 4:挑戰關卡二之按鈕隨機移動】 === - # 提示:當點擊發生時,讓按鈕的 x 和 y 座標隨機分配在畫面內 - button_rect.x = random.randint(0, WIDTH - button_rect.width) - button_rect.y = random.randint(0, HEIGHT - button_rect.height) - print(f" 魔法三指點擊!第 {click_count} 次") - - # 更新上一幀狀態 - was_pinching = is_pinching - - # --- 繪製畫面 --- - screen.fill((20, 20, 35)) - font = pygame.font.SysFont(None, 32) - large_font = pygame.font.SysFont(None, 64) - - if game_state == "PLAYING": - # 繪製按鈕 - btn_color = button_colors[current_color_index] - pygame.draw.rect(screen, btn_color, button_rect, border_radius=16) - - btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255)) - screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2, - button_rect.centery - btn_text.get_height() // 2)) - - # 顯示倒數計時 - time_text = font.render(f"Time Left: {remaining_time:.1f}s", True, (255, 220, 100)) - screen.blit(time_text, (WIDTH - time_text.get_width() - 20, 10)) - - # 畫手指游標 - cursor_color = (50, 255, 50) if is_pinching else (255, 50, 50) - pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) # 食指 - pygame.draw.circle(screen, (255, 255, 255), (thumb_x, thumb_y), 12) # 大拇指 - pygame.draw.circle(screen, (100, 150, 255), (middle_x, middle_y), 12) # 中指 - - # === 【TODO 5:挑戰關卡三之畫面提示】 === - # 提示:當手不在畫面中時,在畫面上方顯示警告文字提醒學生將手部放回 - if not hand_in_frame: - warning_text = font.render("OUT OF FRAME - PLEASE PUT HANDS BACK", True, (255, 255, 255)) - screen.blit(warning_text, (WIDTH // 2 - warning_text.get_width() // 2, 100)) - - # 顯示調試數值 - small_font = pygame.font.SysFont(None, 24) - status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150)) - screen.blit(status, (10, 10)) - - dist_text = small_font.render(f"Dist Thumb-Index: {dist_thumb_index:.0f} px", True, (150, 150, 200)) - screen.blit(dist_text, (10, 35)) - - dist_text2 = small_font.render(f"Dist Thumb-Middle: {dist_thumb_middle:.0f} px", True, (150, 150, 200)) - screen.blit(dist_text2, (10, 60)) - - elif game_state == "GAME_OVER": - # 繪製遊戲結束畫面 - over_text = large_font.render("GAME OVER", True, (255, 50, 50)) - score_text = font.render(f"Total Clicks: {click_count} times", True, (255, 255, 255)) - - screen.blit(over_text, (WIDTH // 2 - over_text.get_width() // 2, HEIGHT // 2 - 50)) - screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, HEIGHT // 2 + 20)) - - pygame.display.update() - clock.tick(30) - -cap.release() -pygame.quit() -sys.exit() +""" +Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版) +================================================================== +本週挑戰目標: + 1. 實作「三指捏合 (Three-Finger Pinch)」: + 必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」 + 以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch + 2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊 + 3. 使用邊緣觸發 (Edge Detection) 防止重複觸發 + 4. 新增 30 秒限時機制,時間到顯示最終得分 +""" +import pygame +import cv2 +import mediapipe as mp +import math +import sys +import os +import random +from mediapipe.tasks import python +from mediapipe.tasks.python import vision +# ============================================ +# 1. 初始化 +# ============================================ +pygame.init() +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ") +clock = pygame.time.Clock() + +# 攝影機 +cap = cv2.VideoCapture(0) + +try: + base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hand_landmarker.task')) + options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) + detector = vision.HandLandmarker.create_from_options(options) +except Exception as e: + print(f"Error loading model: {e}") + pygame.quit() + sys.exit() + +# ============================================ +# 2. 遊戲變數 +# ============================================ +# 手指座標 +finger_x, finger_y = 400, 300 # 食指 +thumb_x, thumb_y = 400, 300 # 大拇指 +middle_x, middle_y = 400, 300 # 中指 + +# 距離變數 +dist_thumb_index = 0.0 +dist_thumb_middle = 0.0 + +# Pinch 相關變數 +is_pinching = False +was_pinching = False # 邊緣觸發 +PINCH_THRESHOLD =40 # 距離臨界值 + +# 按鈕設定 +button_rect = pygame.Rect(300, 250, 200, 100) +button_colors = [(247, 151, 236), (3, 252, 252), + (52, 244, 3), (255, 255, 255)] +current_color_index = 0 + +# 點擊計數器 +click_count = 0 + +# --- 限時機制變數 --- +START_TIME = pygame.time.get_ticks() +GAME_DURATION = 30 # 遊戲限制時間(秒) +game_state = "PLAYING" # 狀態機 + +# ============================================ +# 3. 主迴圈 +# ============================================ +running = True +while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # --- 讀取攝影機與手勢偵測 --- + success, img = cap.read() + is_pinching = False # 每幀重設狀態,防止手離屏時卡死 + + if success: + img = cv2.flip(img, 1) # 鏡像翻轉 + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + # === 【TODO 2:挑戰關卡三之手部安全偵測】 === + # 1. 判斷手部是否在畫面中 (results.hand_landmarks 是否有資料) + # 2. 如果在畫面中,取得食指 (8)、大拇指 (4)、中指 (12) 的像素座標 + # 3. 如果手不在畫面中,將 is_pinching 設為 False + # + hand_in_frame = False + if results.hand_landmarks: + hand_in_frame = True + for hand_lms in results.hand_landmarks: + # 食指 (8) + lm8 = hand_lms[8] + finger_x = int(lm8.x * WIDTH) + finger_y = int(lm8.y * HEIGHT) + + # 大拇指 (4) + lm4 = hand_lms[4] + thumb_x = int(lm4.x * WIDTH) + thumb_y = int(lm4.y * HEIGHT) + + # 中指 (12) + lm12 = hand_lms[12] + middle_x = int(lm12.x * WIDTH) + middle_y = int(lm12.y * HEIGHT) + + # === 【TODO 3:挑戰關卡一之三指捏合判定】 === + # 提示:計算食指與大拇指距離,以及中指與大拇指距離 + # 兩組距離皆小於 PINCH_THRESHOLD 時,is_pinching 才為 True + dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y) + dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y) + is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD) + + # --- 狀態機邏輯處理 --- + if game_state == "PLAYING": + # 1. 計算剩餘時間 + current_ticks = pygame.time.get_ticks() + elapsed_seconds = (current_ticks - START_TIME) / 1000 + remaining_time = max(0, GAME_DURATION - elapsed_seconds) + + # 2. 檢查時間是否到了 + if remaining_time <= 0: + game_state = "GAME_OVER" + + # 3. 邊緣觸發點擊判斷 + if is_pinching and not was_pinching: + finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) + if finger_rect.colliderect(button_rect): + click_count += 1 + current_color_index = (current_color_index + 1) % len(button_colors) + + # === 【TODO 4:挑戰關卡二之按鈕隨機移動】 === + # 提示:當點擊發生時,讓按鈕的 x 和 y 座標隨機分配在畫面內 + button_rect.x = random.randint(0, WIDTH - button_rect.width) + button_rect.y = random.randint(0, HEIGHT - button_rect.height) + print(f" 魔法三指點擊!第 {click_count} 次") + + # 更新上一幀狀態 + was_pinching = is_pinching + + # --- 繪製畫面 --- + screen.fill((20, 20, 35)) + font = pygame.font.SysFont(None, 32) + large_font = pygame.font.SysFont(None, 64) + + if game_state == "PLAYING": + # 繪製按鈕 + btn_color = button_colors[current_color_index] + pygame.draw.rect(screen, btn_color, button_rect, border_radius=16) + + btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255)) + screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2, + button_rect.centery - btn_text.get_height() // 2)) + + # 顯示倒數計時 + time_text = font.render(f"Time Left: {remaining_time:.1f}s", True, (255, 220, 100)) + screen.blit(time_text, (WIDTH - time_text.get_width() - 20, 10)) + + # 畫手指游標 + cursor_color = (50, 255, 50) if is_pinching else (255, 50, 50) + pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) # 食指 + pygame.draw.circle(screen, (255, 255, 255), (thumb_x, thumb_y), 12) # 大拇指 + pygame.draw.circle(screen, (100, 150, 255), (middle_x, middle_y), 12) # 中指 + + # === 【TODO 5:挑戰關卡三之畫面提示】 === + # 提示:當手不在畫面中時,在畫面上方顯示警告文字提醒學生將手部放回 + if not hand_in_frame: + warning_text = font.render("OUT OF FRAME - PLEASE PUT HANDS BACK", True, (255, 255, 255)) + screen.blit(warning_text, (WIDTH // 2 - warning_text.get_width() // 2, 100)) + + # 顯示調試數值 + small_font = pygame.font.SysFont(None, 24) + status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150)) + screen.blit(status, (10, 10)) + + dist_text = small_font.render(f"Dist Thumb-Index: {dist_thumb_index:.0f} px", True, (150, 150, 200)) + screen.blit(dist_text, (10, 35)) + + dist_text2 = small_font.render(f"Dist Thumb-Middle: {dist_thumb_middle:.0f} px", True, (150, 150, 200)) + screen.blit(dist_text2, (10, 60)) + + elif game_state == "GAME_OVER": + # 繪製遊戲結束畫面 + over_text = large_font.render("GAME OVER", True, (255, 50, 50)) + score_text = font.render(f"Total Clicks: {click_count} times", True, (255, 255, 255)) + + screen.blit(over_text, (WIDTH // 2 - over_text.get_width() // 2, HEIGHT // 2 - 50)) + screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, HEIGHT // 2 + 20)) + + pygame.display.update() + clock.tick(30) + +cap.release() +pygame.quit() +sys.exit() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..fcb42c4bf1d6f4dbdfca16288a1aa1e349c298ef --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d5_pinch_challenge_solution.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd5_pinch_challenge_solution.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\272\272\350\207\211\350\276\250\350\255\230/Boa.jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\272\272\350\207\211\350\276\250\350\255\230/Boa.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..612dd2ad7ed036cd87ba3aef9576b49fc0b9d83f --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\272\272\350\207\211\350\276\250\350\255\230/Boa.jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13395d211888bc201f3fda8362245cab13512f9096cdfdb06d16c92b63739eec +size 218899 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..72b1075a69ea1ceeabd526fd9dbbcf8a32934405 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d3_2_face_detection2.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd3_2_face_detection2.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220/d1_4_visualization_coffee.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220/d1_4_visualization_coffee.py" index 73f2378e69a878a2c47431ede103843dce020072..3d270f26d6012a153d34ee35449b07bfc9b40e5b 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220/d1_4_visualization_coffee.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220/d1_4_visualization_coffee.py" @@ -1,3 +1,5 @@ +import matplotlib +matplotlib.use('Agg') # Telearn hub headless # d1_4_visualization_coffee.py # Day 1: 資料科學與數據視覺化 - 咖啡廳營運數據進階分析(學員填空) # cd examples/Day1/student && python3 d1_4_visualization_coffee.py @@ -8,6 +10,34 @@ import pandas as pd import matplotlib.pyplot as plt + +def _telearn_plt_show(*_a, **_k): + """Hub-friendly plt.show: save current figure and display via OpenCV.""" + import os + import matplotlib.pyplot as plt + try: + import cv2 + except ImportError: + plt.savefig("_telearn_last_plot.png", dpi=150, bbox_inches="tight") + print("[Telearn] 已儲存 _telearn_last_plot.png(無 cv2 可顯示)") + return + path = os.path.join(os.getcwd(), "_telearn_last_plot.png") + plt.savefig(path, dpi=150, bbox_inches="tight") + img = cv2.imread(path) + if img is not None: + # fit to screen-ish width + h, w = img.shape[:2] + max_w = 960 + if w > max_w: + scale = max_w / float(w) + img = cv2.resize(img, (int(w * scale), int(h * scale))) + cv2.imshow("Chart (press any key)", img) + print("[Telearn] 圖表已顯示,按任意鍵關閉…") + cv2.waitKey(0) + cv2.destroyAllWindows() + else: + print(f"[Telearn] 圖表已儲存: {path}") + def load_csv(filename="coffee_sales_data.csv"): """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" # === 【學員填空練習點 1:CSV 檔案安全讀取】 === @@ -110,4 +140,4 @@ plt.tight_layout() # 自動調整間距,避免文字重疊 output_image = "coffee_sales_analysis.png" # 請在下方完成圖表存檔與顯示: plt.savefig(output_image, dpi=300) -plt.show() \ No newline at end of file +_telearn_plt_show() \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..1f104b92c6250f22879c19e0c0198fe1a72e3ba3 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_4_visualization_coffee.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_4_visualization_coffee.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..4f7550b344cc508f552737ddcfc49fc178221251 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step3_mouth_ratio.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step3_mouth_ratio.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/step3_mouth_ratio.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/step3_mouth_ratio.py" index ea598d0d25283489995aafd31dbf69631e18b9eb..93e1d132b82be3b7bb98600bb2c35e405730b638 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/step3_mouth_ratio.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/step3_mouth_ratio.py" @@ -1,104 +1,104 @@ -import cv2 -import mediapipe as mp -import math -import sys -import os - -# 1. 初始化攝影機 -cap = cv2.VideoCapture(0) -if not cap.isOpened(): - print("Error: Could not open camera.") - sys.exit() - -# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) -model_path = os.path.join('face_landmarker.task') -if not os.path.exists(model_path): - print(f"Error: Model file not found at {model_path}") - sys.exit() - -try: - from mediapipe.tasks import python - from mediapipe.tasks.python import vision - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) - detector = vision.FaceLandmarker.create_from_options(options) - print("Face Landmarker initialized successfully.") -except Exception as e: - print(f"Error: Failed to initialize detector: {e}") - sys.exit() - -# 特徵點索引定義 -LEFT_EYE_OUTER = 33 -RIGHT_EYE_OUTER = 263 -LIP_TOP = 13 -LIP_BOTTOM = 14 - -# 嘴巴張開的比例閾值 (超過代表張開) -THRESHOLD = 0.25 - -print("Press 'q' in the window to exit.") - -# 3. 主迴圈 -while True: - success, img = cap.read() - if not success: - break - - img = cv2.flip(img, 1) - img_h, img_w, _ = img.shape - - # 影像處理 - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - results = detector.detect(mp_image) - - if results.face_landmarks: - face_lms = results.face_landmarks[0] - - # 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中 - pts = {} - for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]: - pt = face_lms[idx] - pts[idx] = (int(pt.x * img_w), int(pt.y * img_h)) - - - # 【TODO 2】計算兩眼外角距離 (做為基準比例尺) - p_lx, p_ly = pts[LEFT_EYE_OUTER] - p_rx, p_ry = pts[RIGHT_EYE_OUTER] - base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly) - - - if base_dist > 0: - # 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」 - p_top_x, p_top_y = pts[LIP_TOP] - p_bot_x, p_bot_y = pts[LIP_BOTTOM] - mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y) - mouth_ratio = mouth_dist / base_dist - - - # 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線 - # 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色) - is_open = mouth_ratio > THRESHOLD - cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線 - cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線 - - - # 畫出特徵點小黃圓圈 - for idx, pos in pts.items(): - cv2.circle(img, pos, 5, (0, 255, 255), -1) - - # 顯示比例與開合狀態文字 - cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) - status_text = "OPEN" if is_open else "CLOSED" - status_color = (0, 255, 0) if is_open else (0, 0, 255) - cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2) - else: - cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) - - cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img) - - if cv2.waitKey(1) & 0xFF == ord('q'): - break - -cap.release() -cv2.destroyAllWindows() +import cv2 +import mediapipe as mp +import math +import sys +import os + +# 1. 初始化攝影機 +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("Error: Could not open camera.") + sys.exit() + +# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) +model_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task') +if not os.path.exists(model_path): + print(f"Error: Model file not found at {model_path}") + sys.exit() + +try: + from mediapipe.tasks import python + from mediapipe.tasks.python import vision + base_options = python.BaseOptions(model_asset_path=model_path) + options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) + detector = vision.FaceLandmarker.create_from_options(options) + print("Face Landmarker initialized successfully.") +except Exception as e: + print(f"Error: Failed to initialize detector: {e}") + sys.exit() + +# 特徵點索引定義 +LEFT_EYE_OUTER = 33 +RIGHT_EYE_OUTER = 263 +LIP_TOP = 13 +LIP_BOTTOM = 14 + +# 嘴巴張開的比例閾值 (超過代表張開) +THRESHOLD = 0.25 + +print("Press 'q' in the window to exit.") + +# 3. 主迴圈 +while True: + success, img = cap.read() + if not success: + break + + img = cv2.flip(img, 1) + img_h, img_w, _ = img.shape + + # 影像處理 + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + if results.face_landmarks: + face_lms = results.face_landmarks[0] + + # 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中 + pts = {} + for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]: + pt = face_lms[idx] + pts[idx] = (int(pt.x * img_w), int(pt.y * img_h)) + + + # 【TODO 2】計算兩眼外角距離 (做為基準比例尺) + p_lx, p_ly = pts[LEFT_EYE_OUTER] + p_rx, p_ry = pts[RIGHT_EYE_OUTER] + base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly) + + + if base_dist > 0: + # 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」 + p_top_x, p_top_y = pts[LIP_TOP] + p_bot_x, p_bot_y = pts[LIP_BOTTOM] + mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y) + mouth_ratio = mouth_dist / base_dist + + + # 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線 + # 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色) + is_open = mouth_ratio > THRESHOLD + cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線 + cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線 + + + # 畫出特徵點小黃圓圈 + for idx, pos in pts.items(): + cv2.circle(img, pos, 5, (0, 255, 255), -1) + + # 顯示比例與開合狀態文字 + cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) + status_text = "OPEN" if is_open else "CLOSED" + status_color = (0, 255, 0) if is_open else (0, 0, 255) + cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2) + else: + cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) + + cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + +cap.release() +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254\345\231\250/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254\345\231\250/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..6e10ef440ce9cdd4de5bb4ee95fde52ce4fb20ce --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254\345\231\250/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step2_facemesh_complete.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step2_facemesh_complete.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/Boa.jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/Boa.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..612dd2ad7ed036cd87ba3aef9576b49fc0b9d83f --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/Boa.jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13395d211888bc201f3fda8362245cab13512f9096cdfdb06d16c92b63739eec +size 218899 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/d1_3_visualization.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/d1_3_visualization.py" index b61de8fef1d17cc8aa320d2cd5997a795f5bdb81..9c1585b6d23ceec5bfebe2b16b59e2a45c8b676c 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/d1_3_visualization.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/d1_3_visualization.py" @@ -1,87 +1,117 @@ -# d1_3_visualization.py -# Day 1: 資料科學與數據視覺化 - Matplotlib 繪圖基礎(學員填空) -# cd examples/Day1/student && python3 d1_3_visualization.py -# 參考答案:../Teacher/d1_3_visualization.py - -import os -import pandas as pd -import matplotlib.pyplot as plt - -def load_csv(filename="taipei_weather_2026.csv"): - """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" - # === 【學員填空練習點 1:CSV 檔案安全讀取】 === - # TODO: 1. 使用 os.path.join 將當前腳本所在的目錄與檔案名稱拼接成絕對路徑 - # 提示: 當前腳本目錄為 os.path.dirname(__file__) - # 2. 使用 os.path.exists 判斷路徑是否存在,若存在則用 pd.read_csv 讀取並回傳 - path = os.path.join(os.path.dirname(__file__), filename) - - if os.path.exists(path): - # 請在此實作載入邏輯: - return pd.read_csv(path) - else: - print("files not found") - - -df = load_csv() - -# 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊 -plt.rcParams['font.sans-serif'] = [ - 'PingFang TC', 'Heiti TC', 'Arial Unicode MS', - 'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei', -] -plt.rcParams['axes.unicode_minus'] = False - -# === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 === -# TODO: 使用 plt.subplots 建立一個 2 列 1 行 (2x1) 的子圖畫布,尺寸大小為寬 10 英吋、高 8 英吋 -# 並指派給 fig 與 (ax1, ax2) -fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) - -# 1. 繪製折線圖 (Line Plot) - 每日溫度 -# === 【學員填空練習點 3:繪製每日溫度折線圖】 === -# TODO: 1. 在 ax1 畫布上以 ax1.plot() 繪製折線圖,X軸傳入 df['Date'],Y軸傳入 df['Temperature'] -# 設定參數:帶有圓點 marker='o',顏色 color='crimson',圖例 label='溫度 (°C)',線寬 linewidth=2 -# 2. 使用 ax1.set_title 設定標題 "台北觀測站每日溫度趨勢圖" -# 3. 設定 ax1.set_xlabel("日期") 與 ax1.set_ylabel("數值") -# 4. 設定網格 ax1.grid(True, linestyle='--', alpha=0.5) -# 5. 呼叫 ax1.legend() 顯示圖例 -# 6. 設定 X軸刻度旋轉 ax1.tick_params(axis='x', rotation=45) -# -# 請在下方完成 ax1 的溫度折線圖繪製: -ax1.plot(df['Date'], df['Humidity'], marker='s', color='blue'label='濕度',linewidth=2) -ax1.plot(df['Date'], df['Temperature'], marker='o', color='crimson', label='溫度(度)',linewidth=2) -ax1.set_title("台北觀測站每日溫度趨勢圖",fontsize=14, fontweight='bold') -ax1.set_xlabel("日期") -ax1.set_ylabel("數值") -ax1.grid(True, linestyle='--',alpha=0.5) -ax1.legend() -ax1.tick_params(axis='x',rotation=45) - -# 2. 繪製條形圖 (Bar Plot) - 每日降雨量 -# === 【學員填空練習點 4:繪製每日降雨量長條圖】 === -# TODO: 1. 在 ax2 畫布上以 ax2.bar() 繪製長條圖,X軸傳入 df['Date'],Y軸傳入 df['Rainfall'] -# 設定參數:顏色 color='skyblue',外框顏色 edgecolor='royalblue',圖例 label='降雨量 (mm)' -# 2. 使用 ax2.set_title 設定標題 "台北觀測站每日降雨量分佈" -# 3. 設定 ax2.set_xlabel("日期") 與 ax2.set_ylabel("降雨量 (mm)") -# 4. 設定 Y 軸網格 ax2.grid(True, axis='y', linestyle='--', alpha=0.5) -# 5. 呼叫 ax2.legend() 顯示圖例 -# 6. 設定 X軸刻度旋轉 ax2.tick_params(axis='x', rotation=45) -# -# 請在下方完成 ax2 的降雨量長條圖繪製: -ax2.bar(df['Date'], df['Rainfall'], color='turquoise', edgecolor='skyblue', label='降雨量 (mm)') -ax2.set_title("台北觀測站每日降雨量分佈",fontsize=14, fontweight='bold') -ax2.set_xlabel("日期") -ax2.set_ylabel("降雨量") -ax2.grid(True, axis='y', linestyle='--', alpha=0.5) -ax2.legend() -ax2.tick_params(axis='x',rotation=45) - -plt.tight_layout() # 自動調整間距,避免文字重疊 - -# === 【學員填空練習點 5:圖表自動儲存與顯示】 === -# TODO: 1. 呼叫 plt.savefig 將畫布儲存為檔名 "taipei_weather_2026.png",並設定高畫質 dpi=300 -# (⚠️ 注意:savefig 必須在 show() 之前呼叫) -# 2. 呼叫 plt.show() 將圖表視窗顯示在畫面上 -# -# 請在下方完成圖表存檔與顯示: -plt.savefig("taipei_weather_2026.png", dpi=300) -plt.show() \ No newline at end of file +import matplotlib +matplotlib.use('Agg') # Telearn hub headless +# d1_3_visualization.py +# Day 1: 資料科學與數據視覺化 - Matplotlib 繪圖基礎(學員填空) +# cd examples/Day1/student && python3 d1_3_visualization.py +# 參考答案:../Teacher/d1_3_visualization.py + +import os +import pandas as pd +import matplotlib.pyplot as plt + + +def _telearn_plt_show(*_a, **_k): + """Hub-friendly plt.show: save current figure and display via OpenCV.""" + import os + import matplotlib.pyplot as plt + try: + import cv2 + except ImportError: + plt.savefig("_telearn_last_plot.png", dpi=150, bbox_inches="tight") + print("[Telearn] 已儲存 _telearn_last_plot.png(無 cv2 可顯示)") + return + path = os.path.join(os.getcwd(), "_telearn_last_plot.png") + plt.savefig(path, dpi=150, bbox_inches="tight") + img = cv2.imread(path) + if img is not None: + # fit to screen-ish width + h, w = img.shape[:2] + max_w = 960 + if w > max_w: + scale = max_w / float(w) + img = cv2.resize(img, (int(w * scale), int(h * scale))) + cv2.imshow("Chart (press any key)", img) + print("[Telearn] 圖表已顯示,按任意鍵關閉…") + cv2.waitKey(0) + cv2.destroyAllWindows() + else: + print(f"[Telearn] 圖表已儲存: {path}") + +def load_csv(filename="taipei_weather_2026.csv"): + """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" + # === 【學員填空練習點 1:CSV 檔案安全讀取】 === + # TODO: 1. 使用 os.path.join 將當前腳本所在的目錄與檔案名稱拼接成絕對路徑 + # 提示: 當前腳本目錄為 os.path.dirname(__file__) + # 2. 使用 os.path.exists 判斷路徑是否存在,若存在則用 pd.read_csv 讀取並回傳 + path = os.path.join(os.path.dirname(__file__), filename) + + if os.path.exists(path): + # 請在此實作載入邏輯: + return pd.read_csv(path) + else: + print("files not found") + + +df = load_csv() + +# 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊 +plt.rcParams['font.sans-serif'] = [ + 'PingFang TC', 'Heiti TC', 'Arial Unicode MS', + 'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei', +] +plt.rcParams['axes.unicode_minus'] = False + +# === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 === +# TODO: 使用 plt.subplots 建立一個 2 列 1 行 (2x1) 的子圖畫布,尺寸大小為寬 10 英吋、高 8 英吋 +# 並指派給 fig 與 (ax1, ax2) +fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8)) + +# 1. 繪製折線圖 (Line Plot) - 每日溫度 +# === 【學員填空練習點 3:繪製每日溫度折線圖】 === +# TODO: 1. 在 ax1 畫布上以 ax1.plot() 繪製折線圖,X軸傳入 df['Date'],Y軸傳入 df['Temperature'] +# 設定參數:帶有圓點 marker='o',顏色 color='crimson',圖例 label='溫度 (°C)',線寬 linewidth=2 +# 2. 使用 ax1.set_title 設定標題 "台北觀測站每日溫度趨勢圖" +# 3. 設定 ax1.set_xlabel("日期") 與 ax1.set_ylabel("數值") +# 4. 設定網格 ax1.grid(True, linestyle='--', alpha=0.5) +# 5. 呼叫 ax1.legend() 顯示圖例 +# 6. 設定 X軸刻度旋轉 ax1.tick_params(axis='x', rotation=45) +# +# 請在下方完成 ax1 的溫度折線圖繪製: +ax1.plot(df['Date'], df['Humidity'], marker='s', color='blue', label='濕度', linewidth=2) +ax1.plot(df['Date'], df['Temperature'], marker='o', color='crimson', label='溫度(度)',linewidth=2) +ax1.set_title("台北觀測站每日溫度趨勢圖",fontsize=14, fontweight='bold') +ax1.set_xlabel("日期") +ax1.set_ylabel("數值") +ax1.grid(True, linestyle='--',alpha=0.5) +ax1.legend() +ax1.tick_params(axis='x',rotation=45) + +# 2. 繪製條形圖 (Bar Plot) - 每日降雨量 +# === 【學員填空練習點 4:繪製每日降雨量長條圖】 === +# TODO: 1. 在 ax2 畫布上以 ax2.bar() 繪製長條圖,X軸傳入 df['Date'],Y軸傳入 df['Rainfall'] +# 設定參數:顏色 color='skyblue',外框顏色 edgecolor='royalblue',圖例 label='降雨量 (mm)' +# 2. 使用 ax2.set_title 設定標題 "台北觀測站每日降雨量分佈" +# 3. 設定 ax2.set_xlabel("日期") 與 ax2.set_ylabel("降雨量 (mm)") +# 4. 設定 Y 軸網格 ax2.grid(True, axis='y', linestyle='--', alpha=0.5) +# 5. 呼叫 ax2.legend() 顯示圖例 +# 6. 設定 X軸刻度旋轉 ax2.tick_params(axis='x', rotation=45) +# +# 請在下方完成 ax2 的降雨量長條圖繪製: +ax2.bar(df['Date'], df['Rainfall'], color='turquoise', edgecolor='skyblue', label='降雨量 (mm)') +ax2.set_title("台北觀測站每日降雨量分佈",fontsize=14, fontweight='bold') +ax2.set_xlabel("日期") +ax2.set_ylabel("降雨量") +ax2.grid(True, axis='y', linestyle='--', alpha=0.5) +ax2.legend() +ax2.tick_params(axis='x',rotation=45) + +plt.tight_layout() # 自動調整間距,避免文字重疊 + +# === 【學員填空練習點 5:圖表自動儲存與顯示】 === +# TODO: 1. 呼叫 plt.savefig 將畫布儲存為檔名 "taipei_weather_2026.png",並設定高畫質 dpi=300 +# (⚠️ 注意:savefig 必須在 show() 之前呼叫) +# 2. 呼叫 _telearn_plt_show() 將圖表視窗顯示在畫面上 +# +# 請在下方完成圖表存檔與顯示: +plt.savefig("taipei_weather_2026.png", dpi=300) +_telearn_plt_show() \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a4fd3671a6f3d6b1d49df8a77568c765d630e26d --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\244\251\346\260\243\350\263\207\346\226\231\345\234\226\350\241\250/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_3_visualization.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_3_visualization.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\275\261\345\203\217\350\231\225\347\220\206/Boa.jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\275\261\345\203\217\350\231\225\347\220\206/Boa.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..612dd2ad7ed036cd87ba3aef9576b49fc0b9d83f --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\275\261\345\203\217\350\231\225\347\220\206/Boa.jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13395d211888bc201f3fda8362245cab13512f9096cdfdb06d16c92b63739eec +size 218899 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" index cf5df015aac87494579b472b9aac674ac501099f..8014601292aeccc307a131eccf9fab41e7ab7f65 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" @@ -1,69 +1,109 @@ -# d3_1_opencv_basic.py -# Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版) -# 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。 - -import cv2 -import numpy as np - -# 1. 影像讀取 (Read) -img = cv2.imread("Rebecca.webp") -if img is None: - print("Rebecca webp") - exit(1) - -# 預設灰階影像為原始彩色影像,避免未解開註釋時程式出錯 -gray = img - -# === 【重要觀念 TODO 1:灰階轉換 (Grayscale Conversion)】 === -# 觀念重點:大多數電腦視覺演算法在處理單通道(黑白)影像時速度更快。 -# 將 BGR 彩色圖像轉換為單通道灰階圖像,可以減少 2/3 的資料量,且能過濾掉無用的顏色雜訊。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察變為灰階後的影像。 -gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - -# 預設模糊影像為灰階影像,避免未解開註釋時程式出錯 -blurred = gray - -# === 【重要觀念 TODO 2:高斯模糊 (Gaussian Blur)】 === -# 觀念重點:高斯模糊是透過像素加權平均來平滑影像,主要目的是「消除噪點和細微反光」。 -# 這可以避免後續邊緣偵測時把細微的噪點誤判為輪廓,讓偵測出來的邊緣更加平滑。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察影像邊緣的柔焦效果。 -# blurred = cv2.GaussianBlur(gray, (5, 5), 0) - -# 預設邊緣影像為全黑畫布,避免未解開註釋時程式出錯 -edges = np.zeros(img.shape[:2], dtype=np.uint8) - -# === 【重要觀念 TODO 3:Canny 邊緣偵測 (Canny Edge Detection)】 === -# 觀念重點:Canny 演算法會找出影像中明暗變化劇烈的分界線。它使用雙閥值法: -# 高於高閥值(150)的 100% 判定為邊緣;介於高低閥值(50~150)之間的若連接強邊緣則保留;其餘捨棄。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察提取出的白色線條輪廓。 -edges = cv2.Canny(blurred, 120, 150) - -# 預設無偵測到輪廓,且繪製圖像為原始圖像,避免未解開註釋時程式出錯 -contours = [] -contour_img = img.copy() - -# === 【重要觀念 TODO 4:尋找與繪製輪廓 (Find & Draw Contours)】 === -# 觀念重點:`findContours` 會在二值化(邊緣)影像中尋找相連的邊界,並建立幾何結構。 -# `drawContours` 則會將這些幾何結構繪製到指定的影像上(此處使用綠色框繪製於原始彩色影像)。 -# 請解開下方所有註釋 (刪除行首的 #) 並執行,觀察圖形外框被綠線框起來的結果。 -contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) -print(f"在影像中偵測到 {len(contours)} 個獨立輪廓特徵。") -contour_img = img.copy() -cv2.drawContours(contour_img, contours, -1, (249, 204, 202),1) - -# === 【重要觀念 TODO 5:視窗顯示與釋放 (Window Display)】 === -# 觀念重點:使用 cv2.imshow 顯示各步驟處理結果,並用 waitKey(0) 讓程式停下等待按鍵, -# 最後使用 destroyAllWindows 釋放視窗資源。 -# 請解開下方所有註釋 (刪除行首的 #),以觀看影像處理的成果! -# print("\n正在顯示影像處理結果(請點擊影像視窗並按下任意鍵關閉)...") -# cv2.imshow("Original Shapes", img) -cv2.imshow("Grayscale", gray) -cv2.imshow("Canny Edges", edges) -cv2.imshow("Contours Output", contour_img) - -success_gray = cv2.imwrite("Rebecca_gray.webp", gray) -success_edges = cv2.imwrite("Rebecca_edges.webp", edges) -success_contour = cv2.imwrite("Rebecca_contours.webp", contour_img) - -cv2.waitKey(0) -cv2.destroyAllWindows() +# d3_1_opencv_basic.py +# Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版) +# 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。 + +import cv2 +import numpy as np + +# 1. 影像讀取 (Read) + +def telearn_resolve_image(name): + """Resolve image path relative to script, cwd, or student image/ folders.""" + import os + from pathlib import Path + here = Path(__file__).resolve().parent + candidates = [ + Path(name), + here / name, + here / "image" / name, + here / "照片" / name, + here.parent / "image" / name, + here.parent / "照片" / name, + here.parent / "image_create" / name, + ] + # also try basename-only search one level + for base in [here, here.parent]: + for folder in ["image", "照片", "image_create", "处理过的照片"]: + p = base / folder + if p.is_dir(): + direct = p / name + candidates.append(direct) + # case-insensitive match + low = name.lower() + try: + for f in p.iterdir(): + if f.is_file() and f.name.lower() == low: + candidates.append(f) + except OSError: + pass + for c in candidates: + try: + if c.is_file(): + print(f"[Telearn] 讀取影像: {c}") + return str(c) + except OSError: + pass + print(f"[Telearn] 找不到影像 {name},搜尋過: {[str(c) for c in candidates[:8]]}…") + return name + +img = cv2.imread(telearn_resolve_image("Rebecca.webp")) +if img is None: + print("Rebecca webp") + exit(1) + +# 預設灰階影像為原始彩色影像,避免未解開註釋時程式出錯 +gray = img + +# === 【重要觀念 TODO 1:灰階轉換 (Grayscale Conversion)】 === +# 觀念重點:大多數電腦視覺演算法在處理單通道(黑白)影像時速度更快。 +# 將 BGR 彩色圖像轉換為單通道灰階圖像,可以減少 2/3 的資料量,且能過濾掉無用的顏色雜訊。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察變為灰階後的影像。 +gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + +# 預設模糊影像為灰階影像,避免未解開註釋時程式出錯 +blurred = gray + +# === 【重要觀念 TODO 2:高斯模糊 (Gaussian Blur)】 === +# 觀念重點:高斯模糊是透過像素加權平均來平滑影像,主要目的是「消除噪點和細微反光」。 +# 這可以避免後續邊緣偵測時把細微的噪點誤判為輪廓,讓偵測出來的邊緣更加平滑。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察影像邊緣的柔焦效果。 +# blurred = cv2.GaussianBlur(gray, (5, 5), 0) + +# 預設邊緣影像為全黑畫布,避免未解開註釋時程式出錯 +edges = np.zeros(img.shape[:2], dtype=np.uint8) + +# === 【重要觀念 TODO 3:Canny 邊緣偵測 (Canny Edge Detection)】 === +# 觀念重點:Canny 演算法會找出影像中明暗變化劇烈的分界線。它使用雙閥值法: +# 高於高閥值(150)的 100% 判定為邊緣;介於高低閥值(50~150)之間的若連接強邊緣則保留;其餘捨棄。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察提取出的白色線條輪廓。 +edges = cv2.Canny(blurred, 120, 150) + +# 預設無偵測到輪廓,且繪製圖像為原始圖像,避免未解開註釋時程式出錯 +contours = [] +contour_img = img.copy() + +# === 【重要觀念 TODO 4:尋找與繪製輪廓 (Find & Draw Contours)】 === +# 觀念重點:`findContours` 會在二值化(邊緣)影像中尋找相連的邊界,並建立幾何結構。 +# `drawContours` 則會將這些幾何結構繪製到指定的影像上(此處使用綠色框繪製於原始彩色影像)。 +# 請解開下方所有註釋 (刪除行首的 #) 並執行,觀察圖形外框被綠線框起來的結果。 +contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) +print(f"在影像中偵測到 {len(contours)} 個獨立輪廓特徵。") +contour_img = img.copy() +cv2.drawContours(contour_img, contours, -1, (249, 204, 202),1) + +# === 【重要觀念 TODO 5:視窗顯示與釋放 (Window Display)】 === +# 觀念重點:使用 cv2.imshow 顯示各步驟處理結果,並用 waitKey(0) 讓程式停下等待按鍵, +# 最後使用 destroyAllWindows 釋放視窗資源。 +# 請解開下方所有註釋 (刪除行首的 #),以觀看影像處理的成果! +# print("\n正在顯示影像處理結果(請點擊影像視窗並按下任意鍵關閉)...") +# cv2.imshow("Original Shapes", img) +cv2.imshow("Grayscale", gray) +cv2.imshow("Canny Edges", edges) +cv2.imshow("Contours Output", contour_img) + +success_gray = cv2.imwrite("Rebecca_gray.webp", gray) +success_edges = cv2.imwrite("Rebecca_edges.webp", edges) +success_contour = cv2.imwrite("Rebecca_contours.webp", contour_img) + +cv2.waitKey(0) +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..b223ab342e5fe7b1cc69da34d1bfed35520f5217 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d3_1_opencv_basic.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd3_1_opencv_basic.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/Boa.jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/Boa.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..612dd2ad7ed036cd87ba3aef9576b49fc0b9d83f --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/Boa.jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13395d211888bc201f3fda8362245cab13512f9096cdfdb06d16c92b63739eec +size 218899 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/draw_game/draw_game_student.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" similarity index 97% rename from "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/draw_game/draw_game_student.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" index b777bbc46068e41183e1c0be1f6f7e89bbefb488..224eaa0aea1dd6159f8d4baae390fb8c4f8fa1b7 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/draw_game/draw_game_student.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" @@ -1,3 +1,4 @@ +import os import cv2 import numpy as np import mediapipe as mp @@ -14,7 +15,7 @@ from mediapipe.tasks.python import vision # 2. HandLandmarkerOptions 用於設定手部偵測的細項設定,例如 num_hands=1 代表最多偵測一隻手。 # 3. 最後透過 vision.HandLandmarker.create_from_options 建立手部偵測器物件。 # 請解開下方三行程式碼的註釋: -base_options = python.BaseOptions(model_asset_path='hand_landmarker.task') +base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hand_landmarker.task')) options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) detector = vision.HandLandmarker.create_from_options(options) diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..b4b6ab0db6832a42e6763bb130c2aa53c4003caa --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: draw_game_student.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'draw_game_student.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..e271a2a8c0d74bb4e05e2cc91e7720a53098f5cf --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex2_ptt.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex2_ptt.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..afd501d3f608ba4c046ff4983d27e9763690b3b7 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex1_books.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex1_books.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..2ee124121c9acf2163ab18a3f956886b02cea663 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex3_ithelp.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex3_ithelp.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..e51446f49e2d44e7c5116b2f9764bfee686f6e7e --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_web_scraping.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_web_scraping.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220&ai\345\240\261\345\221\212_PTT/d4_2_ex2_crawler_ai.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_PTT/d4_2_ex2_crawler_ai.py" similarity index 88% rename from "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220&ai\345\240\261\345\221\212_PTT/d4_2_ex2_crawler_ai.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_PTT/d4_2_ex2_crawler_ai.py" index aee5762e4402839d4cc31e91c7535f0eb7ed1fed..d7e69de57eac3671b5331ee4c47a314d6677e23a 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220&ai\345\240\261\345\221\212_PTT/d4_2_ex2_crawler_ai.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_PTT/d4_2_ex2_crawler_ai.py" @@ -9,8 +9,39 @@ from urllib.parse import urljoin from google import genai # 設定預設使用的 Gemini 模型 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") +api_key = _telearn_require_gemini_api_key(allow_skip=True) HEADERS = { "User-Agent": ( @@ -175,7 +206,7 @@ def run_analysis_chain(source_type): # 步驟 2: AI 分析 (或 Mock 降級防禦) if not api_key: - print("\n[步驟 2] 未偵測到環境變數 GEMINI_API_KEY,自動啟動 Mock 離線降級機制...") + print("\n[步驟 2] 未輸入 GEMINI_API_KEY,啟動 Mock 離線降級機制...") # === 【學員填空練習點 5:Mock 降級防衛機制】 === # TODO 5: 請設計一段模擬的 Markdown 格式分析報告字串,指派給 report_content。 # 報告內容至少需要包含標題與數據摘要,避免程式在無 Key 時報錯中斷。 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_PTT/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_PTT/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a5ae937359f5ab6c3d6b6730ee4cf5c043ea685a --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_PTT/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_2_ex2_crawler_ai.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_2_ex2_crawler_ai.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220&ai\345\240\261\345\221\212_\345\220\215\350\250\200\345\210\206\346\236\220/d4_2_agent_task_chain.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_\345\220\215\350\250\200\345\210\206\346\236\220/d4_2_agent_task_chain.py" similarity index 80% rename from "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220&ai\345\240\261\345\221\212_\345\220\215\350\250\200\345\210\206\346\236\220/d4_2_agent_task_chain.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_\345\220\215\350\250\200\345\210\206\346\236\220/d4_2_agent_task_chain.py" index b9dadab11c7ec58bf81b57c6635b66b2e5b85a2f..01d09bb367e414f5c424572c7bb5eb3216e04fcc 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220&ai\345\240\261\345\221\212_\345\220\215\350\250\200\345\210\206\346\236\220/d4_2_agent_task_chain.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_\345\220\215\350\250\200\345\210\206\346\236\220/d4_2_agent_task_chain.py" @@ -7,8 +7,39 @@ import os import requests from bs4 import BeautifulSoup + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") +api_key = _telearn_require_gemini_api_key(allow_skip=True) HEADERS = { "User-Agent": ( @@ -96,7 +127,7 @@ def run_task_chain(topic): f"- 模擬數據:\"Love is all, love is new.\" -- John Lennon\n\n" f"## 二、 核心思想分析\n" f"本機模擬分析指出,此主題的語錄主要著眼於人與人之間的連結與心靈啟發。\n" - f"*(注意:此為 Mock 離線生成報告,設定 GEMINI_API_KEY 以啟用 Gemini 真實生成。)*\n" + f"*(注意:此為 Mock 離線生成報告,於啟動時輸入 GEMINI_API_KEY 以啟用 Gemini 真實生成。)*\n" ) else: print(f"\n[步驟 2] 正在調用 Gemini ({GEMINI_MODEL}) 進行深度分析與排版...") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_\345\220\215\350\250\200\345\210\206\346\236\220/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_\345\220\215\350\250\200\345\210\206\346\236\220/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..514f341e8756e8a0b466b18a2766647a7419751d --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207ai\345\240\261\345\221\212_\345\220\215\350\250\200\345\210\206\346\236\220/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_2_agent_task_chain.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_2_agent_task_chain.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..6d9f8e43cbdcde83518590b0a762af898547ad23 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step0_eyeguard_template.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step0_eyeguard_template.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" index e773ab60196d7f89560f04444a46f214ec945a10..cd2517b0c9e330ea0a7ff022eb82132a4b388f18 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\234\274\347\235\233\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" @@ -45,7 +45,7 @@ cap = cv2.VideoCapture(0) # 沒有攝影機就改成影片:cap = cv2.VideoCapture("backup_face_video.mp4") # 【TODO 1】初始化 MediaPipe 臉部偵測 (Face Landmarker) -base_options = python.BaseOptions(model_asset_path=os.path.join('face_landmarker.task')) +base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task')) options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) detector = vision.FaceLandmarker.create_from_options(options) diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\271\252\350\243\275\350\207\211\351\203\2503D\346\250\241\345\236\213/Boa.jpeg" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\271\252\350\243\275\350\207\211\351\203\2503D\346\250\241\345\236\213/Boa.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..612dd2ad7ed036cd87ba3aef9576b49fc0b9d83f --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\271\252\350\243\275\350\207\211\351\203\2503D\346\250\241\345\236\213/Boa.jpeg" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13395d211888bc201f3fda8362245cab13512f9096cdfdb06d16c92b63739eec +size 218899 diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\271\252\350\243\275\350\207\211\351\203\2503D\346\250\241\345\236\213/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\271\252\350\243\275\350\207\211\351\203\2503D\346\250\241\345\236\213/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..1c74dbd4c33dd2ab30e9666859e7d417aa7bfb6c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\271\252\350\243\275\350\207\211\351\203\2503D\346\250\241\345\236\213/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step1_facemesh.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step1_facemesh.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\271\252\350\243\275\350\207\211\351\203\2503D\346\250\241\345\236\213/step1_facemesh.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\271\252\350\243\275\350\207\211\351\203\2503D\346\250\241\345\236\213/step1_facemesh.py" index 3726abe12ccf2361cc010201ec660049850b2803..a2130050f63194b4990b7f5bc131727f6d3dcc8b 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\271\252\350\243\275\350\207\211\351\203\2503D\346\250\241\345\236\213/step1_facemesh.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\347\271\252\350\243\275\350\207\211\351\203\2503D\346\250\241\345\236\213/step1_facemesh.py" @@ -1,115 +1,115 @@ -""" -Week 11 - Step 1: Face Mesh Tracking with Connections (Template 填空版) -=================================================================== -本步驟目標: - 1. 使用 OpenCV 開啟視訊鏡頭 - 2. 載入 MediaPipe 臉部特徵偵測器 (face_landmarker.task) - 3. 偵測人臉,並在臉上畫出綠色的 3D 特效網格線與黃色特徵點 -""" -import cv2 -import mediapipe as mp -import numpy as np -import sys -import os - -# 1. 初始化攝影機 -cap = cv2.VideoCapture(0) -if not cap.isOpened(): - print("Error: Could not open camera.") - sys.exit() - -# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) -model_path = os.path.join('face_landmarker.task') -if not os.path.exists(model_path): - print(f"Error: Model file not found at {model_path}") - print("Please make sure face_landmarker.task is in the examples folder.") - sys.exit() - -try: - from mediapipe.tasks import python - from mediapipe.tasks.python import vision - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) - detector = vision.FaceLandmarker.create_from_options(options) - print("Face Landmarker Tasks API initialized successfully.") -except Exception as e: - print(f"Error initializing detector: {e}") - sys.exit() - -# 匯入臉部網格連接點定義 -try: - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - from face_mesh_connections import FACEMESH_TESSELATION -except ImportError: - FACEMESH_TESSELATION = [] - -# 繪圖模式:0 = 僅畫網格線, 1 = 僅畫點點, 2 = 點線都畫 -draw_mode = 0 - -print("Press 'q' in the window to exit, or 'm' to toggle display modes.") - -# 3. 主迴圈 -while True: - success, img = cap.read() - if not success: - print("Error: Failed to read frame.") - break - - # 【TODO 1】畫面水平翻轉,並將 BGR 轉成 MediaPipe 需要的 RGB 格式 - img = cv2.flip(img, 1) - img_h, img_w, _ = img.shape - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - - # 暫時為了讓未填空時不當機的預設程式碼: - img = cv2.flip(img, 1) - img_h, img_w, _ = img.shape - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - - # 執行臉部偵測 - results = detector.detect(mp_image) - - # 【TODO 2】如果偵測到臉部,畫出 3D 特效網格線 - # 提示:取出 results.face_landmarks[0],並用迴圈巡覽 FACEMESH_TESSELATION - # 點的座標 (lm.x, lm.y) 是 0~1 的比例,需要乘上 img_w 和 img_h 換算成像素座標 - - # 預設執行代碼 (填寫完後學生會將此替換或解除上方註解) - if results.face_landmarks: - face_lms = results.face_landmarks[0] - num_lms = len(face_lms) - - if draw_mode == 0 or draw_mode == 2: - for start_idx, end_idx in FACEMESH_TESSELATION: - if start_idx < num_lms and end_idx < num_lms: - lm1 = face_lms[start_idx] - lm2 = face_lms[end_idx] - pt1 = (int(lm1.x * img_w), int(lm1.y * img_h)) - pt2 = (int(lm2.x * img_w), int(lm2.y * img_h)) - cv2.line(img, pt1, pt2, (224, 224, 0), 1) - - # 【TODO 3】畫出臉部 468 個黃色特徵點點 - if draw_mode == 1 or draw_mode == 2: - for lm in face_lms: - cx = int(lm.x * img_w) - cy = int(lm.y * img_h) - cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) # 黃色小圓點 - - mode_str = "Mesh" if draw_mode == 0 else "Dots" if draw_mode == 1 else "Both" - cv2.putText(img, f"Face Detected | Mode: {mode_str}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) - else: - cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) - - # 顯示操作提示文字 - cv2.putText(img, "Press 'm' to toggle mode | 'q' to quit", (10, img_h - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1) - - cv2.imshow("Face Mesh Connections", img) - - key = cv2.waitKey(1) & 0xFF - if key == ord('q'): - break - elif key == ord('m'): - draw_mode = (draw_mode + 1) % 3 - -cap.release() -cv2.destroyAllWindows() +""" +Week 11 - Step 1: Face Mesh Tracking with Connections (Template 填空版) +=================================================================== +本步驟目標: + 1. 使用 OpenCV 開啟視訊鏡頭 + 2. 載入 MediaPipe 臉部特徵偵測器 (face_landmarker.task) + 3. 偵測人臉,並在臉上畫出綠色的 3D 特效網格線與黃色特徵點 +""" +import cv2 +import mediapipe as mp +import numpy as np +import sys +import os + +# 1. 初始化攝影機 +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("Error: Could not open camera.") + sys.exit() + +# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) +model_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task') +if not os.path.exists(model_path): + print(f"Error: Model file not found at {model_path}") + print("Please make sure face_landmarker.task is in the examples folder.") + sys.exit() + +try: + from mediapipe.tasks import python + from mediapipe.tasks.python import vision + base_options = python.BaseOptions(model_asset_path=model_path) + options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) + detector = vision.FaceLandmarker.create_from_options(options) + print("Face Landmarker Tasks API initialized successfully.") +except Exception as e: + print(f"Error initializing detector: {e}") + sys.exit() + +# 匯入臉部網格連接點定義 +try: + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + from face_mesh_connections import FACEMESH_TESSELATION +except ImportError: + FACEMESH_TESSELATION = [] + +# 繪圖模式:0 = 僅畫網格線, 1 = 僅畫點點, 2 = 點線都畫 +draw_mode = 0 + +print("Press 'q' in the window to exit, or 'm' to toggle display modes.") + +# 3. 主迴圈 +while True: + success, img = cap.read() + if not success: + print("Error: Failed to read frame.") + break + + # 【TODO 1】畫面水平翻轉,並將 BGR 轉成 MediaPipe 需要的 RGB 格式 + img = cv2.flip(img, 1) + img_h, img_w, _ = img.shape + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + + # 暫時為了讓未填空時不當機的預設程式碼: + img = cv2.flip(img, 1) + img_h, img_w, _ = img.shape + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + + # 執行臉部偵測 + results = detector.detect(mp_image) + + # 【TODO 2】如果偵測到臉部,畫出 3D 特效網格線 + # 提示:取出 results.face_landmarks[0],並用迴圈巡覽 FACEMESH_TESSELATION + # 點的座標 (lm.x, lm.y) 是 0~1 的比例,需要乘上 img_w 和 img_h 換算成像素座標 + + # 預設執行代碼 (填寫完後學生會將此替換或解除上方註解) + if results.face_landmarks: + face_lms = results.face_landmarks[0] + num_lms = len(face_lms) + + if draw_mode == 0 or draw_mode == 2: + for start_idx, end_idx in FACEMESH_TESSELATION: + if start_idx < num_lms and end_idx < num_lms: + lm1 = face_lms[start_idx] + lm2 = face_lms[end_idx] + pt1 = (int(lm1.x * img_w), int(lm1.y * img_h)) + pt2 = (int(lm2.x * img_w), int(lm2.y * img_h)) + cv2.line(img, pt1, pt2, (224, 224, 0), 1) + + # 【TODO 3】畫出臉部 468 個黃色特徵點點 + if draw_mode == 1 or draw_mode == 2: + for lm in face_lms: + cx = int(lm.x * img_w) + cy = int(lm.y * img_h) + cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) # 黃色小圓點 + + mode_str = "Mesh" if draw_mode == 0 else "Dots" if draw_mode == 1 else "Both" + cv2.putText(img, f"Face Detected | Mode: {mode_str}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + else: + cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) + + # 顯示操作提示文字 + cv2.putText(img, "Press 'm' to toggle mode | 'q' to quit", (10, img_h - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1) + + cv2.imshow("Face Mesh Connections", img) + + key = cv2.waitKey(1) & 0xFF + if key == ord('q'): + break + elif key == ord('m'): + draw_mode = (draw_mode + 1) % 3 + +cap.release() +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\250\255\350\250\210AI\350\211\262&\345\234\272\346\231\257/d2_2_prompt_engineering.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" similarity index 55% rename from "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\250\255\350\250\210AI\350\211\262&\345\234\272\346\231\257/d2_2_prompt_engineering.py" rename to "projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" index fff4b0721b7e4622a22a0c51a2c018cde4f8c8fd..c1e2f820dd52da927de1db6d1d286a2b6312fc8c 100644 --- "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\250\255\350\250\210AI\350\211\262&\345\234\272\346\231\257/d2_2_prompt_engineering.py" +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" @@ -1,58 +1,86 @@ -# d2_2_prompt_engineering.py -# Day 2: 生成式 AI 串接實戰 - Prompt 工程 (系統指令與結構化輸出) -import os -from google import genai - -# 取得 API 金鑰 -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") -if not api_key: - print("[提示] 請輸入您的 Google AI Studio API 金鑰:") - api_key = input("API Key: ").strip() - -client = genai.Client(api_key=api_key) - -# 1. 設置系統指令 (System Instruction / System Prompt) -# TODO 1: -system_prompt = "你是国中的英文老師,記得教學的時候要有耐心!" - -print("正在初始化英語教學助理模型...") - -# 2. Few-shot (少樣本) 引導與對話演練 -# TODO 2: -user_word = "淘宝" - -print(f"\n[學生輸入]: {user_word}") -prompt = f"請幫我翻譯這個詞:'{user_word}'。" - -response = client.models.generate_content( - model='gemini-3.1-flash-lite', - contents=prompt, - config={'system_instruction': system_prompt} -) - -print("\n[AI 老師回應]:") -print(response.text) - - -# 3. 結構化 JSON 輸出 (Structured Output) 範例 -print("\n" + "="*40) -print("進階挑戰:要求模型回傳 JSON 格式") -print("="*40) - -# 我們可以在 prompt 中要求,並透過 configuration 設定 JSON 輸出格式 -# TODO 3: -json_prompt = ( - f"請將中文單字 '{user_word}' 的翻譯結果整理成 JSON 格式,包含以下三個欄位:\n" - "- 'english': 英文單字\n" - "- 'kk_phonetic': KK音標\n" - "- 'example_sentence': 適合國中生理解的英文例句與中文翻譯" - -) - -response = client.models.generate_content( - model='gemini-3.1-flash-lite', - contents=json_prompt, - config={"response_mime_type": "application/json"} -) -print("\n[AI 回傳的原始 JSON]:") -print(response.text) +# d2_2_prompt_engineering.py +# Day 2: 生成式 AI 串接實戰 - Prompt 工程 (系統指令與結構化輸出) +import os +from google import genai + +# 取得 API 金鑰 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +api_key = _telearn_require_gemini_api_key(allow_skip=False) + +client = genai.Client(api_key=api_key) + +# 1. 設置系統指令 (System Instruction / System Prompt) +# TODO 1: +system_prompt = "你是国中的英文老師,記得教學的時候要有耐心!" + +print("正在初始化英語教學助理模型...") + +# 2. Few-shot (少樣本) 引導與對話演練 +# TODO 2: +user_word = "淘宝" + +print(f"\n[學生輸入]: {user_word}") +prompt = f"請幫我翻譯這個詞:'{user_word}'。" + +response = client.models.generate_content( + model='gemini-3.1-flash-lite', + contents=prompt, + config={'system_instruction': system_prompt} +) + +print("\n[AI 老師回應]:") +print(response.text) + + +# 3. 結構化 JSON 輸出 (Structured Output) 範例 +print("\n" + "="*40) +print("進階挑戰:要求模型回傳 JSON 格式") +print("="*40) + +# 我們可以在 prompt 中要求,並透過 configuration 設定 JSON 輸出格式 +# TODO 3: +json_prompt = ( + f"請將中文單字 '{user_word}' 的翻譯結果整理成 JSON 格式,包含以下三個欄位:\n" + "- 'english': 英文單字\n" + "- 'kk_phonetic': KK音標\n" + "- 'example_sentence': 適合國中生理解的英文例句與中文翻譯" + +) + +response = client.models.generate_content( + model='gemini-3.1-flash-lite', + contents=json_prompt, + config={"response_mime_type": "application/json"} +) +print("\n[AI 回傳的原始 JSON]:") +print(response.text) diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..5f819ed5e0570c50eae23f77a85ad0660170f673 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_2_prompt_engineering.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_2_prompt_engineering.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..69cdb69578cc4ce89cad8c1538eb2191bd6e5200 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\345\212\211\346\237\224\344\275\221/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_2_data_cleaning.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_2_data_cleaning.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\225\264\347\220\206\345\240\261\345\221\212.md" "b/projects/2026_AI\347\207\2371/\346\225\264\347\220\206\345\240\261\345\221\212.md" new file mode 100644 index 0000000000000000000000000000000000000000..86c646f58f23101a13d7bd3782e1221e51036e85 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\225\264\347\220\206\345\240\261\345\221\212.md" @@ -0,0 +1,166 @@ +# 2026_AI營1 資料夾整理報告 + +整理時間:自動腳本 `scripts/fix_2026_ai_camp.py` + 人工修正語法錯誤 +整理後 Hub 可見:**4 位學生、共 90 個可執行專案**(皆有 `main.py`) + +--- + +## 一、平台規格(整理目標) + +展示場只認三層目錄,且專案內要有入口檔: + +```text +projects/{班級}/{學生}/{專案名稱}/main.py +``` + +| 類型 | 條件 | +|------|------| +| Python | 有 `main.py` | +| HTML | 有 `index.html` | +| APK | 有 `*.apk` | +| 外部連結 | 有 `link.txt` | + +**整理前**:`2026_AI營1` 底下 **0 個 `main.py`** → 首頁完全看不到這個班級。 + +--- + +## 二、整理前的主要問題 + +1. **入口檔名不符合規格** + 實際是 `d1_1_pandas_intro.py`、`step3_mouth_ratio.py`、`draw_game_student.py` 等,平台無法掃描。 + +2. **多個練習塞在同一資料夾** + 例如「爬蟲分析」內同時有 books / PTT / ithelp / 名言網站,無法在目錄中分開啟動。 + +3. **巢狀過深** + 曾祺鈞 `step/嘴巴/` 多一層,不符合「學生 → 專案」結構。 + +4. **資源目錄與專案混在一起** + `image/`、`image_create/`、`照片/`、`处理过的照片/`、`圖像繪製結果/` 等沒有程式,不應當成專案。 + +5. **程式錯誤 / 無法在 Hub 執行** + - 硬編碼 **Gemini API Key**(安全風險) + - 劉宗穎三指捏遊戲有單獨一行 `\` → **語法錯誤** + - 劉柔佑圖表:`color='blue'label=` 缺逗號 → **語法錯誤** + - 劉柔佑 mock 雙模式:`system_instruction` **縮排錯誤** + - `cv2.imread("xxx.jpg")` 圖在 `image/` 子目錄 → 讀不到 + - `pd.read_csv("xxx.csv")` 工作目錄不對 → 讀不到 + - `plt.show()` 在 Docker/Hub **無頭環境**無效 + - MediaPipe `.task` 路徑混亂、檔案重複佔空間 + +--- + +## 三、做了哪些修改 + +### A. 目錄與入口(結構) + +| 動作 | 說明 | +|------|------| +| 新增 `main.py` | 每個可執行專案 1 個;內容用 `runpy` 轉呼叫原始學員腳本 | +| 拆分爬蟲練習 | 同資料夾多支 `d4_1_ex*` → 獨立專案 `爬蟲分析_books` / `_PTT` / `_ithelp` / `_名言網站`,原資料夾保留**選單版 main** | +| 提升巢狀目錄 | `曾祺鈞/step/嘴巴/` → `曾祺鈞/嘴巴開合偵測/` | +| 略過資源目錄 | `image`、`image_create`、`照片`、`处理过的照片`、`圖像繪製結果`、`Day1` 等 **不產生 main**(不會出現在目錄) | +| 刪除重複 `.task` | 專案內 MediaPipe 模型改由倉庫根目錄 `models/`,執行時由 `runner.py` 自動連結 | + +### B. 程式碼修正(可執行性 / 安全) + +| 修正 | 作法 | +|------|------| +| API Key | 全部改為 `os.environ.get("GEMINI_API_KEY", "")`,需要時再 `input()`;**已移除寫死在檔案裡的金鑰** | +| 語法錯誤 | 刪除 stray `\`;補上 `color='blue', label=...` 逗號;修正 `system_instruction` 縮排 | +| matplotlib | `matplotlib.use('Agg')`;`plt.show()` 改為存成圖後用 `cv2.imshow`(Hub 可看) | +| 影像路徑 | 新增 `telearn_resolve_image()`,會搜尋專案目錄、`image/`、`照片/` 等 | +| CSV 路徑 | `pd.read_csv` 改為相對 `__file__` 所在目錄;缺檔時從同學/其他練習複製 | +| MediaPipe 模型路徑 | 統一為「腳本同目錄的 `hand_landmarker.task` / `face_landmarker.task`」(runner 會自動 symlink) | +| 入口腳本選擇 | 同資料夾多支時優先:`solution` / `mouth_ratio` / `step4_ear` / `detection2` / `complete` 等較完整版本 | + +### C. 入口對應範例 + +| 專案資料夾 | `main.py` 實際執行 | +|------------|-------------------| +| `draw_game` / `手指畫畫遊戲` | `draw_game_student.py` | +| `三指捏遊戲` / `三指` | `d5_pinch_challenge_solution.py` | +| `人臉辨識` | `d3_2_face_detection2.py`(優先完整版) | +| `嘴巴*` / `open_mouth_close` | `step3_mouth_ratio.py`(有則優先) | +| `曾祺鈞/兩眼` | `step4_ear.py` | +| `DataFrame指令練習` | `d1_1_pandas_intro.py` | +| `天氣資料*` / 咖啡店圖表 | `d1_3_visualization.py` / `d1_4_visualization_coffee.py` | +| `爬蟲分析`(原多檔資料夾) | 選單,或拆出的子專案各自一支 | + +原始 `.py` **保留不刪**,方便對照作業原文;Hub 只認 `main.py`。 + +--- + +## 四、整理後數量 + +| 學生 | 可見專案數(約) | +|------|------------------| +| 劉宗穎 | 21 | +| 劉柔佑 | 23 | +| 曾祺鈞 | 24 | +| 曾麒洋 | 22 | +| **合計** | **90** | + +(另有約 13 個純資源/略過目錄不顯示。) + +--- + +## 五、仍須注意(未改或無法全自動) + +1. **需要網路的作業**(Gemini、爬蟲):在 Spaces 上可能受防火牆/金鑰限制;請設 Space Secret `GEMINI_API_KEY`,或本機 `export GEMINI_API_KEY=...`。 +2. **Mock 密室/聊天** 仍依學生邏輯;無 Key 時部分已走 mock,部分會提示輸入。 +3. **OpenCV 一次開多個 `imshow` 視窗**:Hub 畫面串流通常只顯示最後更新的那一個,屬平台限制。 +4. **專案命名仍中英混用**(如 `draw_game`、`coffe_shop_sales_draw`、簡體目錄名):不影響執行,若要統一命名可再手動改資料夾名(記得同步 `main.py` 即可)。 +5. **依賴**:pandas / matplotlib / google-genai 等若未裝在映像中,對應專案會失敗。目前 base `requirements.txt` 未含 pandas/matplotlib/google-genai;若要在 HF 上跑 Day1–2,需再補依賴。 + +--- + +## 六、如何重跑整理腳本 + +```bash +cd /path/to/project_demo +python3 scripts/fix_2026_ai_camp.py +``` + +本報告路徑: + +`projects/2026_AI營1/整理報告.md` + +--- + +## 七、給教師的快速檢查清單 + +- [ ] 開啟首頁 → 應看到班級 **2026_AI營1** +- [ ] 點任一學生 → 應有多個 Python 專案卡片 +- [ ] 終端型(DataFrame / 資料清洗)→ 右側 log 有輸出 +- [ ] 圖表型 → 應出現圖檔視窗(cv2) +- [ ] 手勢/臉部 → 允許 Webcam;首次會連模型 +- [ ] Gemini 專案 → 先設 API Key 再測 + +--- + +*若需同步上 Hugging Face:先跑 `python3 scripts/sync_hf_demo.py`,再於 `hf_demo` push。* + +--- + +## 八、後續更新(GEMINI 輸入 / 繁體命名 / 依賴) + +### 1. GEMINI_API_KEY 終端輸入介面 + +所有需要 Gemini 的腳本已加入 `_telearn_require_gemini_api_key()`: + +- 啟動時在**播放器右側終端**提示貼上 Key +- 取得金鑰說明:https://aistudio.google.com/apikey +- **必填型**(純 Gemini 呼叫):未輸入則結束 +- **可 Mock 型**(雙模式客服 / 爬蟲+AI 報告):可直接 Enter 略過,改走離線 Mock + +環境變數若已設定 `GEMINI_API_KEY` 則跳過輸入。 + +### 2. 專案資料夾命名改繁體 + +劉宗穎等簡體目錄(如 `人脸辨识`、`爬虫分析`、`设计AI角色&场景`)已改為繁體; +部分英文雜名一併正規化(如 `draw_game` → `手指畫畫遊戲`、`open_mouth_close` → `嘴巴開合偵測`)。 + +### 3. 依賴補齊(`requirements.txt`) + +已納入:`pandas`、`matplotlib`、`beautifulsoup4`、`lxml`、`google-genai`、`google-generativeai`。 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" index da7e7a63caa1e0fd3e3305c640e97645bb599b5a..8f56b37f28acf52f973199f424fc33a2f9eecb40 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" @@ -1,34 +1,35 @@ -# d1_1_pandas_intro.py -# Day 1 學員填空:Pandas 基礎(完成 TODO 後執行) -# cd examples/Day1/student && python3 d1_1_pandas_intro.py -# 參考答案:../Teacher/d1_1_pandas_intro.py - -import pandas as pd - -df = pd.read_csv("taipei_weather_2026.csv") -print("=== 數據載入成功! ===") - -print("\n--- 1. head ---") -print(df.head()) - -print("\n--- 2. info ---") -df.info() - -print("\n--- 3. describe ---") -print(df.describe()) - -print("\n--- 4. Temperature 前 5 筆 ---") -print(df['Temperature'].head()) - -print("\n--- 5. Date 與 Rainfall ---") -print(df[["Date","Rainfall" ]].head()) - -print("\n--- 挑戰:最高溫與平均濕度 ---") -max_temp = df['Temperature'].max() -avg_humidity = df['Humidity'].mean() -min_rainfall = df['Rainfall'].min() -print(f"最高溫度:{max_temp}°C") -print(f"平均濕度:{avg_humidity:.2f}%") -print(f"最小降雨量:{min_rainfall}mm") -df['inidian_can_come'] = (df["Temperature"] > 27) & (df["Rainfall"] ==0.0) +import os +# d1_1_pandas_intro.py +# Day 1 學員填空:Pandas 基礎(完成 TODO 後執行) +# cd examples/Day1/student && python3 d1_1_pandas_intro.py +# 參考答案:../Teacher/d1_1_pandas_intro.py + +import pandas as pd + +df = pd.read_csv(os.path.join(os.path.dirname(__file__), "taipei_weather_2026.csv")) +print("=== 數據載入成功! ===") + +print("\n--- 1. head ---") +print(df.head()) + +print("\n--- 2. info ---") +df.info() + +print("\n--- 3. describe ---") +print(df.describe()) + +print("\n--- 4. Temperature 前 5 筆 ---") +print(df['Temperature'].head()) + +print("\n--- 5. Date 與 Rainfall ---") +print(df[["Date","Rainfall" ]].head()) + +print("\n--- 挑戰:最高溫與平均濕度 ---") +max_temp = df['Temperature'].max() +avg_humidity = df['Humidity'].mean() +min_rainfall = df['Rainfall'].min() +print(f"最高溫度:{max_temp}°C") +print(f"平均濕度:{avg_humidity:.2f}%") +print(f"最小降雨量:{min_rainfall}mm") +df['inidian_can_come'] = (df["Temperature"] > 27) & (df["Rainfall"] ==0.0) print(df) \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..19029aca2c352f92bc7a6ace63c7161bf8b10e90 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_1_pandas_intro.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_1_pandas_intro.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/Day4/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/Day4/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..e51446f49e2d44e7c5116b2f9764bfee686f6e7e --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/Day4/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_web_scraping.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_web_scraping.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" index e3598c40ef2d9e557454983fe80df1790c37ff57..bcf1b3b7fe237f3e76592a845ff507441b7fd66f 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" @@ -1,41 +1,63 @@ -# d2_1_gemini.py — Day2 學員填空 -# cd examples/Day2/Student && python3 d2_1_gemini.py -# 參考答案:../Teacher/d2_1_gemini.py - -import os -from google import genai - -# === 【學員填空練習點 1:設置與載入 API Key】 === -# TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。 -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") - -if not api_key: - print("[提示] 未檢測到環境變數 GEMINI_API_KEY。") - print(" 請先設定:export GEMINI_API_KEY=\"your_key\"") - print(" 或於下方手動輸入(僅供本機測試,勿寫死在檔案中上傳):") - api_key = input("API Key: ").strip() - if not api_key: - print("[結束] 未提供 API Key。可改練習 d2_1_ollama_local.py。") - exit(0) - -# === 【學員填空練習點 2:設定模型名稱】 === -# TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite') -model_name = "gemini-3.1-flash-lite" - -print(f"正在連線至 Google Cloud Model ({model_name})...") - -# === 【學員填空練習點 3:初始化 Gemini Client】 === -# TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數 -client = genai.Client(api_key=api_key) - -# === 【學員填空練習點 4:發送內容生成請求】 === -# TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞 -# 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。 -response = client.models.generate_content(model=model_name, -contents= "What's ur favorite game?") - -# === 【學員填空練習點 5:讀取並印出回應】 === -# TODO: 從 response 物件中取得 text 欄位內容,並列印出來。 -print("\n=== AI 回應結果 ===") -# 請在下方實作列印邏輯: +# d2_1_gemini.py — Day2 學員填空 +# cd examples/Day2/Student && python3 d2_1_gemini.py +# 參考答案:../Teacher/d2_1_gemini.py + +import os +from google import genai + +# === 【學員填空練習點 1:設置與載入 API Key】 === +# TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +api_key = _telearn_require_gemini_api_key(allow_skip=False) + +# === 【學員填空練習點 2:設定模型名稱】 === +# TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite') +model_name = "gemini-3.1-flash-lite" + +print(f"正在連線至 Google Cloud Model ({model_name})...") + +# === 【學員填空練習點 3:初始化 Gemini Client】 === +# TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數 +client = genai.Client(api_key=api_key) + +# === 【學員填空練習點 4:發送內容生成請求】 === +# TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞 +# 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。 +response = client.models.generate_content(model=model_name, +contents= "What's ur favorite game?") + +# === 【學員填空練習點 5:讀取並印出回應】 === +# TODO: 從 response 物件中取得 text 欄位內容,並列印出來。 +print("\n=== AI 回應結果 ===") +# 請在下方實作列印邏輯: print(response.text) \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..9c7cdcc41dc129b0848a190aa8fb9c425c2c376c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_1_gemini.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_1_gemini.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a7ff00b684e34d0c68bab0adcd53df7f1e52fef4 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_3_chatbot_memory_mock.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_3_chatbot_memory_mock.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock_game/d2_3_chatbot_memory_game_template.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/d2_3_chatbot_memory_game_template.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock_game/d2_3_chatbot_memory_game_template.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/d2_3_chatbot_memory_game_template.py" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..2d6d70db2b75c24d81bbc745f45d31d25e02825f --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_3_chatbot_memory_game_template.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_3_chatbot_memory_game_template.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock and AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" similarity index 75% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock and AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" index 8ea8eb9eaaa0b11ca9af7557f6cfc3feec926ae4..f8e8c8db30873f2d9d5974a4663358b0ef258575 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock and AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" @@ -1,103 +1,132 @@ -# d2_3_chatbot_memory.py -# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模) - -import os -from google import genai - -# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite -GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get( - "GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") - - -def local_mock_response(user_input, history): - user_input_lower = user_input.lower() - - # TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句 - if "上學時間" in user_input_lower or "時間" in user_input_lower: - return "我們的上課時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" - elif "籃球課" in user_input_lower or "籃球課價格" in user_input_lower: - return "一堂課為1000NTD" - elif "月票" in user_input_lower or "月票價格" in user_input_lower: - return "一個月有十堂課總價8500NTD。" - elif "教練" in user_input_lower or "教練人選與價格" in user_input_lower: - return "黑人教練1200NTD(因為他會灌籃) 麒洋67教練900NTD(他比較菜,而且他不會投籃,所以他不會教)。" - elif "報名" in user_input_lower or "如何報名" in user_input_lower: - return "去網站(ILOVEblackman.scammer.com)即可報名。" - elif "聯絡" in user_input_lower or "聯絡方式" in user_input_lower: - return "播打(0800676741419487)。" - elif "掰掰" in user_input_lower or "bye bye" in user_input_lower: - return "bye bye, u js got hard scammed.。" - else: - turns = len(history) // 2 + 1 - return ( - f"(模擬模式)收到您的訊息:『{user_input}』。" - f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?" - ) - - -def main(): - print("=" * 50) - print(" 得能 AI 智慧客服對話系統 (支援對話上下文記憶)") - print("=" * 50) - - use_mock = False - chat_session = None - - if not api_key: - print("[提示] 未檢測到環境變數 GEMINI_API_KEY,將自動啟用「離線模擬模式 (Local Mock Mode)」。") - print(" (如需連線雲端模型,請設定 GEMINI_API_KEY 環境變數)") - use_mock = True - else: - try: - print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...") - client = genai.Client(api_key=api_key) - #TODO: 設定系統指令 (System Instruction / System Prompt) - system_instruction = ( - "我們一堂課為1000NTD,我們一個月有十堂課總價8500NTD, 我們黑人教練1200NTD(因為他會灌籃) 麒洋67教練900NTD(他比較菜,而且他不會投籃,所以他不會教)。, 我們, " - "去網站(ILOVEblackman.scammer.com)即可報名,請播打(0800676741419487)" - ) - - chat_session = client.chats.create( - model=GEMINI_MODEL, - config={'system_instruction': system_instruction} - ) - print("雲端客服系統已就緒!") - except Exception as e: - print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...") - use_mock = True - - local_history = [] - - print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):") - print("-" * 50) - - while True: - try: - user_input = input("\n[您]: ").strip() - if not user_input: - continue - - if user_input.lower() in ["quit", "exit"]: - print("對話結束,感謝您的使用!") - break - - if use_mock: - ai_reply = local_mock_response(user_input, local_history) - local_history.append({"role": "user", "text": user_input}) - local_history.append({"role": "model", "text": ai_reply}) - else: - response = chat_session.send_message(user_input) - ai_reply = response.text - - print(f"[AI 客服]: {ai_reply}") - - except KeyboardInterrupt: - print("\n對話強行終止。再見!") - break - except Exception as e: - print(f"\n[錯誤] 發生異常:{e}") - - -if __name__ == "__main__": - main() +# d2_3_chatbot_memory.py +# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模) + +import os +from google import genai + +# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") +api_key = _telearn_require_gemini_api_key(allow_skip=True) + + +def local_mock_response(user_input, history): + user_input_lower = user_input.lower() + + # TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句 + if "上學時間" in user_input_lower or "時間" in user_input_lower: + return "我們的上課時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" + elif "籃球課" in user_input_lower or "籃球課價格" in user_input_lower: + return "一堂課為1000NTD" + elif "月票" in user_input_lower or "月票價格" in user_input_lower: + return "一個月有十堂課總價8500NTD。" + elif "教練" in user_input_lower or "教練人選與價格" in user_input_lower: + return "黑人教練1200NTD(因為他會灌籃) 麒洋67教練900NTD(他比較菜,而且他不會投籃,所以他不會教)。" + elif "報名" in user_input_lower or "如何報名" in user_input_lower: + return "去網站(ILOVEblackman.scammer.com)即可報名。" + elif "聯絡" in user_input_lower or "聯絡方式" in user_input_lower: + return "播打(0800676741419487)。" + elif "掰掰" in user_input_lower or "bye bye" in user_input_lower: + return "bye bye, u js got hard scammed.。" + else: + turns = len(history) // 2 + 1 + return ( + f"(模擬模式)收到您的訊息:『{user_input}』。" + f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?" + ) + + +def main(): + print("=" * 50) + print(" 得能 AI 智慧客服對話系統 (支援對話上下文記憶)") + print("=" * 50) + + use_mock = False + chat_session = None + + if not api_key: + print("[提示] 未輸入 GEMINI_API_KEY,改用「離線模擬模式 (Local Mock Mode)」。") + use_mock = True + else: + try: + print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...") + client = genai.Client(api_key=api_key) + #TODO: 設定系統指令 (System Instruction / System Prompt) + system_instruction = ( + "我們一堂課為1000NTD,我們一個月有十堂課總價8500NTD, 我們黑人教練1200NTD(因為他會灌籃) 麒洋67教練900NTD(他比較菜,而且他不會投籃,所以他不會教)。, 我們, " + "去網站(ILOVEblackman.scammer.com)即可報名,請播打(0800676741419487)" + ) + + chat_session = client.chats.create( + model=GEMINI_MODEL, + config={'system_instruction': system_instruction} + ) + print("雲端客服系統已就緒!") + except Exception as e: + print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...") + use_mock = True + + local_history = [] + + print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):") + print("-" * 50) + + while True: + try: + user_input = input("\n[您]: ").strip() + if not user_input: + continue + + if user_input.lower() in ["quit", "exit"]: + print("對話結束,感謝您的使用!") + break + + if use_mock: + ai_reply = local_mock_response(user_input, local_history) + local_history.append({"role": "user", "text": user_input}) + local_history.append({"role": "model", "text": ai_reply}) + else: + response = chat_session.send_message(user_input) + ai_reply = response.text + + print(f"[AI 客服]: {ai_reply}") + + except KeyboardInterrupt: + print("\n對話強行終止。再見!") + break + except Exception as e: + print(f"\n[錯誤] 發生異常:{e}") + + +if __name__ == "__main__": + main() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..4946a0a52b48301e190fe096df87684bd366261c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_4_chatbot_memory.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_4_chatbot_memory.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" index 88abf5a7de30aeab0aaf0a6532b8ed1e53e2767f..31380bb5e87d4a7601128574c3af6cd887d6001e 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" @@ -1,216 +1,216 @@ -""" -Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版) -================================================================== -本週挑戰目標: - 1. 實作「三指捏合 (Three-Finger Pinch)」: - 必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」 - 以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch - 2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊 - 3. 使用邊緣觸發 (Edge Detection) 防止重複觸發 - 4. 新增 30 秒限時機制,時間到顯示最終得分 -""" -import pygame -import cv2 -import mediapipe as mp -import math -import sys -import os -import random - -from mediapipe.tasks import python -from mediapipe.tasks.python import vision - -# ============================================ -# 1. 初始化 -# ============================================ -pygame.init() -WIDTH, HEIGHT = 800, 600 -screen = pygame.display.set_mode((WIDTH, HEIGHT)) -pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ") -clock = pygame.time.Clock() - -# 攝影機 -cap = cv2.VideoCapture(0) - -# 【TODO 1】初始化 MediaPipe 手勢偵測器 (Hand Landmarker) - -try: - base_options = python.BaseOptions(model_asset_path=os.path.join('hand_landmarker.task')) - options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) - detector = vision.HandLandmarker.create_from_options(options) -except Exception as e: - print(f"Error loading model: {e}") - pygame.quit() - sys.exit() - -# ============================================ -# 2. 遊戲變數 -# ============================================ -# 手指座標 -finger_x, finger_y = 400, 300 # 食指 -thumb_x, thumb_y = 400, 300 # 大拇指 -middle_x, middle_y = 400, 300 # 中指 - -# 距離變數 -dist_thumb_index = 0.0 -dist_thumb_middle = 0.0 - -# Pinch 相關變數 -is_pinching = False -was_pinching = False # 邊緣觸發 -PINCH_THRESHOLD = 40 # 距離臨界值 - -# 按鈕設定 -button_rect = pygame.Rect(300, 250, 200, 100) -button_colors = [(3, 11, 255), (234,255,3), - (100, 220, 100), (220, 180, 50)] -current_color_index = 0 - -# 點擊計數器 -click_count = 0 - -# --- 限時機制變數 --- -START_TIME = pygame.time.get_ticks() -GAME_DURATION = 30 # 遊戲限制時間(秒) -game_state = "PLAYING" # 狀態機 - -# ============================================ -# 3. 主迴圈 -# ============================================ -running = True -while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - # --- 讀取攝影機與手勢偵測 --- - success, img = cap.read() - is_pinching = False # 每幀重設狀態,防止手離屏時卡死 - - if success: - img = cv2.flip(img, 1) # 鏡像翻轉 - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - results = detector.detect(mp_image) - - # === 【TODO 2:挑戰關卡三之手部安全偵測】 === - # 1. 判斷手部是否在畫面中 (results.hand_landmarks 是否有資料) - # 2. 如果在畫面中,取得食指 (8)、大拇指 (4)、中指 (12) 的像素座標 - # 3. 如果手不在畫面中,將 is_pinching 設為 False - # - hand_in_frame = False - if results.hand_landmarks: - hand_in_frame = True - for hand_lms in results.hand_landmarks: - # 食指 (8) - lm8 = hand_lms[8] - finger_x = int(lm8.x * WIDTH) - finger_y = int(lm8.y * HEIGHT) - - # 大拇指 (4) - lm4 = hand_lms[4] - thumb_x = int(lm4.x * WIDTH) - thumb_y = int(lm4.y * HEIGHT) - - # 中指 (12) - lm12 = hand_lms[12] - middle_x = int(lm12.x * WIDTH) - middle_y = int(lm12.y * HEIGHT) - - - # === 【TODO 3:挑戰關卡一之三指捏合判定】 === - # 提示:計算食指與大拇指距離,以及中指與大拇指距離 - # 兩組距離皆小於 PINCH_THRESHOLD 時,is_pinching 才為 True - dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y) - dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y) - is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD) - - - - # --- 狀態機邏輯處理 --- - if game_state == "PLAYING": - # 1. 計算剩餘時間 - current_ticks = pygame.time.get_ticks() - elapsed_seconds = (current_ticks - START_TIME) / 1000 - remaining_time = max(0, GAME_DURATION - elapsed_seconds) - - # 2. 檢查時間是否到了 - if remaining_time <= 0: - game_state = "GAME_OVER" - - # 3. 邊緣觸發點擊判斷 - if is_pinching and not was_pinching: - finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) - if finger_rect.colliderect(button_rect): - click_count += 1 - current_color_index = (current_color_index + 1) % len(button_colors) - - # === 【TODO 4:挑戰關卡二之按鈕隨機移動】 === - # 提示:當點擊發生時,讓按鈕的 x 和 y 座標隨機分配在畫面內 - button_rect.x = random.randint(0, WIDTH - button_rect.width) - button_rect.y = random.randint(0, HEIGHT - button_rect.height) - - - print(f" 魔法三指點擊!第 {click_count} 次") - - # 更新上一幀狀態 - was_pinching = is_pinching - - # --- 繪製畫面 --- - screen.fill((20, 20, 35)) - font = pygame.font.SysFont(None, 32) - large_font = pygame.font.SysFont(None, 64) - - if game_state == "PLAYING": - # 繪製按鈕 - btn_color = button_colors[current_color_index] - pygame.draw.rect(screen, btn_color, button_rect, border_radius=16) - - btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255)) - screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2, - button_rect.centery - btn_text.get_height() // 2)) - - # 顯示倒數計時 - time_text = font.render(f"Time Left: {remaining_time:.1f}s", True, (255, 220, 100)) - screen.blit(time_text, (WIDTH - time_text.get_width() - 20, 10)) - - # 畫手指游標 - cursor_color = (50, 255, 50) if is_pinching else (255, 50, 50) - pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) # 食指 - pygame.draw.circle(screen, (255, 255, 255), (thumb_x, thumb_y), 12) # 大拇指 - pygame.draw.circle(screen, (100, 150, 255), (middle_x, middle_y), 12) # 中指 - - # === 【TODO 5:挑戰關卡三之畫面提示】 === - # 提示:當手不在畫面中時,在畫面上方顯示警告文字提醒學生將手部放回 - if not hand_in_frame: - warning_text = font.render("OUT OF FRAME - PLEASE PUT HANDS BACK", True, (255, 100, 100)) - screen.blit(warning_text, (WIDTH // 2 - warning_text.get_width() // 2, 100)) - - - - # 顯示調試數值 - small_font = pygame.font.SysFont(None, 24) - status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150)) - screen.blit(status, (10, 10)) - - dist_text = small_font.render(f"Dist Thumb-Index: {dist_thumb_index:.0f} px", True, (150, 150, 200)) - screen.blit(dist_text, (10, 35)) - - dist_text2 = small_font.render(f"Dist Thumb-Middle: {dist_thumb_middle:.0f} px", True, (150, 150, 200)) - screen.blit(dist_text2, (10, 60)) - - elif game_state == "GAME_OVER": - # 繪製遊戲結束畫面 - over_text = large_font.render("GAME OVER", True, (255, 50, 50)) - score_text = font.render(f"Total Clicks: {click_count} times", True, (255, 255, 255)) - - screen.blit(over_text, (WIDTH // 2 - over_text.get_width() // 2, HEIGHT // 2 - 50)) - screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, HEIGHT // 2 + 20)) - - pygame.display.update() - clock.tick(30) - -cap.release() -pygame.quit() -sys.exit() +""" +Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版) +================================================================== +本週挑戰目標: + 1. 實作「三指捏合 (Three-Finger Pinch)」: + 必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」 + 以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch + 2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊 + 3. 使用邊緣觸發 (Edge Detection) 防止重複觸發 + 4. 新增 30 秒限時機制,時間到顯示最終得分 +""" +import pygame +import cv2 +import mediapipe as mp +import math +import sys +import os +import random + +from mediapipe.tasks import python +from mediapipe.tasks.python import vision + +# ============================================ +# 1. 初始化 +# ============================================ +pygame.init() +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ") +clock = pygame.time.Clock() + +# 攝影機 +cap = cv2.VideoCapture(0) + +# 【TODO 1】初始化 MediaPipe 手勢偵測器 (Hand Landmarker) + +try: + base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hand_landmarker.task')) + options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) + detector = vision.HandLandmarker.create_from_options(options) +except Exception as e: + print(f"Error loading model: {e}") + pygame.quit() + sys.exit() + +# ============================================ +# 2. 遊戲變數 +# ============================================ +# 手指座標 +finger_x, finger_y = 400, 300 # 食指 +thumb_x, thumb_y = 400, 300 # 大拇指 +middle_x, middle_y = 400, 300 # 中指 + +# 距離變數 +dist_thumb_index = 0.0 +dist_thumb_middle = 0.0 + +# Pinch 相關變數 +is_pinching = False +was_pinching = False # 邊緣觸發 +PINCH_THRESHOLD = 40 # 距離臨界值 + +# 按鈕設定 +button_rect = pygame.Rect(300, 250, 200, 100) +button_colors = [(3, 11, 255), (234,255,3), + (100, 220, 100), (220, 180, 50)] +current_color_index = 0 + +# 點擊計數器 +click_count = 0 + +# --- 限時機制變數 --- +START_TIME = pygame.time.get_ticks() +GAME_DURATION = 30 # 遊戲限制時間(秒) +game_state = "PLAYING" # 狀態機 + +# ============================================ +# 3. 主迴圈 +# ============================================ +running = True +while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # --- 讀取攝影機與手勢偵測 --- + success, img = cap.read() + is_pinching = False # 每幀重設狀態,防止手離屏時卡死 + + if success: + img = cv2.flip(img, 1) # 鏡像翻轉 + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + # === 【TODO 2:挑戰關卡三之手部安全偵測】 === + # 1. 判斷手部是否在畫面中 (results.hand_landmarks 是否有資料) + # 2. 如果在畫面中,取得食指 (8)、大拇指 (4)、中指 (12) 的像素座標 + # 3. 如果手不在畫面中,將 is_pinching 設為 False + # + hand_in_frame = False + if results.hand_landmarks: + hand_in_frame = True + for hand_lms in results.hand_landmarks: + # 食指 (8) + lm8 = hand_lms[8] + finger_x = int(lm8.x * WIDTH) + finger_y = int(lm8.y * HEIGHT) + + # 大拇指 (4) + lm4 = hand_lms[4] + thumb_x = int(lm4.x * WIDTH) + thumb_y = int(lm4.y * HEIGHT) + + # 中指 (12) + lm12 = hand_lms[12] + middle_x = int(lm12.x * WIDTH) + middle_y = int(lm12.y * HEIGHT) + + + # === 【TODO 3:挑戰關卡一之三指捏合判定】 === + # 提示:計算食指與大拇指距離,以及中指與大拇指距離 + # 兩組距離皆小於 PINCH_THRESHOLD 時,is_pinching 才為 True + dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y) + dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y) + is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD) + + + + # --- 狀態機邏輯處理 --- + if game_state == "PLAYING": + # 1. 計算剩餘時間 + current_ticks = pygame.time.get_ticks() + elapsed_seconds = (current_ticks - START_TIME) / 1000 + remaining_time = max(0, GAME_DURATION - elapsed_seconds) + + # 2. 檢查時間是否到了 + if remaining_time <= 0: + game_state = "GAME_OVER" + + # 3. 邊緣觸發點擊判斷 + if is_pinching and not was_pinching: + finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) + if finger_rect.colliderect(button_rect): + click_count += 1 + current_color_index = (current_color_index + 1) % len(button_colors) + + # === 【TODO 4:挑戰關卡二之按鈕隨機移動】 === + # 提示:當點擊發生時,讓按鈕的 x 和 y 座標隨機分配在畫面內 + button_rect.x = random.randint(0, WIDTH - button_rect.width) + button_rect.y = random.randint(0, HEIGHT - button_rect.height) + + + print(f" 魔法三指點擊!第 {click_count} 次") + + # 更新上一幀狀態 + was_pinching = is_pinching + + # --- 繪製畫面 --- + screen.fill((20, 20, 35)) + font = pygame.font.SysFont(None, 32) + large_font = pygame.font.SysFont(None, 64) + + if game_state == "PLAYING": + # 繪製按鈕 + btn_color = button_colors[current_color_index] + pygame.draw.rect(screen, btn_color, button_rect, border_radius=16) + + btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255)) + screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2, + button_rect.centery - btn_text.get_height() // 2)) + + # 顯示倒數計時 + time_text = font.render(f"Time Left: {remaining_time:.1f}s", True, (255, 220, 100)) + screen.blit(time_text, (WIDTH - time_text.get_width() - 20, 10)) + + # 畫手指游標 + cursor_color = (50, 255, 50) if is_pinching else (255, 50, 50) + pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) # 食指 + pygame.draw.circle(screen, (255, 255, 255), (thumb_x, thumb_y), 12) # 大拇指 + pygame.draw.circle(screen, (100, 150, 255), (middle_x, middle_y), 12) # 中指 + + # === 【TODO 5:挑戰關卡三之畫面提示】 === + # 提示:當手不在畫面中時,在畫面上方顯示警告文字提醒學生將手部放回 + if not hand_in_frame: + warning_text = font.render("OUT OF FRAME - PLEASE PUT HANDS BACK", True, (255, 100, 100)) + screen.blit(warning_text, (WIDTH // 2 - warning_text.get_width() // 2, 100)) + + + + # 顯示調試數值 + small_font = pygame.font.SysFont(None, 24) + status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150)) + screen.blit(status, (10, 10)) + + dist_text = small_font.render(f"Dist Thumb-Index: {dist_thumb_index:.0f} px", True, (150, 150, 200)) + screen.blit(dist_text, (10, 35)) + + dist_text2 = small_font.render(f"Dist Thumb-Middle: {dist_thumb_middle:.0f} px", True, (150, 150, 200)) + screen.blit(dist_text2, (10, 60)) + + elif game_state == "GAME_OVER": + # 繪製遊戲結束畫面 + over_text = large_font.render("GAME OVER", True, (255, 50, 50)) + score_text = font.render(f"Total Clicks: {click_count} times", True, (255, 255, 255)) + + screen.blit(over_text, (WIDTH // 2 - over_text.get_width() // 2, HEIGHT // 2 - 50)) + screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, HEIGHT // 2 + 20)) + + pygame.display.update() + clock.tick(30) + +cap.release() +pygame.quit() +sys.exit() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..fcb42c4bf1d6f4dbdfca16288a1aa1e349c298ef --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d5_pinch_challenge_solution.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd5_pinch_challenge_solution.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\272\272\350\207\211\350\276\250\350\255\230/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\272\272\350\207\211\350\276\250\350\255\230/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\272\272\350\207\211\350\276\250\350\255\230/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..72b1075a69ea1ceeabd526fd9dbbcf8a32934405 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d3_2_face_detection2.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd3_2_face_detection2.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..f8a0bdc25b608ccbae54bb49b323c44e16586827 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step4_ear.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step4_ear.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274/step0_eyeguard_template.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" similarity index 98% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274/step0_eyeguard_template.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" index 4e00c38d711c83e6e480c3031ee76023923e48b6..761761cff314141ca06a23948a523e5c2e569e20 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274/step0_eyeguard_template.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" @@ -45,7 +45,7 @@ cap = cv2.VideoCapture(0) # 沒有攝影機就改成影片:cap = cv2.VideoCapture("backup_face_video.mp4") # 【TODO 1】初始化 MediaPipe 臉部偵測 (Face Landmarker) -base_options = python.BaseOptions(model_asset_path=os.path.join('face_landmarker.task')) +base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task')) options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) detector = vision.FaceLandmarker.create_from_options(options) diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274/step4_ear.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/step4_ear.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274/step4_ear.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/step4_ear.py" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220_\351\200\262\351\232\216\347\271\252\345\234\226/d1_4_visualization_coffee.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220_\351\200\262\351\232\216\347\271\252\345\234\226/d1_4_visualization_coffee.py" index 96b51133d0c82f5e167e8e2aae5613ddc1c71999..fe1e13be9b6d5f9be99a7faa1e6db222b5e08a5e 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220_\351\200\262\351\232\216\347\271\252\345\234\226/d1_4_visualization_coffee.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220_\351\200\262\351\232\216\347\271\252\345\234\226/d1_4_visualization_coffee.py" @@ -1,3 +1,5 @@ +import matplotlib +matplotlib.use('Agg') # Telearn hub headless # d1_4_visualization_coffee.py # Day 1: 資料科學與數據視覺化 - 咖啡廳營運數據進階分析(學員填空) # cd examples/Day1/student && python3 d1_4_visualization_coffee.py @@ -8,6 +10,34 @@ import pandas as pd import matplotlib.pyplot as plt + +def _telearn_plt_show(*_a, **_k): + """Hub-friendly plt.show: save current figure and display via OpenCV.""" + import os + import matplotlib.pyplot as plt + try: + import cv2 + except ImportError: + plt.savefig("_telearn_last_plot.png", dpi=150, bbox_inches="tight") + print("[Telearn] 已儲存 _telearn_last_plot.png(無 cv2 可顯示)") + return + path = os.path.join(os.getcwd(), "_telearn_last_plot.png") + plt.savefig(path, dpi=150, bbox_inches="tight") + img = cv2.imread(path) + if img is not None: + # fit to screen-ish width + h, w = img.shape[:2] + max_w = 960 + if w > max_w: + scale = max_w / float(w) + img = cv2.resize(img, (int(w * scale), int(h * scale))) + cv2.imshow("Chart (press any key)", img) + print("[Telearn] 圖表已顯示,按任意鍵關閉…") + cv2.waitKey(0) + cv2.destroyAllWindows() + else: + print(f"[Telearn] 圖表已儲存: {path}") + def load_csv(filename="coffee_sales_data.csv"): """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" # === 【學員填空練習點 1:CSV 檔案安全讀取】 === @@ -111,7 +141,7 @@ plt.tight_layout() # 自動調整間距,避免文字重疊 plt.savefig("coffee_sales_analysis.png", dpi=300) -plt.show() +_telearn_plt_show() # === 【學員填空練習點 6:圖表儲存與顯示】 === # TODO: 1. 呼叫 savefig 將畫布儲存為檔名 "coffee_sales_analysis.png",並設定 dpi=300 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220_\351\200\262\351\232\216\347\271\252\345\234\226/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220_\351\200\262\351\232\216\347\271\252\345\234\226/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..1f104b92c6250f22879c19e0c0198fe1a72e3ba3 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\345\210\206\346\236\220_\351\200\262\351\232\216\347\271\252\345\234\226/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_4_visualization_coffee.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_4_visualization_coffee.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..4f7550b344cc508f552737ddcfc49fc178221251 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step3_mouth_ratio.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step3_mouth_ratio.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233/step3_mouth_ratio.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/step3_mouth_ratio.py" similarity index 96% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233/step3_mouth_ratio.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/step3_mouth_ratio.py" index a1a218a6777e5772816ac5202d52c0f03b3c8bfe..cdc2a3518924aa8b51d5886f2cd4e4b613e950d3 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233/step3_mouth_ratio.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\347\234\274\347\235\233\351\226\213\345\220\210\345\201\265\346\270\254/step3_mouth_ratio.py" @@ -1,113 +1,113 @@ -""" -Week 11 - Step 2: Mouth Opening Ratio Detection (Template 填空版) -============================================================= -本步驟目標: - 1. 找出特定的臉部特徵點:眼角(33, 263)與嘴唇(13, 14) - 2. 計算兩眼距離,當作臉寬基準比例尺,防止人靠近或遠離鏡頭造成的誤判 (歸一化) - 3. 計算嘴巴張開比例:嘴巴距離 / 兩眼距離 - 4. 在 OpenCV 畫面上即時顯示比例與嘴巴開合狀態 (OPEN / CLOSED) -""" -import cv2 -import mediapipe as mp -import math -import sys -import os - -# 1. 初始化攝影機 -cap = cv2.VideoCapture(0) -if not cap.isOpened(): - print("Error: Could not open camera.") - sys.exit() - -# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) -model_path = os.path.join('face_landmarker.task') -if not os.path.exists(model_path): - print(f"Error: Model file not found at {model_path}") - sys.exit() - -try: - from mediapipe.tasks import python - from mediapipe.tasks.python import vision - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) - detector = vision.FaceLandmarker.create_from_options(options) - print("Face Landmarker initialized successfully.") -except Exception as e: - print(f"Error: Failed to initialize detector: {e}") - sys.exit() - -# 特徵點索引定義 -LEFT_EYE_OUTER = 33 -RIGHT_EYE_OUTER = 263 -LIP_TOP = 13 -LIP_BOTTOM = 14 - -# 嘴巴張開的比例閾值 (超過代表張開) -THRESHOLD = 0.35 - -print("Press 'q' in the window to exit.") - -# 3. 主迴圈 -while True: - success, img = cap.read() - if not success: - break - - img = cv2.flip(img, 1) - img_h, img_w, _ = img.shape - - # 影像處理 - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - results = detector.detect(mp_image) - - if results.face_landmarks: - face_lms = results.face_landmarks[0] - - # 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中 - pts = {} - for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]: - pt = face_lms[idx] - pts[idx] = (int(pt.x * img_w), int(pt.y * img_h)) - - - # 【TODO 2】計算兩眼外角距離 (做為基準比例尺) - p_lx, p_ly = pts[LEFT_EYE_OUTER] - p_rx, p_ry = pts[RIGHT_EYE_OUTER] - base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly) - - - if base_dist > 0: - # 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」 - p_top_x, p_top_y = pts[LIP_TOP] - p_bot_x, p_bot_y = pts[LIP_BOTTOM] - mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y) - mouth_ratio = mouth_dist / base_dist - - - # 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線 - # 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色) - is_open = mouth_ratio > THRESHOLD - cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線 - cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線 - - - # 畫出特徵點小黃圓圈 - for idx, pos in pts.items(): - cv2.circle(img, pos, 5, (0, 255, 255), -1) - - # 顯示比例與開合狀態文字 - cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) - status_text = "OPEN" if is_open else "CLOSED" - status_color = (0, 255, 0) if is_open else (0, 0, 255) - cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2) - else: - cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) - - cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img) - - if cv2.waitKey(1) & 0xFF == ord('q'): - break - -cap.release() -cv2.destroyAllWindows() +""" +Week 11 - Step 2: Mouth Opening Ratio Detection (Template 填空版) +============================================================= +本步驟目標: + 1. 找出特定的臉部特徵點:眼角(33, 263)與嘴唇(13, 14) + 2. 計算兩眼距離,當作臉寬基準比例尺,防止人靠近或遠離鏡頭造成的誤判 (歸一化) + 3. 計算嘴巴張開比例:嘴巴距離 / 兩眼距離 + 4. 在 OpenCV 畫面上即時顯示比例與嘴巴開合狀態 (OPEN / CLOSED) +""" +import cv2 +import mediapipe as mp +import math +import sys +import os + +# 1. 初始化攝影機 +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("Error: Could not open camera.") + sys.exit() + +# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) +model_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task') +if not os.path.exists(model_path): + print(f"Error: Model file not found at {model_path}") + sys.exit() + +try: + from mediapipe.tasks import python + from mediapipe.tasks.python import vision + base_options = python.BaseOptions(model_asset_path=model_path) + options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) + detector = vision.FaceLandmarker.create_from_options(options) + print("Face Landmarker initialized successfully.") +except Exception as e: + print(f"Error: Failed to initialize detector: {e}") + sys.exit() + +# 特徵點索引定義 +LEFT_EYE_OUTER = 33 +RIGHT_EYE_OUTER = 263 +LIP_TOP = 13 +LIP_BOTTOM = 14 + +# 嘴巴張開的比例閾值 (超過代表張開) +THRESHOLD = 0.35 + +print("Press 'q' in the window to exit.") + +# 3. 主迴圈 +while True: + success, img = cap.read() + if not success: + break + + img = cv2.flip(img, 1) + img_h, img_w, _ = img.shape + + # 影像處理 + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + if results.face_landmarks: + face_lms = results.face_landmarks[0] + + # 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中 + pts = {} + for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]: + pt = face_lms[idx] + pts[idx] = (int(pt.x * img_w), int(pt.y * img_h)) + + + # 【TODO 2】計算兩眼外角距離 (做為基準比例尺) + p_lx, p_ly = pts[LEFT_EYE_OUTER] + p_rx, p_ry = pts[RIGHT_EYE_OUTER] + base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly) + + + if base_dist > 0: + # 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」 + p_top_x, p_top_y = pts[LIP_TOP] + p_bot_x, p_bot_y = pts[LIP_BOTTOM] + mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y) + mouth_ratio = mouth_dist / base_dist + + + # 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線 + # 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色) + is_open = mouth_ratio > THRESHOLD + cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線 + cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線 + + + # 畫出特徵點小黃圓圈 + for idx, pos in pts.items(): + cv2.circle(img, pos, 5, (0, 255, 255), -1) + + # 顯示比例與開合狀態文字 + cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) + status_text = "OPEN" if is_open else "CLOSED" + status_color = (0, 255, 0) if is_open else (0, 0, 255) + cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2) + else: + cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) + + cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + +cap.release() +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..6e10ef440ce9cdd4de5bb4ee95fde52ce4fb20ce --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step2_facemesh_complete.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step2_facemesh_complete.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/step/\345\230\264\345\267\264/step2_facemesh_complete.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/step2_facemesh_complete.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/step/\345\230\264\345\267\264/step2_facemesh_complete.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/step2_facemesh_complete.py" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226&\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" similarity index 77% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226&\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" index bb859e055006fd0a9c2ff2c1abe8439663e1b68d..f2152679de35154e242331244967566ad3bce4a1 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226&\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" @@ -1,100 +1,130 @@ -# d1_3_visualization.py -# Day 1: 資料科學與數據視覺化 - Matplotlib 繪圖基礎(學員填空) -# cd examples/Day1/student && python3 d1_3_visualization.py -# 參考答案:../Teacher/d1_3_visualization.py - -import os -import pandas as pd -import matplotlib.pyplot as plt - - -def load_csv(filename="taipei_weather_2026.csv"): - """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" - # === 【學員填空練習點 1:CSV 檔案安全讀取】 === - - # - # - path = os.path.join(os.path.dirname(__file__), filename) - - if os.path.exists(path): - - return pd.read_csv(path) - else: - print("找不到CSV檔") - - -df = load_csv() - -# 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊 -plt.rcParams['font.sans-serif'] = [ - 'PingFang TC', 'Heiti TC', 'Arial Unicode MS', - 'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei', -] -plt.rcParams['axes.unicode_minus'] = False - -# === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 === -# TODO: 使用 plt.subplots 建立一個 2 列 1 行 (2x1) 的子圖畫布,尺寸大小為寬 10 英吋、高 8 英吋 -# 並指派給 fig 與 (ax1, ax2) -fig, (ax1, ax2) = plt.subplots(2, 1, figsize = (10, 8)) - -# 1. 繪製折線圖 (Line Plot) - 每日溫度 -# === 【學員填空練習點 3:繪製每日溫度折線圖】 === -ax1.plot(df['Date'], df['Humidity'], marker= 's' , color='orange', label= '濕度(%)', linewidth=2) -ax1.plot(df['Date'], df['Temperature'], marker= 'o' , color='crimson', label= '溫度(C)', linewidth=2) -ax1.set_title ("台北觀測站美日溫度趨勢圖", fontsize=14, fontweight='bold') -ax1.set_xlabel("日期") -ax1.set_ylabel("數值") -ax1.grid(True, linestyle='--', alpha=0.5) -ax1.legend() -ax1.tick_params(axis= 'x', rotation=45) -ax1.plot(df['Date'], df['Humidity'], marker= 's' , color='orange', label= '濕度(%)', linewidth=2) - - - - -ax2.bar(df['Date'], df['Rainfall'], color='skyblue', edgecolor='royalblue', label='降雨量(mm)') -ax2.set_title("台北觀測站每日降雨量分佈", fontsize=14, fontweight='bold') -ax2.set_xlabel("日期") -ax2.set_ylabel('降雨量(mm)') -ax2.grid(True, axis='y', linestyle='--', alpha=0.5) -ax2.legend() -ax2.tick_params(axis= 'x', rotation=45) - - - - -# 1. 設定參數:帶有圓點 marker='o',顏色 color='crimson',圖例 label='溫度 (°C)',線寬 linewidth=2 -# 2. 使用 ax1.set_title 設定標題 "台北觀測站每日溫度趨勢圖" -# 3. 設定 ax1.set_xlabel("日期") 與 ax1.set_ylabel("數值") -# 4. 設定網格 ax1.grid(True, linestyle='--', alpha=0.5) -# 5. 呼叫 ax1.legend() 顯示圖例 -# 6. 設定 X軸刻度旋轉 ax1.tick_params(axis='x', rotation=45) -# -# 請在下方完成 ax1 的溫度折線圖繪製: - - -# 2. 繪製條形圖 (Bar Plot) - 每日降雨量 -# === 【學員填空練習點 4:繪製每日降雨量長條圖】 === -# TODO: 1. 在 ax2 畫布上以 ax2.bar() 繪製長條圖,X軸傳入 df['Date'],Y軸傳入 df['Rainfall'] -# 設定參數:顏色 color='skyblue',外框顏色 edgecolor='royalblue',圖例 label='降雨量 (mm)' -# 2. 使用 ax2.set_title 設定標題 "台北觀測站每日降雨量分佈" -# 3. 設定 ax2.set_xlabel("日期") 與 ax2.set_ylabel("降雨量 (mm)") -# 4. 設定 Y 軸網格 ax2.grid(True, axis='y', linestyle='--', alpha=0.5) -# 5. 呼叫 ax2.legend() 顯示圖例 -# 6. 設定 X軸刻度旋轉 ax2.tick_params(axis='x', rotation=45) -# -# 請在下方完成 ax2 的降雨量長條圖繪製: - - -plt.tight_layout() # 自動調整間距,避免文字重疊 - - - -plt.savefig("taipei_weather_2026.png", dpi=300) -plt.show() -# === 【學員填空練習點 5:圖表自動儲存與顯示】 === -# TODO: 1. 呼叫 plt.savefig 將畫布儲存為檔名 "taipei_weather_2026.png",並設定高畫質 dpi=300 -# (⚠️ 注意:savefig 必須在 show() 之前呼叫) -# 2. 呼叫 plt.show() 將圖表視窗顯示在畫面上 -# -# 請在下方完成圖表存檔與顯示: +import matplotlib +matplotlib.use('Agg') # Telearn hub headless +# d1_3_visualization.py +# Day 1: 資料科學與數據視覺化 - Matplotlib 繪圖基礎(學員填空) +# cd examples/Day1/student && python3 d1_3_visualization.py +# 參考答案:../Teacher/d1_3_visualization.py + +import os +import pandas as pd +import matplotlib.pyplot as plt + + + +def _telearn_plt_show(*_a, **_k): + """Hub-friendly plt.show: save current figure and display via OpenCV.""" + import os + import matplotlib.pyplot as plt + try: + import cv2 + except ImportError: + plt.savefig("_telearn_last_plot.png", dpi=150, bbox_inches="tight") + print("[Telearn] 已儲存 _telearn_last_plot.png(無 cv2 可顯示)") + return + path = os.path.join(os.getcwd(), "_telearn_last_plot.png") + plt.savefig(path, dpi=150, bbox_inches="tight") + img = cv2.imread(path) + if img is not None: + # fit to screen-ish width + h, w = img.shape[:2] + max_w = 960 + if w > max_w: + scale = max_w / float(w) + img = cv2.resize(img, (int(w * scale), int(h * scale))) + cv2.imshow("Chart (press any key)", img) + print("[Telearn] 圖表已顯示,按任意鍵關閉…") + cv2.waitKey(0) + cv2.destroyAllWindows() + else: + print(f"[Telearn] 圖表已儲存: {path}") + +def load_csv(filename="taipei_weather_2026.csv"): + """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" + # === 【學員填空練習點 1:CSV 檔案安全讀取】 === + + # + # + path = os.path.join(os.path.dirname(__file__), filename) + + if os.path.exists(path): + + return pd.read_csv(path) + else: + print("找不到CSV檔") + + +df = load_csv() + +# 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊 +plt.rcParams['font.sans-serif'] = [ + 'PingFang TC', 'Heiti TC', 'Arial Unicode MS', + 'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei', +] +plt.rcParams['axes.unicode_minus'] = False + +# === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 === +# TODO: 使用 plt.subplots 建立一個 2 列 1 行 (2x1) 的子圖畫布,尺寸大小為寬 10 英吋、高 8 英吋 +# 並指派給 fig 與 (ax1, ax2) +fig, (ax1, ax2) = plt.subplots(2, 1, figsize = (10, 8)) + +# 1. 繪製折線圖 (Line Plot) - 每日溫度 +# === 【學員填空練習點 3:繪製每日溫度折線圖】 === +ax1.plot(df['Date'], df['Humidity'], marker= 's' , color='orange', label= '濕度(%)', linewidth=2) +ax1.plot(df['Date'], df['Temperature'], marker= 'o' , color='crimson', label= '溫度(C)', linewidth=2) +ax1.set_title ("台北觀測站美日溫度趨勢圖", fontsize=14, fontweight='bold') +ax1.set_xlabel("日期") +ax1.set_ylabel("數值") +ax1.grid(True, linestyle='--', alpha=0.5) +ax1.legend() +ax1.tick_params(axis= 'x', rotation=45) +ax1.plot(df['Date'], df['Humidity'], marker= 's' , color='orange', label= '濕度(%)', linewidth=2) + + + + +ax2.bar(df['Date'], df['Rainfall'], color='skyblue', edgecolor='royalblue', label='降雨量(mm)') +ax2.set_title("台北觀測站每日降雨量分佈", fontsize=14, fontweight='bold') +ax2.set_xlabel("日期") +ax2.set_ylabel('降雨量(mm)') +ax2.grid(True, axis='y', linestyle='--', alpha=0.5) +ax2.legend() +ax2.tick_params(axis= 'x', rotation=45) + + + + +# 1. 設定參數:帶有圓點 marker='o',顏色 color='crimson',圖例 label='溫度 (°C)',線寬 linewidth=2 +# 2. 使用 ax1.set_title 設定標題 "台北觀測站每日溫度趨勢圖" +# 3. 設定 ax1.set_xlabel("日期") 與 ax1.set_ylabel("數值") +# 4. 設定網格 ax1.grid(True, linestyle='--', alpha=0.5) +# 5. 呼叫 ax1.legend() 顯示圖例 +# 6. 設定 X軸刻度旋轉 ax1.tick_params(axis='x', rotation=45) +# +# 請在下方完成 ax1 的溫度折線圖繪製: + + +# 2. 繪製條形圖 (Bar Plot) - 每日降雨量 +# === 【學員填空練習點 4:繪製每日降雨量長條圖】 === +# TODO: 1. 在 ax2 畫布上以 ax2.bar() 繪製長條圖,X軸傳入 df['Date'],Y軸傳入 df['Rainfall'] +# 設定參數:顏色 color='skyblue',外框顏色 edgecolor='royalblue',圖例 label='降雨量 (mm)' +# 2. 使用 ax2.set_title 設定標題 "台北觀測站每日降雨量分佈" +# 3. 設定 ax2.set_xlabel("日期") 與 ax2.set_ylabel("降雨量 (mm)") +# 4. 設定 Y 軸網格 ax2.grid(True, axis='y', linestyle='--', alpha=0.5) +# 5. 呼叫 ax2.legend() 顯示圖例 +# 6. 設定 X軸刻度旋轉 ax2.tick_params(axis='x', rotation=45) +# +# 請在下方完成 ax2 的降雨量長條圖繪製: + + +plt.tight_layout() # 自動調整間距,避免文字重疊 + + + +plt.savefig("taipei_weather_2026.png", dpi=300) +_telearn_plt_show() +# === 【學員填空練習點 5:圖表自動儲存與顯示】 === +# TODO: 1. 呼叫 plt.savefig 將畫布儲存為檔名 "taipei_weather_2026.png",並設定高畫質 dpi=300 +# (⚠️ 注意:savefig 必須在 show() 之前呼叫) +# 2. 呼叫 _telearn_plt_show() 將圖表視窗顯示在畫面上 +# +# 請在下方完成圖表存檔與顯示: diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a4fd3671a6f3d6b1d49df8a77568c765d630e26d --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_3_visualization.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_3_visualization.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226&\351\225\267\346\242\235\345\234\226/taipei_weather_2026.csv" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/taipei_weather_2026.csv" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226&\351\225\267\346\242\235\345\234\226/taipei_weather_2026.csv" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/taipei_weather_2026.csv" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\275\261\345\203\217\350\231\225\347\220\206/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\275\261\345\203\217\350\231\225\347\220\206/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\275\261\345\203\217\350\231\225\347\220\206/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" index 0a3e03d883fa3431f9506fa0d645699ce83935c5..be834a74e72f2a875976333606e2d22489059aff 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" @@ -1,69 +1,109 @@ -# d3_1_opencv_basic.py -# Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版) -# 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。 - -import cv2 -import numpy as np - -# 1. 影像讀取 (Read) -img = cv2.imread("S.jpg") -if img is None: - print("[錯誤] 無法讀取 jpg") - exit(1) - -# 預設灰階影像為原始彩色影像,避免未解開註釋時程式出錯 -gray = img - -# === 【重要觀念 TODO 1:灰階轉換 (Grayscale Conversion)】 === -# 觀念重點:大多數電腦視覺演算法在處理單通道(黑白)影像時速度更快。 -# 將 BGR 彩色圖像轉換為單通道灰階圖像,可以減少 2/3 的資料量,且能過濾掉無用的顏色雜訊。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察變為灰階後的影像。 -gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - -# 預設模糊影像為灰階影像,避免未解開註釋時程式出錯 -blurred = gray - -# === 【重要觀念 TODO 2:高斯模糊 (Gaussian Blur)】 === -# 觀念重點:高斯模糊是透過像素加權平均來平滑影像,主要目的是「消除噪點和細微反光」。 -# 這可以避免後續邊緣偵測時把細微的噪點誤判為輪廓,讓偵測出來的邊緣更加平滑。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察影像邊緣的柔焦效果。 -blurred = cv2.GaussianBlur(gray, (5, 5), 0) - -# 預設邊緣影像為全黑畫布,避免未解開註釋時程式出錯 -edges = np.zeros(img.shape[:2], dtype=np.uint8) - -# === 【重要觀念 TODO 3:Canny 邊緣偵測 (Canny Edge Detection)】 === -# 觀念重點:Canny 演算法會找出影像中明暗變化劇烈的分界線。它使用雙閥值法: -# 高於高閥值(150)的 100% 判定為邊緣;介於高低閥值(50~150)之間的若連接強邊緣則保留;其餘捨棄。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察提取出的白色線條輪廓。 -edges = cv2.Canny(blurred, 10,11 ) - -# 預設無偵測到輪廓,且繪製圖像為原始圖像,避免未解開註釋時程式出錯 -contours = [] -contour_img = img.copy() - -# === 【重要觀念 TODO 4:尋找與繪製輪廓 (Find & Draw Contours)】 === -# 觀念重點:`findContours` 會在二值化(邊緣)影像中尋找相連的邊界,並建立幾何結構。 -# `drawContours` 則會將這些幾何結構繪製到指定的影像上(此處使用綠色框繪製於原始彩色影像)。 -# 請解開下方所有註釋 (刪除行首的 #) 並執行,觀察圖形外框被綠線框起來的結果。 -contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) -print(f"在影像中偵測到 {len(contours)} 個獨立輪廓特徵。") -contour_img = img.copy() -cv2.drawContours(contour_img, contours, -1, (0, 255, 0), 2) - -# === 【重要觀念 TODO 5:視窗顯示與釋放 (Window Display)】 === -# 觀念重點:使用 cv2.imshow 顯示各步驟處理結果,並用 waitKey(0) 讓程式停下等待按鍵, -# 最後使用 destroyAllWindows 釋放視窗資源。 -# 請解開下方所有註釋 (刪除行首的 #),以觀看影像處理的成果! -print("\n正在顯示影像處理結果(請點擊影像視窗並按下任意鍵關閉)...") -cv2.imshow("Original Shapes", img) -cv2.imshow("Grayscale", gray) -cv2.imshow("Canny Edges", edges) -cv2.imshow("Contours Output", contour_img) - -success_gray = cv2.imwrite("S.gray.jpg", gray) -success_edges = cv2.imwrite("S.edges.jpg", edges) -success_contour = cv2.imwrite("S.contour.jpg", contour_img) - -cv2.waitKey(0) -cv2.destroyAllWindows() +# d3_1_opencv_basic.py +# Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版) +# 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。 + +import cv2 +import numpy as np + +# 1. 影像讀取 (Read) + +def telearn_resolve_image(name): + """Resolve image path relative to script, cwd, or student image/ folders.""" + import os + from pathlib import Path + here = Path(__file__).resolve().parent + candidates = [ + Path(name), + here / name, + here / "image" / name, + here / "照片" / name, + here.parent / "image" / name, + here.parent / "照片" / name, + here.parent / "image_create" / name, + ] + # also try basename-only search one level + for base in [here, here.parent]: + for folder in ["image", "照片", "image_create", "处理过的照片"]: + p = base / folder + if p.is_dir(): + direct = p / name + candidates.append(direct) + # case-insensitive match + low = name.lower() + try: + for f in p.iterdir(): + if f.is_file() and f.name.lower() == low: + candidates.append(f) + except OSError: + pass + for c in candidates: + try: + if c.is_file(): + print(f"[Telearn] 讀取影像: {c}") + return str(c) + except OSError: + pass + print(f"[Telearn] 找不到影像 {name},搜尋過: {[str(c) for c in candidates[:8]]}…") + return name + +img = cv2.imread(telearn_resolve_image("S.jpg")) +if img is None: + print("[錯誤] 無法讀取 jpg") + exit(1) + +# 預設灰階影像為原始彩色影像,避免未解開註釋時程式出錯 +gray = img + +# === 【重要觀念 TODO 1:灰階轉換 (Grayscale Conversion)】 === +# 觀念重點:大多數電腦視覺演算法在處理單通道(黑白)影像時速度更快。 +# 將 BGR 彩色圖像轉換為單通道灰階圖像,可以減少 2/3 的資料量,且能過濾掉無用的顏色雜訊。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察變為灰階後的影像。 +gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + +# 預設模糊影像為灰階影像,避免未解開註釋時程式出錯 +blurred = gray + +# === 【重要觀念 TODO 2:高斯模糊 (Gaussian Blur)】 === +# 觀念重點:高斯模糊是透過像素加權平均來平滑影像,主要目的是「消除噪點和細微反光」。 +# 這可以避免後續邊緣偵測時把細微的噪點誤判為輪廓,讓偵測出來的邊緣更加平滑。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察影像邊緣的柔焦效果。 +blurred = cv2.GaussianBlur(gray, (5, 5), 0) + +# 預設邊緣影像為全黑畫布,避免未解開註釋時程式出錯 +edges = np.zeros(img.shape[:2], dtype=np.uint8) + +# === 【重要觀念 TODO 3:Canny 邊緣偵測 (Canny Edge Detection)】 === +# 觀念重點:Canny 演算法會找出影像中明暗變化劇烈的分界線。它使用雙閥值法: +# 高於高閥值(150)的 100% 判定為邊緣;介於高低閥值(50~150)之間的若連接強邊緣則保留;其餘捨棄。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察提取出的白色線條輪廓。 +edges = cv2.Canny(blurred, 10,11 ) + +# 預設無偵測到輪廓,且繪製圖像為原始圖像,避免未解開註釋時程式出錯 +contours = [] +contour_img = img.copy() + +# === 【重要觀念 TODO 4:尋找與繪製輪廓 (Find & Draw Contours)】 === +# 觀念重點:`findContours` 會在二值化(邊緣)影像中尋找相連的邊界,並建立幾何結構。 +# `drawContours` 則會將這些幾何結構繪製到指定的影像上(此處使用綠色框繪製於原始彩色影像)。 +# 請解開下方所有註釋 (刪除行首的 #) 並執行,觀察圖形外框被綠線框起來的結果。 +contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) +print(f"在影像中偵測到 {len(contours)} 個獨立輪廓特徵。") +contour_img = img.copy() +cv2.drawContours(contour_img, contours, -1, (0, 255, 0), 2) + +# === 【重要觀念 TODO 5:視窗顯示與釋放 (Window Display)】 === +# 觀念重點:使用 cv2.imshow 顯示各步驟處理結果,並用 waitKey(0) 讓程式停下等待按鍵, +# 最後使用 destroyAllWindows 釋放視窗資源。 +# 請解開下方所有註釋 (刪除行首的 #),以觀看影像處理的成果! +print("\n正在顯示影像處理結果(請點擊影像視窗並按下任意鍵關閉)...") +cv2.imshow("Original Shapes", img) +cv2.imshow("Grayscale", gray) +cv2.imshow("Canny Edges", edges) +cv2.imshow("Contours Output", contour_img) + +success_gray = cv2.imwrite("S.gray.jpg", gray) +success_edges = cv2.imwrite("S.edges.jpg", edges) +success_contour = cv2.imwrite("S.contour.jpg", contour_img) + +cv2.waitKey(0) +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..b223ab342e5fe7b1cc69da34d1bfed35520f5217 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d3_1_opencv_basic.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd3_1_opencv_basic.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/draw_game/draw_game_student.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" similarity index 97% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/draw_game/draw_game_student.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" index b2a0d86d39841078d372347745542228819ebd50..d2468a0890e2f8bafa315d899957d6b4aad141b9 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/draw_game/draw_game_student.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" @@ -1,3 +1,4 @@ +import os import cv2 import numpy as np import mediapipe as mp @@ -14,7 +15,7 @@ from mediapipe.tasks.python import vision # 2. HandLandmarkerOptions 用於設定手部偵測的細項設定,例如 num_hands=1 代表最多偵測一隻手。 # 3. 最後透過 vision.HandLandmarker.create_from_options 建立手部偵測器物件。 # 請解開下方三行程式碼的註釋: -base_options = python.BaseOptions(model_asset_path='hand_landmarker.task') +base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hand_landmarker.task')) options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) detector = vision.HandLandmarker.create_from_options(options) diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..b4b6ab0db6832a42e6763bb130c2aa53c4003caa --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: draw_game_student.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'draw_game_student.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..1a227558e5f14ebae620e0c2d7ff2baf91c9dfdc --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220/main.py" @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +"""多練習選單入口(自動產生)""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +sys.path.insert(0, str(_ROOT)) + +CHOICES = { + "1": 'd4_1_ex1_books.py', + "2": 'd4_1_ex2_ptt.py', + "3": 'd4_1_ex3_ithelp.py', + "4": 'd4_1_web_scraping.py', +} + +print("=== 本資料夾含多個練習,請選擇 ===") +print(' 1. d4_1_ex1_books.py') +print(' 2. d4_1_ex2_ptt.py') +print(' 3. d4_1_ex3_ithelp.py') +print(' 4. d4_1_web_scraping.py') +print(" (直接 Enter 預設 1)") +sel = input("編號: ").strip() or "1" +name = CHOICES.get(sel) or CHOICES.get("1") +print(f"執行: {name}") +runpy.run_path(str(_ROOT / name), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/d4_1_ex2_ptt.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/d4_1_ex2_ptt.py" new file mode 100644 index 0000000000000000000000000000000000000000..4c0ad885893936a77642ecaba8a3ac6c53c8e88a --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/d4_1_ex2_ptt.py" @@ -0,0 +1,81 @@ +# d4_1_ex2_ptt.py — Day 4 學員填空 +# cd examples/Day4/student && python d4_1_ex2_ptt.py +# 參考答案:../d4_1_ex2_ptt.py + +import requests +from bs4 import BeautifulSoup +from urllib.parse import urljoin + +# 目標網站:PTT Python 討論板首頁 +URL = "https://www.ptt.cc/bbs/Python/index.html" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 填入瀏覽器的 User-Agent,否則 PTT 會拒絕請求並回傳 403 錯誤 +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在抓取 PTT Python 板最新文章: {URL} ...") + +try: + response = requests.get(URL, headers=HEADERS, timeout=10) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 2:定位文章區塊】 === + # TODO: 觀察網頁 HTML 結構,找出所有包裹文章資訊的
標籤(其 class 名稱為何?) + posts = soup.find_all("div", class_="r-ent") + + print(f"\n成功取得 {len(posts)} 筆文章項目(含已刪除與置底公告):") + print("=" * 80) + + for idx, post in enumerate(posts, 1): + # === 【學員填空練習點 3:提取文章標題與連結並防錯 (Null Check)】 === + # TODO: + # 1. 尋找 class 為 "title" 的
標籤 + # 2. 在該
底下尋找 標籤 (即文章連結) + # 3. ⚠️ 重要思考:若文章已刪除,將不會有 標籤。請撰寫條件判斷: + # - 若 標籤存在:提取其文字,並使用 urljoin 拼接 href 屬性得到完整網址。 + # - 若 標籤不存在:直接將 title_div 的文字作為標題,並將網址設為 "無 (文章已刪除)"。 + title_div = post.find("div", class_="title") + title_link = title_div.find("a") if title_div else None + + if title_link: + title = title_link.get("title") + relative_url = title_link.get("href") + post_url = urljoin(URL, relative_url) + else: + # 文章已刪除 + title_div = post.find("div", class_="title") + title = title_div.get_text().strip() if title_div else "(已刪除)" + post_url = "無 (文章已刪除)" + + + # === 【學員填空練習點 4:提取推文數】 === + # TODO: 尋找 class 為 "nrec" 的
標籤。 + # 技巧:使用 .get_text().strip() 取得文字。若無文字(0 推),請將變數預設為 "0"。 + nrec_div = post.find("div", class_="nrec") + nrec = nrec_div.get_text().strip() if nrec_div else "0" + + # === 【學員填空練習點 5:提取作者與發文日期】 === + # TODO: 分別尋找 class 為 "author" 與 "date" 的
標籤,並提取純文字(記得使用 .strip() 去除空白) + author = post.find("div", class_="author").get_text().strip() + date = post.find("div", class_="date").get_text().strip() + + # 輸出單篇文章資訊 + print(f"[{idx}] 推文數: {nrec:3s} | 發文日期: {date} | 作者: {author}") + print(f" 標題: {title}") + print(f" 網址: {post_url}") + print("-" * 80) + +except requests.exceptions.HTTPError as http_err: + print(f"[錯誤] PTT 伺服器拒絕或找不到網頁:{http_err}") +except requests.exceptions.RequestException as e: + print(f"[錯誤] 網路請求失敗:{e}") +except Exception as ex: + print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..e271a2a8c0d74bb4e05e2cc91e7720a53098f5cf --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex2_ptt.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex2_ptt.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_books/d4_1_ex1_books.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_books/d4_1_ex1_books.py" new file mode 100644 index 0000000000000000000000000000000000000000..e3722d718a124cba097e1ef8b0d5385d238534e5 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_books/d4_1_ex1_books.py" @@ -0,0 +1,94 @@ +# d4_1_ex1_books.py — Day 4 學員填空 +# cd examples/Day4/student && python d4_1_ex1_books.py +# 參考答案:../d4_1_ex1_books.py + +import requests +from bs4 import BeautifulSoup +from urllib.parse import urljoin + +# 目標網站:專供爬蟲練習的圖書商城沙盒 +URL = "http://books.toscrape.com/" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 建立 headers 字典,填入 "User-Agent" 來模擬真實瀏覽器(降低被簡易防爬機制拒絕的機率) +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在連線至圖書沙盒網站: {URL} ...") + +try: + # 發送 GET 請求取得網頁 HTML + response = requests.get(URL, headers=HEADERS, timeout=10) + + # 檢查 HTTP 狀態碼是否為 200 (OK),非 200 時引發 HTTPError 異常 + response.raise_for_status() + + # 解決編碼問題:強制將解碼設定為 utf-8 (避免 requests 預設 ISO-8859-1 解碼導致英鎊符號亂碼且在 Windows 印出時崩潰) + response.encoding = response.apparent_encoding + + print("連線成功!開始解析書籍資料...\n") + + # 初始化 BeautifulSoup 解析器 + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 2:定位 HTML 標籤區塊】 === + # TODO: 找出網頁中所有書籍的容器。每本書都被包在一個
標籤中,且該標籤的 class 固定為 "product_pod" + books = soup.find_all("article", class_="product_pod") + + print(f"成功在本頁找到 {len(books)} 本書籍:") + print("=" * 70) + + for idx, book in enumerate(books, 1): + # === 【學員填空練習點 3:提取書籍名稱與連結】 === + # TODO: + # 1. 尋找

底下的 標籤 + # 2. 取得 的 "title" 屬性(因為畫面上長度限制,.get_text() 可能會被 ellipsis 截斷) + # 3. 取得 的 "href" 屬性並使用 urljoin 拼接成完整網址 + title_link = book.find("h3").find("a") + title = title_link.get("title") + + relative_url = title_link.get("href") + detail_url = urljoin(URL, relative_url) + + # === 【學員填空練習點 4:提取書籍價格】 === + # 提示:為了避免 Windows (CP950) 終端機不支援英鎊符號 '£' 而編碼崩潰,請加上 .replace("£", "GBP") 將其替換 + # 觀察:價格存放在 class 為 "price_color" 的

標籤中,格式如 "£51.77" + # 技巧:將英鎊符號 '£' 替換為 'GBP',避免在 Windows (CP950) 終端機印出時因不支援該字元而崩潰 + price_text = book.find("p", class_="price_color").get_text().replace("£", "GBP") + + # === 【學員填空練習點 5:提取評價星等 (Rating)】 === + # TODO: + # 1. 尋找 class 為 "star-rating" 的

標籤 + # 2. 取得該標籤的 "class" 屬性值(這會是一個串列,如 ['star-rating', 'Three']) + # 3. 提取該串列的第二個元素作為星等字串 (若存在) + + rating_tag = book.find("p", class_="star-rating") + rating_classes = rating_tag.get("class") + rating = "無" + if len(rating_classes) > 1: + rating = rating_classes[1] # 提取例如 'Three' (代表三顆星) + + # === 【學員填空練習點 6:提取庫存狀態】 === + # TODO: 尋找 class 為 "instock availability" 的

標籤,取得文字後去除首尾空白 (.strip()) + stock_text = book.find("p", class_="instock availability").get_text().strip() + + # 印出解析結果 + print(f"【編號 {idx}】") + print(f"[書名] {title}") + print(f"[價格] {price_text}") + print(f"[評價] {rating} 星") + print(f"[狀態] {stock_text}") + print(f"[連結] {detail_url}") + print("-" * 70) + +except requests.exceptions.HTTPError as http_err: + print(f"[錯誤] HTTP 連線異常:{http_err}") +except requests.exceptions.RequestException as e: + print(f"[錯誤] 網路請求失敗:{e}") +except Exception as ex: + print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..afd501d3f608ba4c046ff4983d27e9763690b3b7 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex1_books.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex1_books.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/d4_1_ex3_ithelp.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/d4_1_ex3_ithelp.py" new file mode 100644 index 0000000000000000000000000000000000000000..c0299624ce527f07bab03c30f85184640b1ba40c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/d4_1_ex3_ithelp.py" @@ -0,0 +1,98 @@ +# d4_1_ex3_ithelp.py — Day 4 學員填空 +# cd examples/Day4/student && python d4_1_ex3_ithelp.py +# 參考答案:../d4_1_ex3_ithelp.py + +import requests +from bs4 import BeautifulSoup + +# 目標網站:iT 邦幫忙技術文章首頁 +URL = "https://ithelp.ithome.com.tw/articles?tab=tech" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 填入瀏覽器的 User-Agent,否則可能被伺服器拒絕連線 +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在連線至 iT 邦幫忙並抓取技術文章: {URL} ...") + +try: + response = requests.get(URL, headers=HEADERS, timeout=10) + response.raise_for_status() + + print("連線成功!解析文章列表中...\n") + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 2:定位文章卡片區塊】 === + # TODO: 尋找所有包裹文章資訊的

標籤(其 class 為 "qa-list") + articles = soup.find_all("div", class_="qa-list") + + print(f"成功取得前 {len(articles)} 篇熱門技術文章:") + print("=" * 80) + + for idx, art in enumerate(articles, 1): + # === 【學員填空練習點 3:提取標題與超連結】 === + # TODO: + # 1. 在 art 中尋找 class 為 "qa-list__title-link" 的 標籤 + # 2. 提取其純文字內容並去除首尾空白 (.strip()),作為標題 + # 3. 提取其 "href" 屬性,作為文章連結 + title_link = art.find("a", class_="qa-list__title-link") + if not title_link: + continue + title = title_link.get_text().strip() + post_url = title_link.get("href").strip() + + # === 【學員填空練習點 4:提取作者名稱】 === + # TODO: + # 1. 尋找 class 為 "qa-list__info" 的
標籤 + # 2. 在該
下尋找 class 為 "qa-list__info-link" 的 標籤 + # 3. 提取其文字內容作為作者名稱 + info_div = art.find("div", class_="qa-list__info") + author = "未知作者" + if info_div: + author_link = info_div.find("a", class_="qa-list__info-link") + if author_link: + author = author_link.get_text().strip() + + # === 【學員填空練習點 5:提取發文時間】 === + # TODO: 尋找 class 為 "qa-list__info-time" 的 標籤,並提取其 "title" 屬性值(因 title 屬性中包含完整時間秒數) + time_link = art.find("div", class_="qa-list__info-time") + post_time = time_link.get("title") if time_link else "未知時間" + + # === 【學員填空練習點 6:利用 List 索引提取多項數據】 === + # TODO: + # 1. 使用 find_all 找出卡片中所有 class 為 "qa-condition" 的 標籤 + # 2. 當抓取到的數量大於等於 3 時,利用 List 索引取得對應的資訊: + # - 索引 0 的項目底下的 "qa-condition__count" :Like 數 + # - 索引 1 的項目底下的 "qa-condition__count" :回應數 + # - 索引 2 的項目底下的 "qa-condition__count" :瀏覽數 + conditions = art.find_all("a", class_="qa-condition") + + likes = "0" + comments = "0" + views = "0" + + if len(conditions) >= 3: + # 請在此處填入利用 conditions[0]、conditions[1]、conditions[2] 尋找 span 的代碼 + likes = conditions[0].find("span", class_="qa-condition__count").get_text().strip() + comments = conditions[1].find("span", class_="qa-condition__count").get_text().strip() + views = conditions[2].find("span", class_="qa-condition__count").get_text().strip() + + # 格式化輸出 + print(f"【排名 {idx}】") + print(f"[標題] {title}") + print(f"[作者] {author} | [時間] {post_time}") + print(f"[數據] Like: {likes} | 回應: {comments} | 瀏覽/訂閱: {views}") + print(f"[連結] {post_url}") + print("-" * 80) + +except requests.exceptions.HTTPError as http_err: + print(f"[錯誤] 伺服器回應異常:{http_err}") +except requests.exceptions.RequestException as e: + print(f"[錯誤] 網路請求失敗:{e}") +except Exception as ex: + print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..2ee124121c9acf2163ab18a3f956886b02cea663 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex3_ithelp.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex3_ithelp.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/d4_1_web_scraping.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/d4_1_web_scraping.py" new file mode 100644 index 0000000000000000000000000000000000000000..26792f288eaa50ea59f82a077985c21c5578d14d --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/d4_1_web_scraping.py" @@ -0,0 +1,68 @@ +# d4_1_web_scraping.py — Day4 學員填空 +# cd examples/Day4/student && python3 d4_1_web_scraping.py +# 參考答案:../d4_1_web_scraping.py + +import requests +from bs4 import BeautifulSoup + +# 本範例抓取專供網頁爬蟲教學使用的模擬沙盒網站:Quotes to Scrape +URL = "http://quotes.toscrape.com/" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 建立 headers 字典,填入 "User-Agent" 來模擬真實瀏覽器(降低被簡易防爬機制拒絕的機率) +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在連線並抓取網站內容: {URL} ...") + +try: + # === 【學員填空練習點 2:連線請求與下載網頁】 === + # TODO: 1. 使用 requests.get 發送 GET 請求,帶入 headers=HEADERS 與設定 timeout=5 + # TODO: 2. 判斷 response.status_code 是否為 200 (下載成功) + response = requests.get(URL, headers=HEADERS, timeout=5) + + if response and response.status_code == 200: + print("網頁下載成功!開始解析 HTML 結構...") + + # === 【學員填空練習點 3:初始化 BeautifulSoup 解析器】 === + # TODO: 使用 BeautifulSoup 解析 response.text,初始化 html.parser + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 4:定位 HTML 標籤區塊】 === + # TODO: 找出網頁中所有名言與作者的 div 標籤(其 class 為 'quote') + quote_divs = soup.find_all("div", class_="quote") + + print(f"\n成功解析!本日精選名言如下(共 {len(quote_divs)} 筆):") + print("=" * 60) + + # === 【學員填空練習點 5:迴圈遍歷提取內容與防錯】 === + # TODO: + # 1. 遍歷 quote_divs,並使用 enumerate 取得索引 idx(從 1 開始) + # 2. 用 .find() 尋找 span 且 class 為 'text' 的名言元素 + # 3. 用 .find() 尋找 small 且 class 為 'author' 的作者元素 + # 4. 進行 None 防錯處理,確保兩者皆非空值後,使用 .get_text() 提取純文字並印出 + for idx, quote_div in enumerate(quote_divs, 1): + text_el = quote_div.find("span", class_="text") + author_el = quote_div.find("small", class_="author") + if text_el is None or author_el is None: + continue + text = text_el.get_text() + author = author_el.get_text() + + print(f"[{idx}] \"{text}\"") + print(f" -- by {author}\n") + + print("=" * 60) + print("提醒:請尊重 robots.txt 與網站使用條款,勿對目標網站發送高頻請求。") + else: + status = response.status_code if response else "未知" + print(f"[錯誤] 無法抓取網頁,伺服器回應狀態碼:{status}") + +except requests.exceptions.RequestException as e: + print(f"[錯誤] 連線失敗:{e}") + print("請確認電腦的網路連線是否通暢。") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..e51446f49e2d44e7c5116b2f9764bfee686f6e7e --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_web_scraping.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_web_scraping.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220and AI \345\240\261\345\221\212/d4_2_agent_task_chain.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_agent_task_chain.py" similarity index 80% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220and AI \345\240\261\345\221\212/d4_2_agent_task_chain.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_agent_task_chain.py" index d41fb36c97b21ad5bc3efe39c3c6a67761af0ffd..52a9e552f9173da72d7b65dc85802f91d1c9d53f 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220and AI \345\240\261\345\221\212/d4_2_agent_task_chain.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_agent_task_chain.py" @@ -7,8 +7,39 @@ import os import requests from bs4 import BeautifulSoup + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") +api_key = _telearn_require_gemini_api_key(allow_skip=True) HEADERS = { "User-Agent": ( @@ -96,7 +127,7 @@ def run_task_chain(topic): f"- 模擬數據:\"Love is all, love is new.\" -- John Lennon\n\n" f"## 二、 核心思想分析\n" f"本機模擬分析指出,此主題的語錄主要著眼於人與人之間的連結與心靈啟發。\n" - f"*(注意:此為 Mock 離線生成報告,設定 GEMINI_API_KEY 以啟用 Gemini 真實生成。)*\n" + f"*(注意:此為 Mock 離線生成報告,於啟動時輸入 GEMINI_API_KEY 以啟用 Gemini 真實生成。)*\n" ) else: print(f"\n[步驟 2] 正在調用 Gemini ({GEMINI_MODEL}) 進行深度分析與排版...") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220and AI \345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" similarity index 88% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220and AI \345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" index bb63b174f7029aa21e37a7ca700113e8ee252ba9..af3b968d3a4571db41f1d5fb0bd7bda92a9eec41 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220and AI \345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" @@ -9,8 +9,39 @@ from urllib.parse import urljoin from google import genai # 設定預設使用的 Gemini 模型 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") +api_key = _telearn_require_gemini_api_key(allow_skip=True) HEADERS = { "User-Agent": ( @@ -175,7 +206,7 @@ def run_analysis_chain(source_type): # 步驟 2: AI 分析 (或 Mock 降級防禦) if not api_key: - print("\n[步驟 2] 未偵測到環境變數 GEMINI_API_KEY,自動啟動 Mock 離線降級機制...") + print("\n[步驟 2] 未輸入 GEMINI_API_KEY,啟動 Mock 離線降級機制...") # === 【學員填空練習點 5:Mock 降級防衛機制】 === # TODO 5: 請設計一段模擬的 Markdown 格式分析報告字串,指派給 report_content。 # 報告內容至少需要包含標題與數據摘要,避免程式在無 Key 時報錯中斷。 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a5ae937359f5ab6c3d6b6730ee4cf5c043ea685a --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_2_ex2_crawler_ai.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_2_ex2_crawler_ai.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\2113d/face_mesh_connections.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/face_mesh_connections.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\2113d/face_mesh_connections.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/face_mesh_connections.py" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..1c74dbd4c33dd2ab30e9666859e7d417aa7bfb6c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step1_facemesh.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step1_facemesh.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\2113d/step1_facemesh.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/step1_facemesh.py" similarity index 96% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\2113d/step1_facemesh.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/step1_facemesh.py" index 5784c4c7b468e14274619427f8f8753d7441ed0b..f458732abd9dba06c1688209f81a568cda62b693 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\2113d/step1_facemesh.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\207\211\351\203\2503D\346\250\241\345\236\213/step1_facemesh.py" @@ -1,129 +1,129 @@ -""" -Week 11 - Step 1: Face Mesh Tracking with Connections (Template 填空版) -=================================================================== -本步驟目標: - 1. 使用 OpenCV 開啟視訊鏡頭 - 2. 載入 MediaPipe 臉部特徵偵測器 (face_landmarker.task) - 3. 偵測人臉,並在臉上畫出綠色的 3D 特效網格線與黃色特徵點 -""" -import cv2 -import mediapipe as mp -import numpy as np -import sys -import os - -# 1. 初始化攝影機 -cap = cv2.VideoCapture(0) -if not cap.isOpened(): - print("Error: Could not open camera.") - sys.exit() - -# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) -model_path = os.path.join('face_landmarker.task') -if not os.path.exists(model_path): - print(f"Error: Model file not found at {model_path}") - print("Please make sure face_landmarker.task is in the examples folder.") - sys.exit() - -try: - from mediapipe.tasks import python - from mediapipe.tasks.python import vision - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) - detector = vision.FaceLandmarker.create_from_options(options) - print("Face Landmarker Tasks API initialized successfully.") -except Exception as e: - print(f"Error initializing detector: {e}") - sys.exit() - -# 匯入臉部網格連接點定義 -try: - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - from face_mesh_connections import FACEMESH_TESSELATION -except ImportError: - FACEMESH_TESSELATION = [] - -# 繪圖模式:0 = 僅畫網格線, 1 = 僅畫點點, 2 = 點線都畫 -draw_mode = 0 - -print("Press 'q' in the window to exit, or 'm' to toggle display modes.") - -# 3. 主迴圈 -while True: - success, img = cap.read() - if not success: - print("Error: Failed to read frame.") - break - - # 【TODO 1】畫面水平翻轉,並將 BGR 轉成 MediaPipe 需要的 RGB 格式 - img = cv2.flip(img, 1) - img_h, img_w, _ = img.shape - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - - - - # 執行臉部偵測 - results = detector.detect(mp_image) - - # 【TODO 2】如果偵測到臉部,畫出 3D 特效網格線 - # 提示:取出 results.face_landmarks[0],並用迴圈巡覽 FACEMESH_TESSELATION - # 點的座標 (lm.x, lm.y) 是 0~1 的比例,需要乘上 img_w 和 img_h 換算成像素座標 - if results.face_landmarks: - face_lms = results.face_landmarks[0] - num_lms = len(face_lms) - - if draw_mode == 0 or draw_mode == 2: - for start_idx, end_idx in FACEMESH_TESSELATION: - if start_idx < num_lms and end_idx < num_lms: - lm1 = face_lms[start_idx] - lm2 = face_lms[end_idx] - pt1 = (int(lm1.x * img_w), int(lm1.y * img_h)) - pt2 = (int(lm2.x * img_w), int(lm2.y * img_h)) - cv2.line(img, pt1, pt2, (224, 224, 0), 1) # 青綠色線條 - - # 預設執行代碼 (填寫完後學生會將此替換或解除上方註解) - if results.face_landmarks: - face_lms = results.face_landmarks[0] - num_lms = len(face_lms) - - if draw_mode == 0 or draw_mode == 2: - for start_idx, end_idx in FACEMESH_TESSELATION: - if start_idx < num_lms and end_idx < num_lms: - lm1 = face_lms[start_idx] - lm2 = face_lms[end_idx] - pt1 = (int(lm1.x * img_w), int(lm1.y * img_h)) - pt2 = (int(lm2.x * img_w), int(lm2.y * img_h)) - cv2.line(img, pt1, pt2, (224, 224, 0), 1) - - # 【TODO 3】畫出臉部 468 個黃色特徵點點 - if draw_mode == 1 or draw_mode == 2: - for lm in face_lms: - cx = int(lm.x * img_w) - cy = int(lm.y * img_h) - cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) # 黃色小圓點 - - if draw_mode == 1 or draw_mode == 2: - for lm in face_lms: - cx = int(lm.x * img_w) - cy = int(lm.y * img_h) - cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) - - mode_str = "Mesh" if draw_mode == 0 else "Dots" if draw_mode == 1 else "Both" - cv2.putText(img, f"Face Detected | Mode: {mode_str}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) - else: - cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) - - # 顯示操作提示文字 - cv2.putText(img, "Press 'm' to toggle mode | 'q' to quit", (10, img_h - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1) - - cv2.imshow("Face Mesh Connections", img) - - key = cv2.waitKey(1) & 0xFF - if key == ord('q'): - break - elif key == ord('m'): - draw_mode = (draw_mode + 1) % 3 - -cap.release() -cv2.destroyAllWindows() +""" +Week 11 - Step 1: Face Mesh Tracking with Connections (Template 填空版) +=================================================================== +本步驟目標: + 1. 使用 OpenCV 開啟視訊鏡頭 + 2. 載入 MediaPipe 臉部特徵偵測器 (face_landmarker.task) + 3. 偵測人臉,並在臉上畫出綠色的 3D 特效網格線與黃色特徵點 +""" +import cv2 +import mediapipe as mp +import numpy as np +import sys +import os + +# 1. 初始化攝影機 +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("Error: Could not open camera.") + sys.exit() + +# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) +model_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task') +if not os.path.exists(model_path): + print(f"Error: Model file not found at {model_path}") + print("Please make sure face_landmarker.task is in the examples folder.") + sys.exit() + +try: + from mediapipe.tasks import python + from mediapipe.tasks.python import vision + base_options = python.BaseOptions(model_asset_path=model_path) + options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) + detector = vision.FaceLandmarker.create_from_options(options) + print("Face Landmarker Tasks API initialized successfully.") +except Exception as e: + print(f"Error initializing detector: {e}") + sys.exit() + +# 匯入臉部網格連接點定義 +try: + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + from face_mesh_connections import FACEMESH_TESSELATION +except ImportError: + FACEMESH_TESSELATION = [] + +# 繪圖模式:0 = 僅畫網格線, 1 = 僅畫點點, 2 = 點線都畫 +draw_mode = 0 + +print("Press 'q' in the window to exit, or 'm' to toggle display modes.") + +# 3. 主迴圈 +while True: + success, img = cap.read() + if not success: + print("Error: Failed to read frame.") + break + + # 【TODO 1】畫面水平翻轉,並將 BGR 轉成 MediaPipe 需要的 RGB 格式 + img = cv2.flip(img, 1) + img_h, img_w, _ = img.shape + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + + + + # 執行臉部偵測 + results = detector.detect(mp_image) + + # 【TODO 2】如果偵測到臉部,畫出 3D 特效網格線 + # 提示:取出 results.face_landmarks[0],並用迴圈巡覽 FACEMESH_TESSELATION + # 點的座標 (lm.x, lm.y) 是 0~1 的比例,需要乘上 img_w 和 img_h 換算成像素座標 + if results.face_landmarks: + face_lms = results.face_landmarks[0] + num_lms = len(face_lms) + + if draw_mode == 0 or draw_mode == 2: + for start_idx, end_idx in FACEMESH_TESSELATION: + if start_idx < num_lms and end_idx < num_lms: + lm1 = face_lms[start_idx] + lm2 = face_lms[end_idx] + pt1 = (int(lm1.x * img_w), int(lm1.y * img_h)) + pt2 = (int(lm2.x * img_w), int(lm2.y * img_h)) + cv2.line(img, pt1, pt2, (224, 224, 0), 1) # 青綠色線條 + + # 預設執行代碼 (填寫完後學生會將此替換或解除上方註解) + if results.face_landmarks: + face_lms = results.face_landmarks[0] + num_lms = len(face_lms) + + if draw_mode == 0 or draw_mode == 2: + for start_idx, end_idx in FACEMESH_TESSELATION: + if start_idx < num_lms and end_idx < num_lms: + lm1 = face_lms[start_idx] + lm2 = face_lms[end_idx] + pt1 = (int(lm1.x * img_w), int(lm1.y * img_h)) + pt2 = (int(lm2.x * img_w), int(lm2.y * img_h)) + cv2.line(img, pt1, pt2, (224, 224, 0), 1) + + # 【TODO 3】畫出臉部 468 個黃色特徵點點 + if draw_mode == 1 or draw_mode == 2: + for lm in face_lms: + cx = int(lm.x * img_w) + cy = int(lm.y * img_h) + cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) # 黃色小圓點 + + if draw_mode == 1 or draw_mode == 2: + for lm in face_lms: + cx = int(lm.x * img_w) + cy = int(lm.y * img_h) + cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) + + mode_str = "Mesh" if draw_mode == 0 else "Dots" if draw_mode == 1 else "Both" + cv2.putText(img, f"Face Detected | Mode: {mode_str}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + else: + cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) + + # 顯示操作提示文字 + cv2.putText(img, "Press 'm' to toggle mode | 'q' to quit", (10, img_h - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1) + + cv2.imshow("Face Mesh Connections", img) + + key = cv2.waitKey(1) & 0xFF + if key == ord('q'): + break + elif key == ord('m'): + draw_mode = (draw_mode + 1) % 3 + +cap.release() +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\250\255\350\250\210AI\350\247\222\350\211\262and \345\240\264\346\231\257/d2_2_prompt_engineering.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" similarity index 56% rename from "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\250\255\350\250\210AI\350\247\222\350\211\262and \345\240\264\346\231\257/d2_2_prompt_engineering.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" index b19f164b34db109a68c211edd92197c89052db72..ff92386f852d471d8f282b87b796d9964ff03a31 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\250\255\350\250\210AI\350\247\222\350\211\262and \345\240\264\346\231\257/d2_2_prompt_engineering.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" @@ -1,58 +1,86 @@ -# d2_2_prompt_engineering.py -# Day 2: 生成式 AI 串接實戰 - Prompt 工程 (系統指令與結構化輸出) -import os -from google import genai - -# 取得 API 金鑰 -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JvbUqLW-wcddeMducmta0aGE2QtcrmF4Kfd8nnpZ9mzg") -if not api_key: - print("[提示] 請輸入您的 Google AI Studio API 金鑰:") - api_key = input("API Key: ").strip() - -client = genai.Client(api_key=api_key) - -# 1. 設置系統指令 (System Instruction / System Prompt) -# TODO 1: -system_prompt = "你是國中英語老師, 你很專業但是你又是老屁孩" - -print("正在初始化英語教學助理模型...") - -# 2. Few-shot (少樣本) 引導與對話演練 -# TODO 2: -user_word = "六七" - -print(f"\n[學生輸入]: {user_word}") -prompt = f"請幫我翻譯這個詞:'{user_word}'。" - -response = client.models.generate_content( - model='gemini-3.1-flash-lite', - contents=prompt, - config={'system_instruction': system_prompt} -) - -print("\n[AI 老師回應]:") -print(response.text) - - -# 3. 結構化 JSON 輸出 (Structured Output) 範例 -print("\n" + "="*40) -print("進階挑戰:要求模型回傳 JSON 格式") -print("="*40) - -# 我們可以在 prompt 中要求,並透過 configuration 設定 JSON 輸出格式 -# TODO 3: -json_prompt = ( - f"請將中文單字 '{user_word}' 的翻譯結果整理成 JSON 格式,包含以下三個欄位:\n" - "- 'english': 英文單字\n" - "- 'kk_phonetic': KK音標\n" - "- 'example_sentence': 適合國中生理解的英文例句與中文翻譯" - "- '用法':在什麼時機可以使用這個字" - ) - -response = client.models.generate_content( - model='gemini-3.1-flash-lite', - contents=json_prompt, - config={"response_mime_type": "application/json"} -) -print("\n[AI 回傳的原始 JSON]:") -print(response.text) +# d2_2_prompt_engineering.py +# Day 2: 生成式 AI 串接實戰 - Prompt 工程 (系統指令與結構化輸出) +import os +from google import genai + +# 取得 API 金鑰 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +api_key = _telearn_require_gemini_api_key(allow_skip=False) + +client = genai.Client(api_key=api_key) + +# 1. 設置系統指令 (System Instruction / System Prompt) +# TODO 1: +system_prompt = "你是國中英語老師, 你很專業但是你又是老屁孩" + +print("正在初始化英語教學助理模型...") + +# 2. Few-shot (少樣本) 引導與對話演練 +# TODO 2: +user_word = "六七" + +print(f"\n[學生輸入]: {user_word}") +prompt = f"請幫我翻譯這個詞:'{user_word}'。" + +response = client.models.generate_content( + model='gemini-3.1-flash-lite', + contents=prompt, + config={'system_instruction': system_prompt} +) + +print("\n[AI 老師回應]:") +print(response.text) + + +# 3. 結構化 JSON 輸出 (Structured Output) 範例 +print("\n" + "="*40) +print("進階挑戰:要求模型回傳 JSON 格式") +print("="*40) + +# 我們可以在 prompt 中要求,並透過 configuration 設定 JSON 輸出格式 +# TODO 3: +json_prompt = ( + f"請將中文單字 '{user_word}' 的翻譯結果整理成 JSON 格式,包含以下三個欄位:\n" + "- 'english': 英文單字\n" + "- 'kk_phonetic': KK音標\n" + "- 'example_sentence': 適合國中生理解的英文例句與中文翻譯" + "- '用法':在什麼時機可以使用這個字" + ) + +response = client.models.generate_content( + model='gemini-3.1-flash-lite', + contents=json_prompt, + config={"response_mime_type": "application/json"} +) +print("\n[AI 回傳的原始 JSON]:") +print(response.text) diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..5f819ed5e0570c50eae23f77a85ad0660170f673 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_2_prompt_engineering.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_2_prompt_engineering.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..69cdb69578cc4ce89cad8c1538eb2191bd6e5200 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\347\245\272\351\210\236/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_2_data_cleaning.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_2_data_cleaning.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" index 2eb731a53ce01cb4f5ce185ec06c76e019ef80c4..a2bb9583eb0a192b99679c8e8bb1ebeb472222d3 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/d1_1_pandas_intro.py" @@ -1,38 +1,39 @@ -# d1_1_pandas_intro.py -# Day 1 學員填空:Pandas 基礎(完成 TODO 後執行) -# cd examples/Day1/student && python3 d1_1_pandas_intro.py -# 參考答案:../Teacher/d1_1_pandas_intro.py - -import pandas as pd -import numpy as np - -df = pd.read_csv("taipei_weather_2026.csv") -print("=== 數據載入成功! ===") -df = pd.DataFrame(df) -print("\n--- 1. head ---") -print(df.head()) - -print("\n--- 2. info ---") -df.info() - -print("\n--- 3. describe ---") -print(df.describe()) - -print("\n--- 4. Temperature 前 5 筆 ---") -print(df['Temperature'].head()) - -print("\n--- 5. Date 與 Rainfall ---") -print(df[["Date", "Rainfall"]].head()) - -print("\n--- 挑戰:最高溫與平均濕度 ---") -max_temp = df['Temperature'].max() -avg_humidity = df['Humidity'].mean() -min_rainfall = df['Rainfall'].min() -print(f"最高溫度:{max_temp}°C") -print(f"平均濕度:{avg_humidity:.2f}%") -print(f"最小降雨量:{min_rainfall}mm") - -df['Egypt_Can_Come'] = (df['Temperature'] > 27.0) & (df['Rainfall'] == 0.0) -print(df) - - +import os +# d1_1_pandas_intro.py +# Day 1 學員填空:Pandas 基礎(完成 TODO 後執行) +# cd examples/Day1/student && python3 d1_1_pandas_intro.py +# 參考答案:../Teacher/d1_1_pandas_intro.py + +import pandas as pd +import numpy as np + +df = pd.read_csv(os.path.join(os.path.dirname(__file__), "taipei_weather_2026.csv")) +print("=== 數據載入成功! ===") +df = pd.DataFrame(df) +print("\n--- 1. head ---") +print(df.head()) + +print("\n--- 2. info ---") +df.info() + +print("\n--- 3. describe ---") +print(df.describe()) + +print("\n--- 4. Temperature 前 5 筆 ---") +print(df['Temperature'].head()) + +print("\n--- 5. Date 與 Rainfall ---") +print(df[["Date", "Rainfall"]].head()) + +print("\n--- 挑戰:最高溫與平均濕度 ---") +max_temp = df['Temperature'].max() +avg_humidity = df['Humidity'].mean() +min_rainfall = df['Rainfall'].min() +print(f"最高溫度:{max_temp}°C") +print(f"平均濕度:{avg_humidity:.2f}%") +print(f"最小降雨量:{min_rainfall}mm") + +df['Egypt_Can_Come'] = (df['Temperature'] > 27.0) & (df['Rainfall'] == 0.0) +print(df) + + diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..19029aca2c352f92bc7a6ace63c7161bf8b10e90 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/DataFrame\346\214\207\344\273\244\347\267\264\347\277\222/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_1_pandas_intro.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_1_pandas_intro.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" index c3c4db6288f7275f35d87d907709d726e184b3db..9fa6d8382d84be2da9a52efd1b69c265078377ad 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/d2_1_gemini.py" @@ -1,43 +1,65 @@ -# d2_1_gemini.py — Day2 學員填空 -# cd examples/Day2/Student && python3 d2_1_gemini.py -# 參考答案:../Teacher/d2_1_gemini.py - -import os -from google import genai - -# === 【學員填空練習點 1:設置與載入 API Key】 === -# TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。 -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JynZRgTmhZIMkFFN5GHSFClFfC3sraiSQ4Pb8d9v0V6Q") - -if not api_key: - print("[提示] 未檢測到環境變數 GEMINI_API_KEY。") - print(" 請先設定:export GEMINI_API_KEY=\"your_key\"") - print(" 或於下方手動輸入(僅供本機測試,勿寫死在檔案中上傳):") - api_key = input("API Key: ").strip() - if not api_key: - print("[結束] 未提供 API Key。可改練習 d2_1_ollama_local.py。") - exit(0) - -# === 【學員填空練習點 2:設定模型名稱】 === -# TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite') -model_name = "gemini-3.1-flash-lite" - -print(f"正在連線至 Google Cloud Model ({model_name})...") - -# === 【學員填空練習點 3:初始化 Gemini Client】 === -# TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數 -client = genai.Client(api_key=api_key) - -# === 【學員填空練習點 4:發送內容生成請求】 === -# TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞 -# 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。 -response = client.models.generate_content( - model=model_name, - contents = "請以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。" -) - -# === 【學員填空練習點 5:讀取並印出回應】 === -# TODO: 從 response 物件中取得 text 欄位內容,並列印出來。 -print("\n=== AI 回應結果 ===") -# 請在下方實作列印邏輯: -print(response.text) +# d2_1_gemini.py — Day2 學員填空 +# cd examples/Day2/Student && python3 d2_1_gemini.py +# 參考答案:../Teacher/d2_1_gemini.py + +import os +from google import genai + +# === 【學員填空練習點 1:設置與載入 API Key】 === +# TODO: 嘗試從環境變數 GEMINI_API_KEY 取得 API Key。如果未設定,則使用 input() 提示手動輸入。 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +api_key = _telearn_require_gemini_api_key(allow_skip=False) + +# === 【學員填空練習點 2:設定模型名稱】 === +# TODO: 設定使用的 Gemini 模型名稱(例如 'gemini-3.1-flash-lite') +model_name = "gemini-3.1-flash-lite" + +print(f"正在連線至 Google Cloud Model ({model_name})...") + +# === 【學員填空練習點 3:初始化 Gemini Client】 === +# TODO: 呼叫 genai.Client 並傳入 api_key 參數來初始化 Client 物件,指派給 client 變數 +client = genai.Client(api_key=api_key) + +# === 【學員填空練習點 4:發送內容生成請求】 === +# TODO: 使用 client.models.generate_content 呼叫模型,傳入 model 參數與 contents 提示詞 +# 提示詞內容:請模型以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。 +response = client.models.generate_content( + model=model_name, + contents = "請以繁體中文解釋什麼是『API 串接』,並用餐廳點餐作為比喻,字數約 100 字。" +) + +# === 【學員填空練習點 5:讀取並印出回應】 === +# TODO: 從 response 物件中取得 text 欄位內容,並列印出來。 +print("\n=== AI 回應結果 ===") +# 請在下方實作列印邏輯: +print(response.text) diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..9c7cdcc41dc129b0848a190aa8fb9c425c2c376c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/ai_api\344\270\262\346\216\245\347\267\264\347\277\222/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_1_gemini.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_1_gemini.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a7ff00b684e34d0c68bab0adcd53df7f1e52fef4 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock_\351\227\234\351\215\265\350\251\236\346\212\223\345\217\226/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_3_chatbot_memory_mock.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_3_chatbot_memory_mock.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock_game/d2_3_chatbot_memory_game_template.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/d2_3_chatbot_memory_game_template.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock_game/d2_3_chatbot_memory_game_template.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/d2_3_chatbot_memory_game_template.py" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..2d6d70db2b75c24d81bbc745f45d31d25e02825f --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\345\257\206\345\256\244\351\200\203\350\204\253\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_3_chatbot_memory_game_template.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_3_chatbot_memory_game_template.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock_and_AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" similarity index 81% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock_and_AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" index 5ddf7415e666d0e0124a24fae0b6de58f6f03eba..ff4ee921d7c5c244c9e24cc25f60c96c587f9c8c 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock_and_AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/d2_4_chatbot_memory.py" @@ -1,120 +1,149 @@ -# d2_3_chatbot_memory.py -# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模) - -import os -from google import genai - -# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite -GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get( - "GEMINI_API_KEY", "AQ.Ab8RN6JynZRgTmhZIMkFFN5GHSFClFfC3sraiSQ4Pb8d9v0V6Q") - - -def local_mock_response(user_input, history): - user_input_lower = user_input.lower() - - # TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句 - if "營業時間" in user_input_lower or "時間" in user_input_lower: - return "我們的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" - elif "課程" in user_input_lower or "課程資訊" in user_input_lower: - return "數學課早上9點到12點,周二到周五,英語課下午1點到3點,周三到五. 國文課下午3點到5點,周一到周五." - elif"價格" in user_input_lower or "多少錢" in user_input_lower: - return "我們的課程價格為每堂課 700 元,有折扣優惠。" - elif "優惠" in user_input_lower or "折扣" in user_input_lower: - return "目前我們提供新學員首次報名可享有 9 折優惠,購買套票有8折優惠。" - elif "套票價格" in user_input_lower or "套票" in user_input_lower: - return "套票為 10 堂課,價格為 5600 元,原價為7000元。" - elif "你好" in user_input_lower or "您好" in user_input_lower: - return "您好!我是補習班客服,請問是要看課程資訊還是報名流程呢? 還是營業時間, 也有試聽課程的體驗喔!" - elif "報名流程" in user_input_lower or "報名" in user_input_lower: - return "報名流程很簡單,您可以先選擇課程,然後上官網填寫報名表格(Ilovelearning67.com),最後完成付款即可。請問要看課程資訊嗎?" - elif "再見" in user_input_lower or "bye" in user_input_lower: - return "感謝您的尋問,再見!" - elif "試聽" in user_input_lower or "試聽課程" in user_input_lower: - return "我們有提供每門課程一次免費試聽的機會喔!歡迎提供您的姓名與電話,我們會請專人為您安排。" - elif "師資" in user_input_lower or "老師" in user_input_lower or "誰上課" in user_input_lower: - return "我們的數學老師畢業於台大數學系,英語老師是從美國來台10年的黑人,有TOEIC證書,國文老師畢業於政大中文系。" - elif "付款" in user_input_lower or "繳費" in user_input_lower or "刷卡" in user_input_lower or "匯款" in user_input_lower: - return "我們支援多種付款方式,包含線上刷卡、ATM 轉帳以及超商條碼繳費,您可以選擇最便利的方式付款喔!" - else: - turns = len(history) // 2 + 1 - return ( - f"(模擬模式)收到您的訊息:『{user_input}』。" - f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?" - ) - - -def main(): - print("=" * 50) - print(" 得能 AI 智慧客服對話系統 (支援對話上下文記憶)") - print("=" * 50) - - use_mock = False - chat_session = None - - if not api_key: - print("[提示] 未檢測到環境變數 GEMINI_API_KEY,將自動啟用「離線模擬模式 (Local Mock Mode)」。") - print(" (如需連線雲端模型,請設定 GEMINI_API_KEY 環境變數)") - use_mock = True - else: - try: - print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...") - client = genai.Client(api_key=api_key) - # TODO: 設定系統指令 (System Instruction / System Prompt) - system_instruction = ( - "你是一位專業的補習班客服人員,語氣溫和客氣" - "你需要回答使用者關於課程資訊、價格、優惠、套票價格、營業時間等問題" - "親切" - "我們的課程價格為每堂課 700 元,有折扣優惠" - "我們的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" - "我們提供新學員首次報名可享有 9 折優惠,購買套票有8折優惠。" - "我們的課程價格為每堂課 700 元,有折扣優惠。" - "套票為 10 堂課,價格為 5600 元,原價為7000元。" - "我們有提供每門課程一次免費試聽的機會喔!歡迎提供您的姓名與電話,我們會請專人為您安排。" - "我們支援多種付款方式,包含線上刷卡、ATM 轉帳以及超商條碼繳費" - "我們的數學老師畢業於台大數學系,英語老師是從美國來台10年的黑人,有TOEIC證書,國文老師畢業於政大中文系。" - ) - - chat_session = client.chats.create( - model=GEMINI_MODEL, - config={'system_instruction': system_instruction} - ) - print("雲端客服系統已就緒!") - except Exception as e: - print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...") - use_mock = True - - local_history = [] - - print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):") - print("-" * 50) - - while True: - try: - user_input = input("\n[您]: ").strip() - if not user_input: - continue - - if user_input.lower() in ["quit", "exit"]: - print("對話結束,感謝您的使用!") - break - - if use_mock: - ai_reply = local_mock_response(user_input, local_history) - local_history.append({"role": "user", "text": user_input}) - local_history.append({"role": "model", "text": ai_reply}) - else: - response = chat_session.send_message(user_input) - ai_reply = response.text - - print(f"[AI 客服]: {ai_reply}") - - except KeyboardInterrupt: - print("\n對話強行終止。再見!") - break - except Exception as e: - print(f"\n[錯誤] 發生異常:{e}") - - -if __name__ == "__main__": - main() +# d2_3_chatbot_memory.py +# Day 2: 生成式 AI 串接實戰 - 具對話記憶的智慧客服機器人 (Gemini / Local Mock 雙模) + +import os +from google import genai + +# 優先讀取環境變數 GEMINI_MODEL,預設為 gemini-3.1-flash-lite + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") +api_key = _telearn_require_gemini_api_key(allow_skip=True) + + +def local_mock_response(user_input, history): + user_input_lower = user_input.lower() + + # TODO: 處理 營業時間 / 你好 / 再見,其餘回兜底句 + if "營業時間" in user_input_lower or "時間" in user_input_lower: + return "我們的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" + elif "課程" in user_input_lower or "課程資訊" in user_input_lower: + return "數學課早上9點到12點,周二到周五,英語課下午1點到3點,周三到五. 國文課下午3點到5點,周一到周五." + elif"價格" in user_input_lower or "多少錢" in user_input_lower: + return "我們的課程價格為每堂課 700 元,有折扣優惠。" + elif "優惠" in user_input_lower or "折扣" in user_input_lower: + return "目前我們提供新學員首次報名可享有 9 折優惠,購買套票有8折優惠。" + elif "套票價格" in user_input_lower or "套票" in user_input_lower: + return "套票為 10 堂課,價格為 5600 元,原價為7000元。" + elif "你好" in user_input_lower or "您好" in user_input_lower: + return "您好!我是補習班客服,請問是要看課程資訊還是報名流程呢? 還是營業時間, 也有試聽課程的體驗喔!" + elif "報名流程" in user_input_lower or "報名" in user_input_lower: + return "報名流程很簡單,您可以先選擇課程,然後上官網填寫報名表格(Ilovelearning67.com),最後完成付款即可。請問要看課程資訊嗎?" + elif "再見" in user_input_lower or "bye" in user_input_lower: + return "感謝您的尋問,再見!" + elif "試聽" in user_input_lower or "試聽課程" in user_input_lower: + return "我們有提供每門課程一次免費試聽的機會喔!歡迎提供您的姓名與電話,我們會請專人為您安排。" + elif "師資" in user_input_lower or "老師" in user_input_lower or "誰上課" in user_input_lower: + return "我們的數學老師畢業於台大數學系,英語老師是從美國來台10年的黑人,有TOEIC證書,國文老師畢業於政大中文系。" + elif "付款" in user_input_lower or "繳費" in user_input_lower or "刷卡" in user_input_lower or "匯款" in user_input_lower: + return "我們支援多種付款方式,包含線上刷卡、ATM 轉帳以及超商條碼繳費,您可以選擇最便利的方式付款喔!" + else: + turns = len(history) // 2 + 1 + return ( + f"(模擬模式)收到您的訊息:『{user_input}』。" + f"這是我與您的第 {turns} 次對話,請問還有其他需要協助的嗎?" + ) + + +def main(): + print("=" * 50) + print(" 得能 AI 智慧客服對話系統 (支援對話上下文記憶)") + print("=" * 50) + + use_mock = False + chat_session = None + + if not api_key: + print("[提示] 未輸入 GEMINI_API_KEY,改用「離線模擬模式 (Local Mock Mode)」。") + use_mock = True + else: + try: + print(f"正在連線 Google Gemini 雲端客服伺服器 ({GEMINI_MODEL})...") + client = genai.Client(api_key=api_key) + # TODO: 設定系統指令 (System Instruction / System Prompt) + system_instruction = ( + "你是一位專業的補習班客服人員,語氣溫和客氣" + "你需要回答使用者關於課程資訊、價格、優惠、套票價格、營業時間等問題" + "親切" + "我們的課程價格為每堂課 700 元,有折扣優惠" + "我們的營業時間為週一至週五的上午 9:00 至下午 6:00,週末休息。" + "我們提供新學員首次報名可享有 9 折優惠,購買套票有8折優惠。" + "我們的課程價格為每堂課 700 元,有折扣優惠。" + "套票為 10 堂課,價格為 5600 元,原價為7000元。" + "我們有提供每門課程一次免費試聽的機會喔!歡迎提供您的姓名與電話,我們會請專人為您安排。" + "我們支援多種付款方式,包含線上刷卡、ATM 轉帳以及超商條碼繳費" + "我們的數學老師畢業於台大數學系,英語老師是從美國來台10年的黑人,有TOEIC證書,國文老師畢業於政大中文系。" + ) + + chat_session = client.chats.create( + model=GEMINI_MODEL, + config={'system_instruction': system_instruction} + ) + print("雲端客服系統已就緒!") + except Exception as e: + print(f"連線雲端客服失敗 ({e}),切換至離線模擬模式...") + use_mock = True + + local_history = [] + + print("\n系統啟動完成!請輸入您要諮詢的問題(輸入 'quit' 或 'exit' 結束對話):") + print("-" * 50) + + while True: + try: + user_input = input("\n[您]: ").strip() + if not user_input: + continue + + if user_input.lower() in ["quit", "exit"]: + print("對話結束,感謝您的使用!") + break + + if use_mock: + ai_reply = local_mock_response(user_input, local_history) + local_history.append({"role": "user", "text": user_input}) + local_history.append({"role": "model", "text": ai_reply}) + else: + response = chat_session.send_message(user_input) + ai_reply = response.text + + print(f"[AI 客服]: {ai_reply}") + + except KeyboardInterrupt: + print("\n對話強行終止。再見!") + break + except Exception as e: + print(f"\n[錯誤] 發生異常:{e}") + + +if __name__ == "__main__": + main() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..4946a0a52b48301e190fe096df87684bd366261c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/mock\350\210\207AI_api\351\233\231\346\250\241\345\274\217/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_4_chatbot_memory.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_4_chatbot_memory.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\270\211\346\214\207/d5_pinch_challenge_solution.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" similarity index 97% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\270\211\346\214\207/d5_pinch_challenge_solution.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" index 88abf5a7de30aeab0aaf0a6532b8ed1e53e2767f..31380bb5e87d4a7601128574c3af6cd887d6001e 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\270\211\346\214\207/d5_pinch_challenge_solution.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/d5_pinch_challenge_solution.py" @@ -1,216 +1,216 @@ -""" -Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版) -================================================================== -本週挑戰目標: - 1. 實作「三指捏合 (Three-Finger Pinch)」: - 必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」 - 以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch - 2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊 - 3. 使用邊緣觸發 (Edge Detection) 防止重複觸發 - 4. 新增 30 秒限時機制,時間到顯示最終得分 -""" -import pygame -import cv2 -import mediapipe as mp -import math -import sys -import os -import random - -from mediapipe.tasks import python -from mediapipe.tasks.python import vision - -# ============================================ -# 1. 初始化 -# ============================================ -pygame.init() -WIDTH, HEIGHT = 800, 600 -screen = pygame.display.set_mode((WIDTH, HEIGHT)) -pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ") -clock = pygame.time.Clock() - -# 攝影機 -cap = cv2.VideoCapture(0) - -# 【TODO 1】初始化 MediaPipe 手勢偵測器 (Hand Landmarker) - -try: - base_options = python.BaseOptions(model_asset_path=os.path.join('hand_landmarker.task')) - options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) - detector = vision.HandLandmarker.create_from_options(options) -except Exception as e: - print(f"Error loading model: {e}") - pygame.quit() - sys.exit() - -# ============================================ -# 2. 遊戲變數 -# ============================================ -# 手指座標 -finger_x, finger_y = 400, 300 # 食指 -thumb_x, thumb_y = 400, 300 # 大拇指 -middle_x, middle_y = 400, 300 # 中指 - -# 距離變數 -dist_thumb_index = 0.0 -dist_thumb_middle = 0.0 - -# Pinch 相關變數 -is_pinching = False -was_pinching = False # 邊緣觸發 -PINCH_THRESHOLD = 40 # 距離臨界值 - -# 按鈕設定 -button_rect = pygame.Rect(300, 250, 200, 100) -button_colors = [(3, 11, 255), (234,255,3), - (100, 220, 100), (220, 180, 50)] -current_color_index = 0 - -# 點擊計數器 -click_count = 0 - -# --- 限時機制變數 --- -START_TIME = pygame.time.get_ticks() -GAME_DURATION = 30 # 遊戲限制時間(秒) -game_state = "PLAYING" # 狀態機 - -# ============================================ -# 3. 主迴圈 -# ============================================ -running = True -while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - # --- 讀取攝影機與手勢偵測 --- - success, img = cap.read() - is_pinching = False # 每幀重設狀態,防止手離屏時卡死 - - if success: - img = cv2.flip(img, 1) # 鏡像翻轉 - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - results = detector.detect(mp_image) - - # === 【TODO 2:挑戰關卡三之手部安全偵測】 === - # 1. 判斷手部是否在畫面中 (results.hand_landmarks 是否有資料) - # 2. 如果在畫面中,取得食指 (8)、大拇指 (4)、中指 (12) 的像素座標 - # 3. 如果手不在畫面中,將 is_pinching 設為 False - # - hand_in_frame = False - if results.hand_landmarks: - hand_in_frame = True - for hand_lms in results.hand_landmarks: - # 食指 (8) - lm8 = hand_lms[8] - finger_x = int(lm8.x * WIDTH) - finger_y = int(lm8.y * HEIGHT) - - # 大拇指 (4) - lm4 = hand_lms[4] - thumb_x = int(lm4.x * WIDTH) - thumb_y = int(lm4.y * HEIGHT) - - # 中指 (12) - lm12 = hand_lms[12] - middle_x = int(lm12.x * WIDTH) - middle_y = int(lm12.y * HEIGHT) - - - # === 【TODO 3:挑戰關卡一之三指捏合判定】 === - # 提示:計算食指與大拇指距離,以及中指與大拇指距離 - # 兩組距離皆小於 PINCH_THRESHOLD 時,is_pinching 才為 True - dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y) - dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y) - is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD) - - - - # --- 狀態機邏輯處理 --- - if game_state == "PLAYING": - # 1. 計算剩餘時間 - current_ticks = pygame.time.get_ticks() - elapsed_seconds = (current_ticks - START_TIME) / 1000 - remaining_time = max(0, GAME_DURATION - elapsed_seconds) - - # 2. 檢查時間是否到了 - if remaining_time <= 0: - game_state = "GAME_OVER" - - # 3. 邊緣觸發點擊判斷 - if is_pinching and not was_pinching: - finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) - if finger_rect.colliderect(button_rect): - click_count += 1 - current_color_index = (current_color_index + 1) % len(button_colors) - - # === 【TODO 4:挑戰關卡二之按鈕隨機移動】 === - # 提示:當點擊發生時,讓按鈕的 x 和 y 座標隨機分配在畫面內 - button_rect.x = random.randint(0, WIDTH - button_rect.width) - button_rect.y = random.randint(0, HEIGHT - button_rect.height) - - - print(f" 魔法三指點擊!第 {click_count} 次") - - # 更新上一幀狀態 - was_pinching = is_pinching - - # --- 繪製畫面 --- - screen.fill((20, 20, 35)) - font = pygame.font.SysFont(None, 32) - large_font = pygame.font.SysFont(None, 64) - - if game_state == "PLAYING": - # 繪製按鈕 - btn_color = button_colors[current_color_index] - pygame.draw.rect(screen, btn_color, button_rect, border_radius=16) - - btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255)) - screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2, - button_rect.centery - btn_text.get_height() // 2)) - - # 顯示倒數計時 - time_text = font.render(f"Time Left: {remaining_time:.1f}s", True, (255, 220, 100)) - screen.blit(time_text, (WIDTH - time_text.get_width() - 20, 10)) - - # 畫手指游標 - cursor_color = (50, 255, 50) if is_pinching else (255, 50, 50) - pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) # 食指 - pygame.draw.circle(screen, (255, 255, 255), (thumb_x, thumb_y), 12) # 大拇指 - pygame.draw.circle(screen, (100, 150, 255), (middle_x, middle_y), 12) # 中指 - - # === 【TODO 5:挑戰關卡三之畫面提示】 === - # 提示:當手不在畫面中時,在畫面上方顯示警告文字提醒學生將手部放回 - if not hand_in_frame: - warning_text = font.render("OUT OF FRAME - PLEASE PUT HANDS BACK", True, (255, 100, 100)) - screen.blit(warning_text, (WIDTH // 2 - warning_text.get_width() // 2, 100)) - - - - # 顯示調試數值 - small_font = pygame.font.SysFont(None, 24) - status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150)) - screen.blit(status, (10, 10)) - - dist_text = small_font.render(f"Dist Thumb-Index: {dist_thumb_index:.0f} px", True, (150, 150, 200)) - screen.blit(dist_text, (10, 35)) - - dist_text2 = small_font.render(f"Dist Thumb-Middle: {dist_thumb_middle:.0f} px", True, (150, 150, 200)) - screen.blit(dist_text2, (10, 60)) - - elif game_state == "GAME_OVER": - # 繪製遊戲結束畫面 - over_text = large_font.render("GAME OVER", True, (255, 50, 50)) - score_text = font.render(f"Total Clicks: {click_count} times", True, (255, 255, 255)) - - screen.blit(over_text, (WIDTH // 2 - over_text.get_width() // 2, HEIGHT // 2 - 50)) - screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, HEIGHT // 2 + 20)) - - pygame.display.update() - clock.tick(30) - -cap.release() -pygame.quit() -sys.exit() +""" +Week 10 - 魔法手勢:Pinch 偵測與手勢按鈕 (三指捏合挑戰 Template 填空版) +================================================================== +本週挑戰目標: + 1. 實作「三指捏合 (Three-Finger Pinch)」: + 必須同時滿足「大拇指 (4) 與食指 (8) 的距離 < Threshold」 + 以及「大拇指 (4) 與中指 (12) 的距離 < Threshold」才判定為 Pinch + 2. 用三指碰觸 Pygame 畫面上的按鈕 + 捏合 = 魔法點擊 + 3. 使用邊緣觸發 (Edge Detection) 防止重複觸發 + 4. 新增 30 秒限時機制,時間到顯示最終得分 +""" +import pygame +import cv2 +import mediapipe as mp +import math +import sys +import os +import random + +from mediapipe.tasks import python +from mediapipe.tasks.python import vision + +# ============================================ +# 1. 初始化 +# ============================================ +pygame.init() +WIDTH, HEIGHT = 800, 600 +screen = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("Week 10 - 三指魔法手勢按鈕 (填空版) ") +clock = pygame.time.Clock() + +# 攝影機 +cap = cv2.VideoCapture(0) + +# 【TODO 1】初始化 MediaPipe 手勢偵測器 (Hand Landmarker) + +try: + base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hand_landmarker.task')) + options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) + detector = vision.HandLandmarker.create_from_options(options) +except Exception as e: + print(f"Error loading model: {e}") + pygame.quit() + sys.exit() + +# ============================================ +# 2. 遊戲變數 +# ============================================ +# 手指座標 +finger_x, finger_y = 400, 300 # 食指 +thumb_x, thumb_y = 400, 300 # 大拇指 +middle_x, middle_y = 400, 300 # 中指 + +# 距離變數 +dist_thumb_index = 0.0 +dist_thumb_middle = 0.0 + +# Pinch 相關變數 +is_pinching = False +was_pinching = False # 邊緣觸發 +PINCH_THRESHOLD = 40 # 距離臨界值 + +# 按鈕設定 +button_rect = pygame.Rect(300, 250, 200, 100) +button_colors = [(3, 11, 255), (234,255,3), + (100, 220, 100), (220, 180, 50)] +current_color_index = 0 + +# 點擊計數器 +click_count = 0 + +# --- 限時機制變數 --- +START_TIME = pygame.time.get_ticks() +GAME_DURATION = 30 # 遊戲限制時間(秒) +game_state = "PLAYING" # 狀態機 + +# ============================================ +# 3. 主迴圈 +# ============================================ +running = True +while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + + # --- 讀取攝影機與手勢偵測 --- + success, img = cap.read() + is_pinching = False # 每幀重設狀態,防止手離屏時卡死 + + if success: + img = cv2.flip(img, 1) # 鏡像翻轉 + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + # === 【TODO 2:挑戰關卡三之手部安全偵測】 === + # 1. 判斷手部是否在畫面中 (results.hand_landmarks 是否有資料) + # 2. 如果在畫面中,取得食指 (8)、大拇指 (4)、中指 (12) 的像素座標 + # 3. 如果手不在畫面中,將 is_pinching 設為 False + # + hand_in_frame = False + if results.hand_landmarks: + hand_in_frame = True + for hand_lms in results.hand_landmarks: + # 食指 (8) + lm8 = hand_lms[8] + finger_x = int(lm8.x * WIDTH) + finger_y = int(lm8.y * HEIGHT) + + # 大拇指 (4) + lm4 = hand_lms[4] + thumb_x = int(lm4.x * WIDTH) + thumb_y = int(lm4.y * HEIGHT) + + # 中指 (12) + lm12 = hand_lms[12] + middle_x = int(lm12.x * WIDTH) + middle_y = int(lm12.y * HEIGHT) + + + # === 【TODO 3:挑戰關卡一之三指捏合判定】 === + # 提示:計算食指與大拇指距離,以及中指與大拇指距離 + # 兩組距離皆小於 PINCH_THRESHOLD 時,is_pinching 才為 True + dist_thumb_index = math.hypot(finger_x - thumb_x, finger_y - thumb_y) + dist_thumb_middle = math.hypot(middle_x - thumb_x, middle_y - thumb_y) + is_pinching = (dist_thumb_index < PINCH_THRESHOLD) and (dist_thumb_middle < PINCH_THRESHOLD) + + + + # --- 狀態機邏輯處理 --- + if game_state == "PLAYING": + # 1. 計算剩餘時間 + current_ticks = pygame.time.get_ticks() + elapsed_seconds = (current_ticks - START_TIME) / 1000 + remaining_time = max(0, GAME_DURATION - elapsed_seconds) + + # 2. 檢查時間是否到了 + if remaining_time <= 0: + game_state = "GAME_OVER" + + # 3. 邊緣觸發點擊判斷 + if is_pinching and not was_pinching: + finger_rect = pygame.Rect(finger_x - 5, finger_y - 5, 10, 10) + if finger_rect.colliderect(button_rect): + click_count += 1 + current_color_index = (current_color_index + 1) % len(button_colors) + + # === 【TODO 4:挑戰關卡二之按鈕隨機移動】 === + # 提示:當點擊發生時,讓按鈕的 x 和 y 座標隨機分配在畫面內 + button_rect.x = random.randint(0, WIDTH - button_rect.width) + button_rect.y = random.randint(0, HEIGHT - button_rect.height) + + + print(f" 魔法三指點擊!第 {click_count} 次") + + # 更新上一幀狀態 + was_pinching = is_pinching + + # --- 繪製畫面 --- + screen.fill((20, 20, 35)) + font = pygame.font.SysFont(None, 32) + large_font = pygame.font.SysFont(None, 64) + + if game_state == "PLAYING": + # 繪製按鈕 + btn_color = button_colors[current_color_index] + pygame.draw.rect(screen, btn_color, button_rect, border_radius=16) + + btn_text = font.render(f"Clicks: {click_count}", True, (255, 255, 255)) + screen.blit(btn_text, (button_rect.centerx - btn_text.get_width() // 2, + button_rect.centery - btn_text.get_height() // 2)) + + # 顯示倒數計時 + time_text = font.render(f"Time Left: {remaining_time:.1f}s", True, (255, 220, 100)) + screen.blit(time_text, (WIDTH - time_text.get_width() - 20, 10)) + + # 畫手指游標 + cursor_color = (50, 255, 50) if is_pinching else (255, 50, 50) + pygame.draw.circle(screen, cursor_color, (finger_x, finger_y), 12) # 食指 + pygame.draw.circle(screen, (255, 255, 255), (thumb_x, thumb_y), 12) # 大拇指 + pygame.draw.circle(screen, (100, 150, 255), (middle_x, middle_y), 12) # 中指 + + # === 【TODO 5:挑戰關卡三之畫面提示】 === + # 提示:當手不在畫面中時,在畫面上方顯示警告文字提醒學生將手部放回 + if not hand_in_frame: + warning_text = font.render("OUT OF FRAME - PLEASE PUT HANDS BACK", True, (255, 100, 100)) + screen.blit(warning_text, (WIDTH // 2 - warning_text.get_width() // 2, 100)) + + + + # 顯示調試數值 + small_font = pygame.font.SysFont(None, 24) + status = small_font.render(f"Finger: ({finger_x}, {finger_y})", True, (150, 150, 150)) + screen.blit(status, (10, 10)) + + dist_text = small_font.render(f"Dist Thumb-Index: {dist_thumb_index:.0f} px", True, (150, 150, 200)) + screen.blit(dist_text, (10, 35)) + + dist_text2 = small_font.render(f"Dist Thumb-Middle: {dist_thumb_middle:.0f} px", True, (150, 150, 200)) + screen.blit(dist_text2, (10, 60)) + + elif game_state == "GAME_OVER": + # 繪製遊戲結束畫面 + over_text = large_font.render("GAME OVER", True, (255, 50, 50)) + score_text = font.render(f"Total Clicks: {click_count} times", True, (255, 255, 255)) + + screen.blit(over_text, (WIDTH // 2 - over_text.get_width() // 2, HEIGHT // 2 - 50)) + screen.blit(score_text, (WIDTH // 2 - score_text.get_width() // 2, HEIGHT // 2 + 20)) + + pygame.display.update() + clock.tick(30) + +cap.release() +pygame.quit() +sys.exit() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..fcb42c4bf1d6f4dbdfca16288a1aa1e349c298ef --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\270\211\346\214\207\346\215\217\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d5_pinch_challenge_solution.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd5_pinch_challenge_solution.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\272\272\350\207\211\350\276\250\350\255\230/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\272\272\350\207\211\350\276\250\350\255\230/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\272\272\350\207\211\350\276\250\350\255\230/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..72b1075a69ea1ceeabd526fd9dbbcf8a32934405 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\344\272\272\350\207\211\350\276\250\350\255\230/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d3_2_face_detection2.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd3_2_face_detection2.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..6d9f8e43cbdcde83518590b0a762af898547ad23 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step0_eyeguard_template.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step0_eyeguard_template.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\205\251\347\234\274/step0_eyeguard_template.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" similarity index 98% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\205\251\347\234\274/step0_eyeguard_template.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" index 4cd9603129e33fc7e6e87f3034ef4ac68225cca5..09cb1a46ff37bc74aa2f282263d2934f1429160f 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\205\251\347\234\274/step0_eyeguard_template.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\205\251\347\234\274\350\267\235\351\233\242\345\201\265\346\270\254/step0_eyeguard_template.py" @@ -45,7 +45,7 @@ cap = cv2.VideoCapture(0) # 沒有攝影機就改成影片:cap = cv2.VideoCapture("backup_face_video.mp4") # 【TODO 1】初始化 MediaPipe 臉部偵測 (Face Landmarker) -base_options = python.BaseOptions(model_asset_path=os.path.join('face_landmarker.task')) +base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task')) options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) detector = vision.FaceLandmarker.create_from_options(options) diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/coffe_shop_sales_draw/coffee_sales_data.csv" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/coffee_sales_data.csv" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/coffe_shop_sales_draw/coffee_sales_data.csv" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/coffee_sales_data.csv" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/coffe_shop_sales_draw/d1_4_visualization_coffee.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/d1_4_visualization_coffee.py" similarity index 84% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/coffe_shop_sales_draw/d1_4_visualization_coffee.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/d1_4_visualization_coffee.py" index bc551b681de42163edc3afcd7b82e7695f7a8941..e3a1c101add52218cf00ded59fbde7a3616fba56 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/coffe_shop_sales_draw/d1_4_visualization_coffee.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/d1_4_visualization_coffee.py" @@ -1,3 +1,5 @@ +import matplotlib +matplotlib.use('Agg') # Telearn hub headless # d1_4_visualization_coffee.py # Day 1: 資料科學與數據視覺化 - 咖啡廳營運數據進階分析(學員填空) # cd examples/Day1/student && python3 d1_4_visualization_coffee.py @@ -8,6 +10,34 @@ import pandas as pd import matplotlib.pyplot as plt + +def _telearn_plt_show(*_a, **_k): + """Hub-friendly plt.show: save current figure and display via OpenCV.""" + import os + import matplotlib.pyplot as plt + try: + import cv2 + except ImportError: + plt.savefig("_telearn_last_plot.png", dpi=150, bbox_inches="tight") + print("[Telearn] 已儲存 _telearn_last_plot.png(無 cv2 可顯示)") + return + path = os.path.join(os.getcwd(), "_telearn_last_plot.png") + plt.savefig(path, dpi=150, bbox_inches="tight") + img = cv2.imread(path) + if img is not None: + # fit to screen-ish width + h, w = img.shape[:2] + max_w = 960 + if w > max_w: + scale = max_w / float(w) + img = cv2.resize(img, (int(w * scale), int(h * scale))) + cv2.imshow("Chart (press any key)", img) + print("[Telearn] 圖表已顯示,按任意鍵關閉…") + cv2.waitKey(0) + cv2.destroyAllWindows() + else: + print(f"[Telearn] 圖表已儲存: {path}") + def load_csv(filename="coffee_sales_data.csv"): """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" # === 【學員填空練習點 1:CSV 檔案安全讀取】 === @@ -120,6 +150,6 @@ plt.tight_layout() # 自動調整間距,避免文字重疊 output_image = "coffee_sales_analysis.png" # 請在下方完成圖表存檔與顯示: plt.savefig('coffee_sales_analysis.png', dpi=300) -plt.show() +_telearn_plt_show() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..1f104b92c6250f22879c19e0c0198fe1a72e3ba3 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\222\226\345\225\241\345\272\227\347\207\237\346\224\266\347\271\252\345\234\226/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_4_visualization_coffee.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_4_visualization_coffee.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..4f7550b344cc508f552737ddcfc49fc178221251 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step3_mouth_ratio.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step3_mouth_ratio.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/open_mouth_close/step2_facemesh_complete.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/step2_facemesh_complete.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/open_mouth_close/step2_facemesh_complete.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/step2_facemesh_complete.py" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/open_mouth_close/step3_mouth_ratio.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/step3_mouth_ratio.py" similarity index 96% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/open_mouth_close/step3_mouth_ratio.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/step3_mouth_ratio.py" index a1a218a6777e5772816ac5202d52c0f03b3c8bfe..cdc2a3518924aa8b51d5886f2cd4e4b613e950d3 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/open_mouth_close/step3_mouth_ratio.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\230\264\345\267\264\351\226\213\345\220\210\345\201\265\346\270\254/step3_mouth_ratio.py" @@ -1,113 +1,113 @@ -""" -Week 11 - Step 2: Mouth Opening Ratio Detection (Template 填空版) -============================================================= -本步驟目標: - 1. 找出特定的臉部特徵點:眼角(33, 263)與嘴唇(13, 14) - 2. 計算兩眼距離,當作臉寬基準比例尺,防止人靠近或遠離鏡頭造成的誤判 (歸一化) - 3. 計算嘴巴張開比例:嘴巴距離 / 兩眼距離 - 4. 在 OpenCV 畫面上即時顯示比例與嘴巴開合狀態 (OPEN / CLOSED) -""" -import cv2 -import mediapipe as mp -import math -import sys -import os - -# 1. 初始化攝影機 -cap = cv2.VideoCapture(0) -if not cap.isOpened(): - print("Error: Could not open camera.") - sys.exit() - -# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) -model_path = os.path.join('face_landmarker.task') -if not os.path.exists(model_path): - print(f"Error: Model file not found at {model_path}") - sys.exit() - -try: - from mediapipe.tasks import python - from mediapipe.tasks.python import vision - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) - detector = vision.FaceLandmarker.create_from_options(options) - print("Face Landmarker initialized successfully.") -except Exception as e: - print(f"Error: Failed to initialize detector: {e}") - sys.exit() - -# 特徵點索引定義 -LEFT_EYE_OUTER = 33 -RIGHT_EYE_OUTER = 263 -LIP_TOP = 13 -LIP_BOTTOM = 14 - -# 嘴巴張開的比例閾值 (超過代表張開) -THRESHOLD = 0.35 - -print("Press 'q' in the window to exit.") - -# 3. 主迴圈 -while True: - success, img = cap.read() - if not success: - break - - img = cv2.flip(img, 1) - img_h, img_w, _ = img.shape - - # 影像處理 - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - results = detector.detect(mp_image) - - if results.face_landmarks: - face_lms = results.face_landmarks[0] - - # 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中 - pts = {} - for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]: - pt = face_lms[idx] - pts[idx] = (int(pt.x * img_w), int(pt.y * img_h)) - - - # 【TODO 2】計算兩眼外角距離 (做為基準比例尺) - p_lx, p_ly = pts[LEFT_EYE_OUTER] - p_rx, p_ry = pts[RIGHT_EYE_OUTER] - base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly) - - - if base_dist > 0: - # 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」 - p_top_x, p_top_y = pts[LIP_TOP] - p_bot_x, p_bot_y = pts[LIP_BOTTOM] - mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y) - mouth_ratio = mouth_dist / base_dist - - - # 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線 - # 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色) - is_open = mouth_ratio > THRESHOLD - cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線 - cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線 - - - # 畫出特徵點小黃圓圈 - for idx, pos in pts.items(): - cv2.circle(img, pos, 5, (0, 255, 255), -1) - - # 顯示比例與開合狀態文字 - cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) - status_text = "OPEN" if is_open else "CLOSED" - status_color = (0, 255, 0) if is_open else (0, 0, 255) - cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2) - else: - cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) - - cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img) - - if cv2.waitKey(1) & 0xFF == ord('q'): - break - -cap.release() -cv2.destroyAllWindows() +""" +Week 11 - Step 2: Mouth Opening Ratio Detection (Template 填空版) +============================================================= +本步驟目標: + 1. 找出特定的臉部特徵點:眼角(33, 263)與嘴唇(13, 14) + 2. 計算兩眼距離,當作臉寬基準比例尺,防止人靠近或遠離鏡頭造成的誤判 (歸一化) + 3. 計算嘴巴張開比例:嘴巴距離 / 兩眼距離 + 4. 在 OpenCV 畫面上即時顯示比例與嘴巴開合狀態 (OPEN / CLOSED) +""" +import cv2 +import mediapipe as mp +import math +import sys +import os + +# 1. 初始化攝影機 +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("Error: Could not open camera.") + sys.exit() + +# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) +model_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task') +if not os.path.exists(model_path): + print(f"Error: Model file not found at {model_path}") + sys.exit() + +try: + from mediapipe.tasks import python + from mediapipe.tasks.python import vision + base_options = python.BaseOptions(model_asset_path=model_path) + options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) + detector = vision.FaceLandmarker.create_from_options(options) + print("Face Landmarker initialized successfully.") +except Exception as e: + print(f"Error: Failed to initialize detector: {e}") + sys.exit() + +# 特徵點索引定義 +LEFT_EYE_OUTER = 33 +RIGHT_EYE_OUTER = 263 +LIP_TOP = 13 +LIP_BOTTOM = 14 + +# 嘴巴張開的比例閾值 (超過代表張開) +THRESHOLD = 0.35 + +print("Press 'q' in the window to exit.") + +# 3. 主迴圈 +while True: + success, img = cap.read() + if not success: + break + + img = cv2.flip(img, 1) + img_h, img_w, _ = img.shape + + # 影像處理 + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + results = detector.detect(mp_image) + + if results.face_landmarks: + face_lms = results.face_landmarks[0] + + # 【TODO 1】取得這 4 個特徵點在畫面中的 (x, y) 像素座標,並存在 pts 字典中 + pts = {} + for idx in [LEFT_EYE_OUTER, RIGHT_EYE_OUTER, LIP_TOP, LIP_BOTTOM]: + pt = face_lms[idx] + pts[idx] = (int(pt.x * img_w), int(pt.y * img_h)) + + + # 【TODO 2】計算兩眼外角距離 (做為基準比例尺) + p_lx, p_ly = pts[LEFT_EYE_OUTER] + p_rx, p_ry = pts[RIGHT_EYE_OUTER] + base_dist = math.hypot(p_rx - p_lx, p_ry - p_ly) + + + if base_dist > 0: + # 【TODO 3】計算嘴巴張開的實際像素距離,並除以眼距算出「嘴巴張開比例 (mouth_ratio)」 + p_top_x, p_top_y = pts[LIP_TOP] + p_bot_x, p_bot_y = pts[LIP_BOTTOM] + mouth_dist = math.hypot(p_bot_x - p_top_x, p_bot_y - p_top_y) + mouth_ratio = mouth_dist / base_dist + + + # 【TODO 4】判斷嘴巴狀態是否大於 THRESHOLD,並在畫面上繪製量測輔助線 + # 提示:用 cv2.line 畫出眼部基準線 (藍色) 和嘴巴測量線 (橘色) + is_open = mouth_ratio > THRESHOLD + cv2.line(img, (p_lx, p_ly), (p_rx, p_ry), (255, 0, 0), 2) # 藍色眼線 + cv2.line(img, (p_top_x, p_top_y), (p_bot_x, p_bot_y), (0, 165, 255), 2) # 橘色嘴線 + + + # 畫出特徵點小黃圓圈 + for idx, pos in pts.items(): + cv2.circle(img, pos, 5, (0, 255, 255), -1) + + # 顯示比例與開合狀態文字 + cv2.putText(img, f"Mouth Ratio: {mouth_ratio:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2) + status_text = "OPEN" if is_open else "CLOSED" + status_color = (0, 255, 0) if is_open else (0, 0, 255) + cv2.putText(img, f"State: {status_text}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2) + else: + cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) + + cv2.imshow("Mouth Ratio Detector - Press 'q' to quit", img) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + +cap.release() +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226and\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" similarity index 77% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226and\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" index 107064dc32369d7a9e0d43b084c16090ef21525a..c618a6bb584b66f8ac7222a6c29abf84e195ae51 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226and\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/d1_3_visualization.py" @@ -1,92 +1,122 @@ -# d1_3_visualization.py -# Day 1: 資料科學與數據視覺化 - Matplotlib 繪圖基礎(學員填空) -# cd examples/Day1/student && python3 d1_3_visualization.py -# 參考答案:../Teacher/d1_3_visualization.py - -import os -import pandas as pd -import matplotlib.pyplot as plt - - -def load_csv(filename="taipei_weather_2026.csv"): - """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" - # === 【學員填空練習點 1:CSV 檔案安全讀取】 === - path = os.path.join(os.path.dirname(__file__), filename) - if os.path.exists(path): - - return pd.read_csv(path) - else : - print("SCV檔案不存在") - - - - -df = load_csv() - -# 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊 -plt.rcParams['font.sans-serif'] = [ - 'PingFang TC', 'Heiti TC', 'Arial Unicode MS', - 'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei', -] -plt.rcParams['axes.unicode_minus'] = False - -# === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 === -# TODO: 使用 plt.subplots 建立一個 2 列 1 行 (2x1) 的子圖畫布,尺寸大小為寬 10 英吋、高 8 英吋 -# 並指派給 fig 與 (ax1, ax2) -fig, (ax1, ax2) = plt.subplots(2, 1, figsize =(10, 8)) - -# 1. 繪製折線圖 (Line Plot) - 每日溫度 -# === 【學員填空練習點 3:繪製每日溫度折線圖】 === -# TODO: 1. 在 ax1 畫布上以 ax1.plot() 繪製折線圖,X軸傳入 df['Date'],Y軸傳入 df['Temperature'] -# 設定參數:帶有圓點 marker='o',顏色 color='crimson',圖例 label='溫度 (°C)',線寬 linewidth=2 -# 2. 使用 ax1.set_title 設定標題 "台北觀測站每日溫度趨勢圖" -# 3. 設定 ax1.set_xlabel("日期") 與 ax1.set_ylabel("數值") -# 4. 設定網格 ax1.grid(True, linestyle='--', alpha=0.5) -# 5. 呼叫 ax1.legend() 顯示圖例 -# 6. 設定 X軸刻度旋轉 ax1.tick_params(axis='x', rotation=45) -# -# 請在下方完成 ax1 的溫度折線圖繪製: -ax1.plot(df['Date'], df['Temperature'], marker='o', color='crimson', label='溫度 (C)', linewidth=2) -ax1.plot(df['Date'], df['Humidity'], marker='s', color='orange', label='濕度 (%)', linewidth=2) -ax1.set_title("台北觀測站每日溫度趨勢圖", fontsize=14, fontweight = 'bold') -ax1.set_xlabel("日期") -ax1.set_ylabel("數值") -ax1.grid(True, linestyle='--', alpha=0.5) -ax1.legend() -ax1.tick_params(axis='x', rotation = 45) - - - - - -# 2. 繪製條形圖 (Bar Plot) - 每日降雨量 -# === 【學員填空練習點 4:繪製每日降雨量長條圖】 === -# TODO: 1. 在 ax2 畫布上以 ax2.bar() 繪製長條圖,X軸傳入 df['Date'],Y軸傳入 df['Rainfall'] -# 設定參數:顏色 color='skyblue',外框顏色 edgecolor='royalblue',圖例 label='降雨量 (mm)' -# 2. 使用 ax2.set_title 設定標題 "台北觀測站每日降雨量分佈" -# 3. 設定 ax2.set_xlabel("日期") 與 ax2.set_ylabel("降雨量 (mm)") -# 4. 設定 Y 軸網格 ax2.grid(True, axis='y', linestyle='--', alpha=0.5) -# 5. 呼叫 ax2.legend() 顯示圖例 -# 6. 設定 X軸刻度旋轉 ax2.tick_params(axis='x', rotation=45) -# -# 請在下方完成 ax2 的降雨量長條圖繪製: -ax2.bar(df['Date'], df['Rainfall'], color='skyblue',edgecolor='royalblue', label='降雨量 (mm)') -ax2.set_title("台北觀測站每日降雨量分佈", fontsize=14, fontweight = 'bold') -ax2.set_xlabel("日期") -ax2.set_ylabel("降雨量 (mm)") -ax2.grid(True, axis = 'y', linestyle='--', alpha=0.5) -ax2.legend() -ax2.tick_params(axis='x', rotation=45) - - - -plt.tight_layout() # 自動調整間距,避免文字重疊 - -# === 【學員填空練習點 5:圖表自動儲存與顯示】 === -# TODO: 1. 呼叫 plt.savefig 將畫布儲存為檔名 "taipei_weather_2026.png",並設定高畫質 dpi=300 -# (⚠️ 注意:savefig 必須在 show() 之前呼叫) -# 2. 呼叫 plt.show() 將圖表視窗顯示在畫面上 -# -# 請在下方完成圖表存檔與顯示: -plt.savefig("taipei_weather_2026.png", dpi=300) -plt.show() \ No newline at end of file +import matplotlib +matplotlib.use('Agg') # Telearn hub headless +# d1_3_visualization.py +# Day 1: 資料科學與數據視覺化 - Matplotlib 繪圖基礎(學員填空) +# cd examples/Day1/student && python3 d1_3_visualization.py +# 參考答案:../Teacher/d1_3_visualization.py + +import os +import pandas as pd +import matplotlib.pyplot as plt + + + +def _telearn_plt_show(*_a, **_k): + """Hub-friendly plt.show: save current figure and display via OpenCV.""" + import os + import matplotlib.pyplot as plt + try: + import cv2 + except ImportError: + plt.savefig("_telearn_last_plot.png", dpi=150, bbox_inches="tight") + print("[Telearn] 已儲存 _telearn_last_plot.png(無 cv2 可顯示)") + return + path = os.path.join(os.getcwd(), "_telearn_last_plot.png") + plt.savefig(path, dpi=150, bbox_inches="tight") + img = cv2.imread(path) + if img is not None: + # fit to screen-ish width + h, w = img.shape[:2] + max_w = 960 + if w > max_w: + scale = max_w / float(w) + img = cv2.resize(img, (int(w * scale), int(h * scale))) + cv2.imshow("Chart (press any key)", img) + print("[Telearn] 圖表已顯示,按任意鍵關閉…") + cv2.waitKey(0) + cv2.destroyAllWindows() + else: + print(f"[Telearn] 圖表已儲存: {path}") + +def load_csv(filename="taipei_weather_2026.csv"): + """安全載入 CSV 的輔助函式,用以防範在不同執行路徑下相對路徑讀檔失敗的問題""" + # === 【學員填空練習點 1:CSV 檔案安全讀取】 === + path = os.path.join(os.path.dirname(__file__), filename) + if os.path.exists(path): + + return pd.read_csv(path) + else : + print("SCV檔案不存在") + + + + +df = load_csv() + +# 設定中文字型與負號顯示防禦,避免圖表出現亂碼方塊 +plt.rcParams['font.sans-serif'] = [ + 'PingFang TC', 'Heiti TC', 'Arial Unicode MS', + 'Microsoft JhengHei', 'Noto Sans CJK TC', 'SimHei', +] +plt.rcParams['axes.unicode_minus'] = False + +# === 【學員填空練習點 2:建立子圖畫布 (Subplots)】 === +# TODO: 使用 plt.subplots 建立一個 2 列 1 行 (2x1) 的子圖畫布,尺寸大小為寬 10 英吋、高 8 英吋 +# 並指派給 fig 與 (ax1, ax2) +fig, (ax1, ax2) = plt.subplots(2, 1, figsize =(10, 8)) + +# 1. 繪製折線圖 (Line Plot) - 每日溫度 +# === 【學員填空練習點 3:繪製每日溫度折線圖】 === +# TODO: 1. 在 ax1 畫布上以 ax1.plot() 繪製折線圖,X軸傳入 df['Date'],Y軸傳入 df['Temperature'] +# 設定參數:帶有圓點 marker='o',顏色 color='crimson',圖例 label='溫度 (°C)',線寬 linewidth=2 +# 2. 使用 ax1.set_title 設定標題 "台北觀測站每日溫度趨勢圖" +# 3. 設定 ax1.set_xlabel("日期") 與 ax1.set_ylabel("數值") +# 4. 設定網格 ax1.grid(True, linestyle='--', alpha=0.5) +# 5. 呼叫 ax1.legend() 顯示圖例 +# 6. 設定 X軸刻度旋轉 ax1.tick_params(axis='x', rotation=45) +# +# 請在下方完成 ax1 的溫度折線圖繪製: +ax1.plot(df['Date'], df['Temperature'], marker='o', color='crimson', label='溫度 (C)', linewidth=2) +ax1.plot(df['Date'], df['Humidity'], marker='s', color='orange', label='濕度 (%)', linewidth=2) +ax1.set_title("台北觀測站每日溫度趨勢圖", fontsize=14, fontweight = 'bold') +ax1.set_xlabel("日期") +ax1.set_ylabel("數值") +ax1.grid(True, linestyle='--', alpha=0.5) +ax1.legend() +ax1.tick_params(axis='x', rotation = 45) + + + + + +# 2. 繪製條形圖 (Bar Plot) - 每日降雨量 +# === 【學員填空練習點 4:繪製每日降雨量長條圖】 === +# TODO: 1. 在 ax2 畫布上以 ax2.bar() 繪製長條圖,X軸傳入 df['Date'],Y軸傳入 df['Rainfall'] +# 設定參數:顏色 color='skyblue',外框顏色 edgecolor='royalblue',圖例 label='降雨量 (mm)' +# 2. 使用 ax2.set_title 設定標題 "台北觀測站每日降雨量分佈" +# 3. 設定 ax2.set_xlabel("日期") 與 ax2.set_ylabel("降雨量 (mm)") +# 4. 設定 Y 軸網格 ax2.grid(True, axis='y', linestyle='--', alpha=0.5) +# 5. 呼叫 ax2.legend() 顯示圖例 +# 6. 設定 X軸刻度旋轉 ax2.tick_params(axis='x', rotation=45) +# +# 請在下方完成 ax2 的降雨量長條圖繪製: +ax2.bar(df['Date'], df['Rainfall'], color='skyblue',edgecolor='royalblue', label='降雨量 (mm)') +ax2.set_title("台北觀測站每日降雨量分佈", fontsize=14, fontweight = 'bold') +ax2.set_xlabel("日期") +ax2.set_ylabel("降雨量 (mm)") +ax2.grid(True, axis = 'y', linestyle='--', alpha=0.5) +ax2.legend() +ax2.tick_params(axis='x', rotation=45) + + + +plt.tight_layout() # 自動調整間距,避免文字重疊 + +# === 【學員填空練習點 5:圖表自動儲存與顯示】 === +# TODO: 1. 呼叫 plt.savefig 將畫布儲存為檔名 "taipei_weather_2026.png",並設定高畫質 dpi=300 +# (⚠️ 注意:savefig 必須在 show() 之前呼叫) +# 2. 呼叫 _telearn_plt_show() 將圖表視窗顯示在畫面上 +# +# 請在下方完成圖表存檔與顯示: +plt.savefig("taipei_weather_2026.png", dpi=300) +_telearn_plt_show() \ No newline at end of file diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a4fd3671a6f3d6b1d49df8a77568c765d630e26d --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_3_visualization.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_3_visualization.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226and\351\225\267\346\242\235\345\234\226/taipei_weather_2026.csv" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/taipei_weather_2026.csv" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226and\351\225\267\346\242\235\345\234\226/taipei_weather_2026.csv" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\244\251\346\260\243\350\263\207\346\226\231_\347\233\264\347\267\232\345\234\226\350\210\207\351\225\267\346\242\235\345\234\226/taipei_weather_2026.csv" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\275\261\345\203\217\350\231\225\347\220\206/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\275\261\345\203\217\350\231\225\347\220\206/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\275\261\345\203\217\350\231\225\347\220\206/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" index 24a49ad0190737d2ec76670520c16d138fdd435b..8216ea421ff98c1511c930e3b4ccc11fd7c6b1fb 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\275\261\345\203\217\350\231\225\347\220\206/d3_1_opencv_basic.py" @@ -1,69 +1,109 @@ -# d3_1_opencv_basic.py -# Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版) -# 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。 - -import cv2 -import numpy as np - -# 1. 影像讀取 (Read) -img = cv2.imread("gay.jpg") -if img is None: - print("[錯誤] 無法讀取 jpg") - exit(1) - -# 預設灰階影像為原始彩色影像,避免未解開註釋時程式出錯 -gray = img - -# === 【重要觀念 TODO 1:灰階轉換 (Grayscale Conversion)】 === -# 觀念重點:大多數電腦視覺演算法在處理單通道(黑白)影像時速度更快。 -# 將 BGR 彩色圖像轉換為單通道灰階圖像,可以減少 2/3 的資料量,且能過濾掉無用的顏色雜訊。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察變為灰階後的影像。 -gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - -# 預設模糊影像為灰階影像,避免未解開註釋時程式出錯 -blurred = gray - -# === 【重要觀念 TODO 2:高斯模糊 (Gaussian Blur)】 === -# 觀念重點:高斯模糊是透過像素加權平均來平滑影像,主要目的是「消除噪點和細微反光」。 -# 這可以避免後續邊緣偵測時把細微的噪點誤判為輪廓,讓偵測出來的邊緣更加平滑。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察影像邊緣的柔焦效果。 -blurred = cv2.GaussianBlur(gray, (5, 5), 0) - -# 預設邊緣影像為全黑畫布,避免未解開註釋時程式出錯 -edges = np.zeros(img.shape[:2], dtype=np.uint8) - -# === 【重要觀念 TODO 3:Canny 邊緣偵測 (Canny Edge Detection)】 === -# 觀念重點:Canny 演算法會找出影像中明暗變化劇烈的分界線。它使用雙閥值法: -# 高於高閥值(150)的 100% 判定為邊緣;介於高低閥值(50~150)之間的若連接強邊緣則保留;其餘捨棄。 -# 請解開下方註釋 (刪除行首的 #) 並執行,觀察提取出的白色線條輪廓。 -edges = cv2.Canny(blurred, 150, 150) - -# 預設無偵測到輪廓,且繪製圖像為原始圖像,避免未解開註釋時程式出錯 -contours = [] -contour_img = img.copy() - -# === 【重要觀念 TODO 4:尋找與繪製輪廓 (Find & Draw Contours)】 === -# 觀念重點:`findContours` 會在二值化(邊緣)影像中尋找相連的邊界,並建立幾何結構。 -# `drawContours` 則會將這些幾何結構繪製到指定的影像上(此處使用綠色框繪製於原始彩色影像)。 -# 請解開下方所有註釋 (刪除行首的 #) 並執行,觀察圖形外框被綠線框起來的結果。 -contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) -print(f"在影像中偵測到 {len(contours)} 個獨立輪廓特徵。") -contour_img = img.copy() -cv2.drawContours(contour_img, contours, -1, (0, 255, 0), 1) - -# === 【重要觀念 TODO 5:視窗顯示與釋放 (Window Display)】 === -# 觀念重點:使用 cv2.imshow 顯示各步驟處理結果,並用 waitKey(0) 讓程式停下等待按鍵, -# 最後使用 destroyAllWindows 釋放視窗資源。 -# 請解開下方所有註釋 (刪除行首的 #),以觀看影像處理的成果! -print("\n正在顯示影像處理結果(請點擊影像視窗並按下任意鍵關閉)...") -cv2.imshow("Grayscale", gray) -cv2.imshow("Original Shapes", img) -cv2.imshow("Canny Edges", edges) -cv2.imshow("Contours Output", contour_img) - -success_gray = cv2.imwrite("67_gray.jpg", gray) -success_edges = cv2.imwrite("67_edges.jpg", edges) -success_contour = cv2.imwrite("67_contours.jpg", contour_img) - -cv2.waitKey(0) -cv2.destroyAllWindows() +# d3_1_opencv_basic.py +# Day 3: 電腦視覺與影像辨識 - OpenCV 影像處理基礎 (學員練習版) +# 練習指引:依序找到程式碼中的 TODO 1 到 TODO 5,將其下方的程式碼「解除註釋(刪除行首的 # )」並執行,觀察影像處理管線中每一步的變化。 + +import cv2 +import numpy as np + +# 1. 影像讀取 (Read) + +def telearn_resolve_image(name): + """Resolve image path relative to script, cwd, or student image/ folders.""" + import os + from pathlib import Path + here = Path(__file__).resolve().parent + candidates = [ + Path(name), + here / name, + here / "image" / name, + here / "照片" / name, + here.parent / "image" / name, + here.parent / "照片" / name, + here.parent / "image_create" / name, + ] + # also try basename-only search one level + for base in [here, here.parent]: + for folder in ["image", "照片", "image_create", "处理过的照片"]: + p = base / folder + if p.is_dir(): + direct = p / name + candidates.append(direct) + # case-insensitive match + low = name.lower() + try: + for f in p.iterdir(): + if f.is_file() and f.name.lower() == low: + candidates.append(f) + except OSError: + pass + for c in candidates: + try: + if c.is_file(): + print(f"[Telearn] 讀取影像: {c}") + return str(c) + except OSError: + pass + print(f"[Telearn] 找不到影像 {name},搜尋過: {[str(c) for c in candidates[:8]]}…") + return name + +img = cv2.imread(telearn_resolve_image("gay.jpg")) +if img is None: + print("[錯誤] 無法讀取 jpg") + exit(1) + +# 預設灰階影像為原始彩色影像,避免未解開註釋時程式出錯 +gray = img + +# === 【重要觀念 TODO 1:灰階轉換 (Grayscale Conversion)】 === +# 觀念重點:大多數電腦視覺演算法在處理單通道(黑白)影像時速度更快。 +# 將 BGR 彩色圖像轉換為單通道灰階圖像,可以減少 2/3 的資料量,且能過濾掉無用的顏色雜訊。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察變為灰階後的影像。 +gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + +# 預設模糊影像為灰階影像,避免未解開註釋時程式出錯 +blurred = gray + +# === 【重要觀念 TODO 2:高斯模糊 (Gaussian Blur)】 === +# 觀念重點:高斯模糊是透過像素加權平均來平滑影像,主要目的是「消除噪點和細微反光」。 +# 這可以避免後續邊緣偵測時把細微的噪點誤判為輪廓,讓偵測出來的邊緣更加平滑。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察影像邊緣的柔焦效果。 +blurred = cv2.GaussianBlur(gray, (5, 5), 0) + +# 預設邊緣影像為全黑畫布,避免未解開註釋時程式出錯 +edges = np.zeros(img.shape[:2], dtype=np.uint8) + +# === 【重要觀念 TODO 3:Canny 邊緣偵測 (Canny Edge Detection)】 === +# 觀念重點:Canny 演算法會找出影像中明暗變化劇烈的分界線。它使用雙閥值法: +# 高於高閥值(150)的 100% 判定為邊緣;介於高低閥值(50~150)之間的若連接強邊緣則保留;其餘捨棄。 +# 請解開下方註釋 (刪除行首的 #) 並執行,觀察提取出的白色線條輪廓。 +edges = cv2.Canny(blurred, 150, 150) + +# 預設無偵測到輪廓,且繪製圖像為原始圖像,避免未解開註釋時程式出錯 +contours = [] +contour_img = img.copy() + +# === 【重要觀念 TODO 4:尋找與繪製輪廓 (Find & Draw Contours)】 === +# 觀念重點:`findContours` 會在二值化(邊緣)影像中尋找相連的邊界,並建立幾何結構。 +# `drawContours` 則會將這些幾何結構繪製到指定的影像上(此處使用綠色框繪製於原始彩色影像)。 +# 請解開下方所有註釋 (刪除行首的 #) 並執行,觀察圖形外框被綠線框起來的結果。 +contours, hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) +print(f"在影像中偵測到 {len(contours)} 個獨立輪廓特徵。") +contour_img = img.copy() +cv2.drawContours(contour_img, contours, -1, (0, 255, 0), 1) + +# === 【重要觀念 TODO 5:視窗顯示與釋放 (Window Display)】 === +# 觀念重點:使用 cv2.imshow 顯示各步驟處理結果,並用 waitKey(0) 讓程式停下等待按鍵, +# 最後使用 destroyAllWindows 釋放視窗資源。 +# 請解開下方所有註釋 (刪除行首的 #),以觀看影像處理的成果! +print("\n正在顯示影像處理結果(請點擊影像視窗並按下任意鍵關閉)...") +cv2.imshow("Grayscale", gray) +cv2.imshow("Original Shapes", img) +cv2.imshow("Canny Edges", edges) +cv2.imshow("Contours Output", contour_img) + +success_gray = cv2.imwrite("67_gray.jpg", gray) +success_edges = cv2.imwrite("67_edges.jpg", edges) +success_contour = cv2.imwrite("67_contours.jpg", contour_img) + +cv2.waitKey(0) +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..b223ab342e5fe7b1cc69da34d1bfed35520f5217 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\345\275\261\345\203\217\350\231\225\347\220\206/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d3_1_opencv_basic.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd3_1_opencv_basic.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/draw_game/draw_game_student.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" similarity index 97% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/draw_game/draw_game_student.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" index 6474b500b920eb18bd4536cf7d599abd8d404278..2bbf5b8e7535c2af452c5951d6e935a7693b3415 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/draw_game/draw_game_student.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/draw_game_student.py" @@ -1,3 +1,4 @@ +import os import cv2 import numpy as np import mediapipe as mp @@ -14,7 +15,7 @@ from mediapipe.tasks.python import vision # 2. HandLandmarkerOptions 用於設定手部偵測的細項設定,例如 num_hands=1 代表最多偵測一隻手。 # 3. 最後透過 vision.HandLandmarker.create_from_options 建立手部偵測器物件。 # 請解開下方三行程式碼的註釋: -base_options = python.BaseOptions(model_asset_path='hand_landmarker.task') +base_options = python.BaseOptions(model_asset_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'hand_landmarker.task')) options = vision.HandLandmarkerOptions(base_options=base_options, num_hands=1) detector = vision.HandLandmarker.create_from_options(options) diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..b4b6ab0db6832a42e6763bb130c2aa53c4003caa --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\346\211\213\346\214\207\347\225\253\347\225\253\351\201\212\346\210\262/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: draw_game_student.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'draw_game_student.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..1a227558e5f14ebae620e0c2d7ff2baf91c9dfdc --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220/main.py" @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +"""多練習選單入口(自動產生)""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +sys.path.insert(0, str(_ROOT)) + +CHOICES = { + "1": 'd4_1_ex1_books.py', + "2": 'd4_1_ex2_ptt.py', + "3": 'd4_1_ex3_ithelp.py', + "4": 'd4_1_web_scraping.py', +} + +print("=== 本資料夾含多個練習,請選擇 ===") +print(' 1. d4_1_ex1_books.py') +print(' 2. d4_1_ex2_ptt.py') +print(' 3. d4_1_ex3_ithelp.py') +print(' 4. d4_1_web_scraping.py') +print(" (直接 Enter 預設 1)") +sel = input("編號: ").strip() or "1" +name = CHOICES.get(sel) or CHOICES.get("1") +print(f"執行: {name}") +runpy.run_path(str(_ROOT / name), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/d4_1_ex2_ptt.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/d4_1_ex2_ptt.py" new file mode 100644 index 0000000000000000000000000000000000000000..4c0ad885893936a77642ecaba8a3ac6c53c8e88a --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/d4_1_ex2_ptt.py" @@ -0,0 +1,81 @@ +# d4_1_ex2_ptt.py — Day 4 學員填空 +# cd examples/Day4/student && python d4_1_ex2_ptt.py +# 參考答案:../d4_1_ex2_ptt.py + +import requests +from bs4 import BeautifulSoup +from urllib.parse import urljoin + +# 目標網站:PTT Python 討論板首頁 +URL = "https://www.ptt.cc/bbs/Python/index.html" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 填入瀏覽器的 User-Agent,否則 PTT 會拒絕請求並回傳 403 錯誤 +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在抓取 PTT Python 板最新文章: {URL} ...") + +try: + response = requests.get(URL, headers=HEADERS, timeout=10) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 2:定位文章區塊】 === + # TODO: 觀察網頁 HTML 結構,找出所有包裹文章資訊的
標籤(其 class 名稱為何?) + posts = soup.find_all("div", class_="r-ent") + + print(f"\n成功取得 {len(posts)} 筆文章項目(含已刪除與置底公告):") + print("=" * 80) + + for idx, post in enumerate(posts, 1): + # === 【學員填空練習點 3:提取文章標題與連結並防錯 (Null Check)】 === + # TODO: + # 1. 尋找 class 為 "title" 的
標籤 + # 2. 在該
底下尋找 標籤 (即文章連結) + # 3. ⚠️ 重要思考:若文章已刪除,將不會有 標籤。請撰寫條件判斷: + # - 若 標籤存在:提取其文字,並使用 urljoin 拼接 href 屬性得到完整網址。 + # - 若 標籤不存在:直接將 title_div 的文字作為標題,並將網址設為 "無 (文章已刪除)"。 + title_div = post.find("div", class_="title") + title_link = title_div.find("a") if title_div else None + + if title_link: + title = title_link.get("title") + relative_url = title_link.get("href") + post_url = urljoin(URL, relative_url) + else: + # 文章已刪除 + title_div = post.find("div", class_="title") + title = title_div.get_text().strip() if title_div else "(已刪除)" + post_url = "無 (文章已刪除)" + + + # === 【學員填空練習點 4:提取推文數】 === + # TODO: 尋找 class 為 "nrec" 的
標籤。 + # 技巧:使用 .get_text().strip() 取得文字。若無文字(0 推),請將變數預設為 "0"。 + nrec_div = post.find("div", class_="nrec") + nrec = nrec_div.get_text().strip() if nrec_div else "0" + + # === 【學員填空練習點 5:提取作者與發文日期】 === + # TODO: 分別尋找 class 為 "author" 與 "date" 的
標籤,並提取純文字(記得使用 .strip() 去除空白) + author = post.find("div", class_="author").get_text().strip() + date = post.find("div", class_="date").get_text().strip() + + # 輸出單篇文章資訊 + print(f"[{idx}] 推文數: {nrec:3s} | 發文日期: {date} | 作者: {author}") + print(f" 標題: {title}") + print(f" 網址: {post_url}") + print("-" * 80) + +except requests.exceptions.HTTPError as http_err: + print(f"[錯誤] PTT 伺服器拒絕或找不到網頁:{http_err}") +except requests.exceptions.RequestException as e: + print(f"[錯誤] 網路請求失敗:{e}") +except Exception as ex: + print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..e271a2a8c0d74bb4e05e2cc91e7720a53098f5cf --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_PTT/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex2_ptt.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex2_ptt.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_books/d4_1_ex1_books.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_books/d4_1_ex1_books.py" new file mode 100644 index 0000000000000000000000000000000000000000..7b20bb8c1ce86a4d012d8b072db8a037636431d9 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_books/d4_1_ex1_books.py" @@ -0,0 +1,94 @@ +# d4_1_ex1_books.py — Day 4 學員填空 +# cd examples/Day4/student && python d4_1_ex1_books.py +# 參考答案:../d4_1_ex1_books.py + +import requests +from bs4 import BeautifulSoup +from urllib.parse import urljoin + +# 目標網站:專供爬蟲練習的圖書商城沙盒 +URL = "http://books.toscrape.com/" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 建立 headers 字典,填入 "User-Agent" 來模擬真實瀏覽器(降低被簡易防爬機制拒絕的機率) +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在連線至圖書沙盒網站: {URL} ...") + +try: + # 發送 GET 請求取得網頁 HTML + response = requests.get(URL, headers=HEADERS, timeout=10) + + # 檢查 HTTP 狀態碼是否為 200 (OK),非 200 時引發 HTTPError 異常 + response.raise_for_status() + + # 解決編碼問題:強制將解碼設定為 utf-8 (避免 requests 預設 ISO-8859-1 解碼導致英鎊符號亂碼且在 Windows 印出時崩潰) + response.encoding = response.apparent_encoding + + print("連線成功!開始解析書籍資料...\n") + + # 初始化 BeautifulSoup 解析器 + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 2:定位 HTML 標籤區塊】 === + # TODO: 找出網頁中所有書籍的容器。每本書都被包在一個
標籤中,且該標籤的 class 固定為 "product_pod" + books = soup.find_all("article", class_="product_pod") + + print(f"成功在本頁找到 {len(books)} 本書籍:") + print("=" * 70) + + for idx, book in enumerate(books, 1): + # === 【學員填空練習點 3:提取書籍名稱與連結】 === + # TODO: + # 1. 尋找

底下的 標籤 + # 2. 取得 的 "title" 屬性(因為畫面上長度限制,.get_text() 可能會被 ellipsis 截斷) + # 3. 取得 的 "href" 屬性並使用 urljoin 拼接成完整網址 + title_link = book.find("h3").find("a") + title = title_link.get("title") # 取得屬性 'title' 的值,避免拿到被截斷的文字 + + relative_url = title_link.get("href") + detail_url = urljoin(URL, relative_url) + + # === 【學員填空練習點 4:提取書籍價格】 === + # 提示:為了避免 Windows (CP950) 終端機不支援英鎊符號 '£' 而編碼崩潰,請加上 .replace("£", "GBP") 將其替換 + # 觀察:價格存放在 class 為 "price_color" 的

標籤中,格式如 "£51.77" + # 技巧:將英鎊符號 '£' 替換為 'GBP',避免在 Windows (CP950) 終端機印出時因不支援該字元而崩潰 + price_text = book.find("p", class_="price_color").get_text().replace("£", "GBP") + + # === 【學員填空練習點 5:提取評價星等 (Rating)】 === + # TODO: + # 1. 尋找 class 為 "star-rating" 的

標籤 + # 2. 取得該標籤的 "class" 屬性值(這會是一個串列,如 ['star-rating', 'Three']) + # 3. 提取該串列的第二個元素作為星等字串 (若存在) + + rating_tag = book.find("p", class_="star-rating") + rating_classes = rating_tag.get("class") + rating = "無" + if len(rating_classes) > 1: + rating = rating_classes[1] # 提取例如 'Three' (代表三顆星) + + # === 【學員填空練習點 6:提取庫存狀態】 === + # TODO: 尋找 class 為 "instock availability" 的

標籤,取得文字後去除首尾空白 (.strip()) + stock_text = book.find("p", class_="instock availability").get_text().strip() + + # 印出解析結果 + print(f"【編號 {idx}】") + print(f"[書名] {title}") + print(f"[價格] {price_text}") + print(f"[評價] {rating} 星") + print(f"[狀態] {stock_text}") + print(f"[連結] {detail_url}") + print("-" * 70) + +except requests.exceptions.HTTPError as http_err: + print(f"[錯誤] HTTP 連線異常:{http_err}") +except requests.exceptions.RequestException as e: + print(f"[錯誤] 網路請求失敗:{e}") +except Exception as ex: + print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..afd501d3f608ba4c046ff4983d27e9763690b3b7 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_books/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex1_books.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex1_books.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/d4_1_ex3_ithelp.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/d4_1_ex3_ithelp.py" new file mode 100644 index 0000000000000000000000000000000000000000..c0299624ce527f07bab03c30f85184640b1ba40c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/d4_1_ex3_ithelp.py" @@ -0,0 +1,98 @@ +# d4_1_ex3_ithelp.py — Day 4 學員填空 +# cd examples/Day4/student && python d4_1_ex3_ithelp.py +# 參考答案:../d4_1_ex3_ithelp.py + +import requests +from bs4 import BeautifulSoup + +# 目標網站:iT 邦幫忙技術文章首頁 +URL = "https://ithelp.ithome.com.tw/articles?tab=tech" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 填入瀏覽器的 User-Agent,否則可能被伺服器拒絕連線 +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在連線至 iT 邦幫忙並抓取技術文章: {URL} ...") + +try: + response = requests.get(URL, headers=HEADERS, timeout=10) + response.raise_for_status() + + print("連線成功!解析文章列表中...\n") + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 2:定位文章卡片區塊】 === + # TODO: 尋找所有包裹文章資訊的

標籤(其 class 為 "qa-list") + articles = soup.find_all("div", class_="qa-list") + + print(f"成功取得前 {len(articles)} 篇熱門技術文章:") + print("=" * 80) + + for idx, art in enumerate(articles, 1): + # === 【學員填空練習點 3:提取標題與超連結】 === + # TODO: + # 1. 在 art 中尋找 class 為 "qa-list__title-link" 的 標籤 + # 2. 提取其純文字內容並去除首尾空白 (.strip()),作為標題 + # 3. 提取其 "href" 屬性,作為文章連結 + title_link = art.find("a", class_="qa-list__title-link") + if not title_link: + continue + title = title_link.get_text().strip() + post_url = title_link.get("href").strip() + + # === 【學員填空練習點 4:提取作者名稱】 === + # TODO: + # 1. 尋找 class 為 "qa-list__info" 的
標籤 + # 2. 在該
下尋找 class 為 "qa-list__info-link" 的 標籤 + # 3. 提取其文字內容作為作者名稱 + info_div = art.find("div", class_="qa-list__info") + author = "未知作者" + if info_div: + author_link = info_div.find("a", class_="qa-list__info-link") + if author_link: + author = author_link.get_text().strip() + + # === 【學員填空練習點 5:提取發文時間】 === + # TODO: 尋找 class 為 "qa-list__info-time" 的 標籤,並提取其 "title" 屬性值(因 title 屬性中包含完整時間秒數) + time_link = art.find("div", class_="qa-list__info-time") + post_time = time_link.get("title") if time_link else "未知時間" + + # === 【學員填空練習點 6:利用 List 索引提取多項數據】 === + # TODO: + # 1. 使用 find_all 找出卡片中所有 class 為 "qa-condition" 的 標籤 + # 2. 當抓取到的數量大於等於 3 時,利用 List 索引取得對應的資訊: + # - 索引 0 的項目底下的 "qa-condition__count" :Like 數 + # - 索引 1 的項目底下的 "qa-condition__count" :回應數 + # - 索引 2 的項目底下的 "qa-condition__count" :瀏覽數 + conditions = art.find_all("a", class_="qa-condition") + + likes = "0" + comments = "0" + views = "0" + + if len(conditions) >= 3: + # 請在此處填入利用 conditions[0]、conditions[1]、conditions[2] 尋找 span 的代碼 + likes = conditions[0].find("span", class_="qa-condition__count").get_text().strip() + comments = conditions[1].find("span", class_="qa-condition__count").get_text().strip() + views = conditions[2].find("span", class_="qa-condition__count").get_text().strip() + + # 格式化輸出 + print(f"【排名 {idx}】") + print(f"[標題] {title}") + print(f"[作者] {author} | [時間] {post_time}") + print(f"[數據] Like: {likes} | 回應: {comments} | 瀏覽/訂閱: {views}") + print(f"[連結] {post_url}") + print("-" * 80) + +except requests.exceptions.HTTPError as http_err: + print(f"[錯誤] 伺服器回應異常:{http_err}") +except requests.exceptions.RequestException as e: + print(f"[錯誤] 網路請求失敗:{e}") +except Exception as ex: + print(f"[錯誤] 解析資料時發生非預期錯誤:{ex}") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..2ee124121c9acf2163ab18a3f956886b02cea663 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_ithelp/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_ex3_ithelp.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_ex3_ithelp.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/d4_1_web_scraping.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/d4_1_web_scraping.py" new file mode 100644 index 0000000000000000000000000000000000000000..26792f288eaa50ea59f82a077985c21c5578d14d --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/d4_1_web_scraping.py" @@ -0,0 +1,68 @@ +# d4_1_web_scraping.py — Day4 學員填空 +# cd examples/Day4/student && python3 d4_1_web_scraping.py +# 參考答案:../d4_1_web_scraping.py + +import requests +from bs4 import BeautifulSoup + +# 本範例抓取專供網頁爬蟲教學使用的模擬沙盒網站:Quotes to Scrape +URL = "http://quotes.toscrape.com/" + +# === 【學員填空練習點 1:模擬瀏覽器 Header】 === +# TODO: 建立 headers 字典,填入 "User-Agent" 來模擬真實瀏覽器(降低被簡易防爬機制拒絕的機率) +HEADERS = { + "User-Agent": ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) +} + +print(f"正在連線並抓取網站內容: {URL} ...") + +try: + # === 【學員填空練習點 2:連線請求與下載網頁】 === + # TODO: 1. 使用 requests.get 發送 GET 請求,帶入 headers=HEADERS 與設定 timeout=5 + # TODO: 2. 判斷 response.status_code 是否為 200 (下載成功) + response = requests.get(URL, headers=HEADERS, timeout=5) + + if response and response.status_code == 200: + print("網頁下載成功!開始解析 HTML 結構...") + + # === 【學員填空練習點 3:初始化 BeautifulSoup 解析器】 === + # TODO: 使用 BeautifulSoup 解析 response.text,初始化 html.parser + soup = BeautifulSoup(response.text, "html.parser") + + # === 【學員填空練習點 4:定位 HTML 標籤區塊】 === + # TODO: 找出網頁中所有名言與作者的 div 標籤(其 class 為 'quote') + quote_divs = soup.find_all("div", class_="quote") + + print(f"\n成功解析!本日精選名言如下(共 {len(quote_divs)} 筆):") + print("=" * 60) + + # === 【學員填空練習點 5:迴圈遍歷提取內容與防錯】 === + # TODO: + # 1. 遍歷 quote_divs,並使用 enumerate 取得索引 idx(從 1 開始) + # 2. 用 .find() 尋找 span 且 class 為 'text' 的名言元素 + # 3. 用 .find() 尋找 small 且 class 為 'author' 的作者元素 + # 4. 進行 None 防錯處理,確保兩者皆非空值後,使用 .get_text() 提取純文字並印出 + for idx, quote_div in enumerate(quote_divs, 1): + text_el = quote_div.find("span", class_="text") + author_el = quote_div.find("small", class_="author") + if text_el is None or author_el is None: + continue + text = text_el.get_text() + author = author_el.get_text() + + print(f"[{idx}] \"{text}\"") + print(f" -- by {author}\n") + + print("=" * 60) + print("提醒:請尊重 robots.txt 與網站使用條款,勿對目標網站發送高頻請求。") + else: + status = response.status_code if response else "未知" + print(f"[錯誤] 無法抓取網頁,伺服器回應狀態碼:{status}") + +except requests.exceptions.RequestException as e: + print(f"[錯誤] 連線失敗:{e}") + print("請確認電腦的網路連線是否通暢。") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..e51446f49e2d44e7c5116b2f9764bfee686f6e7e --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220_\345\220\215\350\250\200\347\266\262\347\253\231/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_1_web_scraping.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_1_web_scraping.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220and ai\345\240\261\345\221\212/agent_report_world.md" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/agent_report_world.md" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220and ai\345\240\261\345\221\212/agent_report_world.md" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/agent_report_world.md" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220and ai\345\240\261\345\221\212/ai_analysis_report_ptt.md" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/ai_analysis_report_ptt.md" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220and ai\345\240\261\345\221\212/ai_analysis_report_ptt.md" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/ai_analysis_report_ptt.md" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220and ai\345\240\261\345\221\212/d4_2_agent_task_chain.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_agent_task_chain.py" similarity index 80% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220and ai\345\240\261\345\221\212/d4_2_agent_task_chain.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_agent_task_chain.py" index fd39682a1a822f231b28e8b64cd01f90c739e558..b0d587bbf79c8a92bc9d2760ecf4394311cf1832 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220and ai\345\240\261\345\221\212/d4_2_agent_task_chain.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_agent_task_chain.py" @@ -7,8 +7,39 @@ import os import requests from bs4 import BeautifulSoup + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JynZRgTmhZIMkFFN5GHSFClFfC3sraiSQ4Pb8d9v0V6Q") +api_key = _telearn_require_gemini_api_key(allow_skip=True) HEADERS = { "User-Agent": ( @@ -96,7 +127,7 @@ def run_task_chain(topic): f"- 模擬數據:\"Love is all, love is new.\" -- John Lennon\n\n" f"## 二、 核心思想分析\n" f"本機模擬分析指出,此主題的語錄主要著眼於人與人之間的連結與心靈啟發。\n" - f"*(注意:此為 Mock 離線生成報告,設定 GEMINI_API_KEY 以啟用 Gemini 真實生成。)*\n" + f"*(注意:此為 Mock 離線生成報告,於啟動時輸入 GEMINI_API_KEY 以啟用 Gemini 真實生成。)*\n" ) else: print(f"\n[步驟 2] 正在調用 Gemini ({GEMINI_MODEL}) 進行深度分析與排版...") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220and ai\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" similarity index 86% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220and ai\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" index bcc7bc24788d54745af8e8640a2c785dee418878..70209a507e1af1b8fa76f9b4de859b481d1901f0 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220and ai\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/d4_2_ex2_crawler_ai.py" @@ -9,8 +9,39 @@ from urllib.parse import urljoin from google import genai # 設定預設使用的 Gemini 模型 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-3.1-flash-lite") -api_key = os.environ.get("GEMINI_API_KEY","AQ.Ab8RN6JynZRgTmhZIMkFFN5GHSFClFfC3sraiSQ4Pb8d9v0V6Q") +api_key = _telearn_require_gemini_api_key(allow_skip=True) HEADERS = { @@ -159,7 +190,7 @@ def run_analysis_chain(source_type): } if not api_key: - print("\n[步驟 2] 未偵測到環境變數 GEMINI_API_KEY,自動啟動 Mock 離線降級機制...") + print("\n[步驟 2] 未輸入 GEMINI_API_KEY,啟動 Mock 離線降級機制...") report_content = ( f"# 模擬輿情分析報告\n\n" f"數據來源: {source_type.upper()}\n\n" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..a5ae937359f5ab6c3d6b6730ee4cf5c043ea685a --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\347\210\254\350\237\262\345\210\206\346\236\220\350\210\207AI\345\240\261\345\221\212/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d4_2_ex2_crawler_ai.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd4_2_ex2_crawler_ai.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/3F.webp" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/3F.webp" new file mode 100644 index 0000000000000000000000000000000000000000..973d2fe4c6a401a65bf9a6485019516ce4e5611c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/3F.webp" @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31fea88b7ebdddfd05553cb1f44474ce8277cd99a4a7b6f9553194e394ec4376 +size 28714 diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/3d_model_face/face_mesh_connections.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/face_mesh_connections.py" similarity index 100% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/3d_model_face/face_mesh_connections.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/face_mesh_connections.py" diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..1c74dbd4c33dd2ab30e9666859e7d417aa7bfb6c --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: step1_facemesh.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'step1_facemesh.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/3d_model_face/step1_facemesh.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/step1_facemesh.py" similarity index 96% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/3d_model_face/step1_facemesh.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/step1_facemesh.py" index c869cf44f1b0f001aa70da2de5fa464a9962d5c3..a3b339775f1ad7036bdab501e528767ada540257 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/3d_model_face/step1_facemesh.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\207\211\351\203\2503D\346\250\241\345\236\213/step1_facemesh.py" @@ -1,111 +1,111 @@ -""" -Week 11 - Step 1: Face Mesh Tracking with Connections (Template 填空版) -=================================================================== -本步驟目標: - 1. 使用 OpenCV 開啟視訊鏡頭 - 2. 載入 MediaPipe 臉部特徵偵測器 (face_landmarker.task) - 3. 偵測人臉,並在臉上畫出綠色的 3D 特效網格線與黃色特徵點 -""" -import cv2 -import mediapipe as mp -import numpy as np -import sys -import os - -# 1. 初始化攝影機 -cap = cv2.VideoCapture(0) -if not cap.isOpened(): - print("Error: Could not open camera.") - sys.exit() - -# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) -model_path = os.path.join('face_landmarker.task') -if not os.path.exists(model_path): - print(f"Error: Model file not found at {model_path}") - print("Please make sure face_landmarker.task is in the examples folder.") - sys.exit() - -try: - from mediapipe.tasks import python - from mediapipe.tasks.python import vision - base_options = python.BaseOptions(model_asset_path=model_path) - options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) - detector = vision.FaceLandmarker.create_from_options(options) - print("Face Landmarker Tasks API initialized successfully.") -except Exception as e: - print(f"Error initializing detector: {e}") - sys.exit() - -# 匯入臉部網格連接點定義 -try: - sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - from face_mesh_connections import FACEMESH_TESSELATION -except ImportError: - FACEMESH_TESSELATION = [] - -# 繪圖模式:0 = 僅畫網格線, 1 = 僅畫點點, 2 = 點線都畫 -draw_mode = 0 - -print("Press 'q' in the window to exit, or 'm' to toggle display modes.") - -# 3. 主迴圈 -while True: - success, img = cap.read() - if not success: - print("Error: Failed to read frame.") - break - - # 【TODO 1】畫面水平翻轉,並將 BGR 轉成 MediaPipe 需要的 RGB 格式 - img = cv2.flip(img, 1) - img_h, img_w, _ = img.shape - img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) - - - # 執行臉部偵測 - results = detector.detect(mp_image) - - # 【TODO 2】如果偵測到臉部,畫出 3D 特效網格線 - # 提示:取出 results.face_landmarks[0],並用迴圈巡覽 FACEMESH_TESSELATION - # 點的座標 (lm.x, lm.y) 是 0~1 的比例,需要乘上 img_w 和 img_h 換算成像素座標 - if results.face_landmarks: - face_lms = results.face_landmarks[0] - num_lms = len(face_lms) - - if draw_mode == 0 or draw_mode == 2: - for start_idx, end_idx in FACEMESH_TESSELATION: - if start_idx < num_lms and end_idx < num_lms: - lm1 = face_lms[start_idx] - lm2 = face_lms[end_idx] - pt1 = (int(lm1.x * img_w), int(lm1.y * img_h)) - pt2 = (int(lm2.x * img_w), int(lm2.y * img_h)) - cv2.line(img, pt1, pt2, (224, 224, 0), 1) # 青綠色線條 - - - # 【TODO 3】畫出臉部 468 個黃色特徵點點 - # if draw_mode == 1 or draw_mode == 2: - - if draw_mode == 1 or draw_mode == 2: - for lm in face_lms: - cx = int(lm.x * img_w) - cy = int(lm.y * img_h) - cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) - - mode_str = "Mesh" if draw_mode == 0 else "Dots" if draw_mode == 1 else "Both" - cv2.putText(img, f"Face Detected | Mode: {mode_str}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) - else: - cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) - - # 顯示操作提示文字 - cv2.putText(img, "Press 'm' to toggle mode | 'q' to quit", (10, img_h - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1) - - cv2.imshow("Face Mesh Connections", img) - - key = cv2.waitKey(1) & 0xFF - if key == ord('q'): - break - elif key == ord('m'): - draw_mode = (draw_mode + 1) % 3 - -cap.release() -cv2.destroyAllWindows() +""" +Week 11 - Step 1: Face Mesh Tracking with Connections (Template 填空版) +=================================================================== +本步驟目標: + 1. 使用 OpenCV 開啟視訊鏡頭 + 2. 載入 MediaPipe 臉部特徵偵測器 (face_landmarker.task) + 3. 偵測人臉,並在臉上畫出綠色的 3D 特效網格線與黃色特徵點 +""" +import cv2 +import mediapipe as mp +import numpy as np +import sys +import os + +# 1. 初始化攝影機 +cap = cv2.VideoCapture(0) +if not cap.isOpened(): + print("Error: Could not open camera.") + sys.exit() + +# 2. 載入 MediaPipe 臉部偵測器模型 (Face Landmarker) +model_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'face_landmarker.task') +if not os.path.exists(model_path): + print(f"Error: Model file not found at {model_path}") + print("Please make sure face_landmarker.task is in the examples folder.") + sys.exit() + +try: + from mediapipe.tasks import python + from mediapipe.tasks.python import vision + base_options = python.BaseOptions(model_asset_path=model_path) + options = vision.FaceLandmarkerOptions(base_options=base_options, num_faces=1) + detector = vision.FaceLandmarker.create_from_options(options) + print("Face Landmarker Tasks API initialized successfully.") +except Exception as e: + print(f"Error initializing detector: {e}") + sys.exit() + +# 匯入臉部網格連接點定義 +try: + sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + from face_mesh_connections import FACEMESH_TESSELATION +except ImportError: + FACEMESH_TESSELATION = [] + +# 繪圖模式:0 = 僅畫網格線, 1 = 僅畫點點, 2 = 點線都畫 +draw_mode = 0 + +print("Press 'q' in the window to exit, or 'm' to toggle display modes.") + +# 3. 主迴圈 +while True: + success, img = cap.read() + if not success: + print("Error: Failed to read frame.") + break + + # 【TODO 1】畫面水平翻轉,並將 BGR 轉成 MediaPipe 需要的 RGB 格式 + img = cv2.flip(img, 1) + img_h, img_w, _ = img.shape + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb) + + + # 執行臉部偵測 + results = detector.detect(mp_image) + + # 【TODO 2】如果偵測到臉部,畫出 3D 特效網格線 + # 提示:取出 results.face_landmarks[0],並用迴圈巡覽 FACEMESH_TESSELATION + # 點的座標 (lm.x, lm.y) 是 0~1 的比例,需要乘上 img_w 和 img_h 換算成像素座標 + if results.face_landmarks: + face_lms = results.face_landmarks[0] + num_lms = len(face_lms) + + if draw_mode == 0 or draw_mode == 2: + for start_idx, end_idx in FACEMESH_TESSELATION: + if start_idx < num_lms and end_idx < num_lms: + lm1 = face_lms[start_idx] + lm2 = face_lms[end_idx] + pt1 = (int(lm1.x * img_w), int(lm1.y * img_h)) + pt2 = (int(lm2.x * img_w), int(lm2.y * img_h)) + cv2.line(img, pt1, pt2, (224, 224, 0), 1) # 青綠色線條 + + + # 【TODO 3】畫出臉部 468 個黃色特徵點點 + # if draw_mode == 1 or draw_mode == 2: + + if draw_mode == 1 or draw_mode == 2: + for lm in face_lms: + cx = int(lm.x * img_w) + cy = int(lm.y * img_h) + cv2.circle(img, (cx, cy), 1, (0, 255, 255), -1) + + mode_str = "Mesh" if draw_mode == 0 else "Dots" if draw_mode == 1 else "Both" + cv2.putText(img, f"Face Detected | Mode: {mode_str}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + else: + cv2.putText(img, "No Face Detected", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) + + # 顯示操作提示文字 + cv2.putText(img, "Press 'm' to toggle mode | 'q' to quit", (10, img_h - 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1) + + cv2.imshow("Face Mesh Connections", img) + + key = cv2.waitKey(1) & 0xFF + if key == ord('q'): + break + elif key == ord('m'): + draw_mode = (draw_mode + 1) % 3 + +cap.release() +cv2.destroyAllWindows() diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\250\255\350\250\210AI\350\247\222\350\211\262and\345\240\264\346\231\257/d2_2_prompt_engineering.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" similarity index 57% rename from "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\250\255\350\250\210AI\350\247\222\350\211\262and\345\240\264\346\231\257/d2_2_prompt_engineering.py" rename to "projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" index 9936416cb5297c13c2c45377afbc8a138864eed7..da5691a06db382cfb0e079d4fb53dfde6f3819ef 100644 --- "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\250\255\350\250\210AI\350\247\222\350\211\262and\345\240\264\346\231\257/d2_2_prompt_engineering.py" +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/d2_2_prompt_engineering.py" @@ -1,59 +1,87 @@ -# d2_2_prompt_engineering.py -# Day 2: 生成式 AI 串接實戰 - Prompt 工程 (系統指令與結構化輸出) -import os -from google import genai - -# 取得 API 金鑰 -api_key = os.environ.get("GEMINI_API_KEY", "AQ.Ab8RN6JynZRgTmhZIMkFFN5GHSFClFfC3sraiSQ4Pb8d9v0V6Q") -if not api_key: - print("[提示] 請輸入您的 Google AI Studio API 金鑰:") - api_key = input("API Key: ").strip() - -client = genai.Client(api_key=api_key) - -# 1. 設置系統指令 (System Instruction / System Prompt) -# TODO 1: -system_prompt = "你是國中英語老師,請用簡單易懂的方式解釋英文單字,專業且有耐心,並提供 KK音標與例句" - - -print("正在初始化英語教學助理模型...") - -# 2. Few-shot (少樣本) 引導與對話演練 -# TODO 2: -user_word = "黑人" - -print(f"\n[學生輸入]: {user_word}") -prompt = f"請幫我翻譯這個詞:'{user_word}'。" - -response = client.models.generate_content( - model='gemini-3.1-flash-lite', - contents=prompt, - config={'system_instruction': system_prompt} -) - -print("\n[AI 老師回應]:") -print(response.text) - - -# 3. 結構化 JSON 輸出 (Structured Output) 範例 -print("\n" + "="*40) -print("進階挑戰:要求模型回傳 JSON 格式") -print("="*40) - -# 我們可以在 prompt 中要求,並透過 configuration 設定 JSON 輸出格式 -# TODO 3: -json_prompt = ( - f"請將中文單字 '{user_word}' 的翻譯結果整理成 JSON 格式,包含以下三個欄位:\n" - "- 'english': 英文單字\n" - "- 'kk_phonetic': KK音標\n" - "- 'example_sentence': 適合國中生理解的英文例句與中文翻譯" - "- '用法': 詞性與用法說明\n" - ) - -response = client.models.generate_content( - model='gemini-3.1-flash-lite', - contents=json_prompt, - config={"response_mime_type": "application/json"} -) -print("\n[AI 回傳的原始 JSON]:") -print(response.text) +# d2_2_prompt_engineering.py +# Day 2: 生成式 AI 串接實戰 - Prompt 工程 (系統指令與結構化輸出) +import os +from google import genai + +# 取得 API 金鑰 + +def _telearn_require_gemini_api_key(*, allow_skip: bool = False) -> str: + """終端機輸入介面:請使用者自行貼上 GEMINI_API_KEY。""" + import os + key = (os.environ.get("GEMINI_API_KEY") or "").strip() + if key: + print("[Telearn] 已使用環境變數中的 GEMINI_API_KEY") + return key + print() + print("=" * 54) + print(" 本專案需要 Google Gemini API Key") + print(" 取得金鑰:https://aistudio.google.com/apikey") + print(" 請在下方貼上 Key 後按 Enter") + print(" (僅本次執行使用,不會寫入任何檔案)") + if allow_skip: + print(" 若要改用離線 Mock 模式:直接按 Enter 略過") + print("=" * 54) + try: + key = input("請輸入 GEMINI_API_KEY: ").strip() + except EOFError: + key = "" + if key: + os.environ["GEMINI_API_KEY"] = key + print("[Telearn] 已接收 API Key,開始連線…") + return key + if allow_skip: + print("[Telearn] 未輸入 Key → 使用離線 Mock / 略過雲端功能") + return "" + print("[結束] 未輸入 API Key,無法繼續執行 Gemini 功能。") + raise SystemExit(0) + +api_key = _telearn_require_gemini_api_key(allow_skip=False) + +client = genai.Client(api_key=api_key) + +# 1. 設置系統指令 (System Instruction / System Prompt) +# TODO 1: +system_prompt = "你是國中英語老師,請用簡單易懂的方式解釋英文單字,專業且有耐心,並提供 KK音標與例句" + + +print("正在初始化英語教學助理模型...") + +# 2. Few-shot (少樣本) 引導與對話演練 +# TODO 2: +user_word = "黑人" + +print(f"\n[學生輸入]: {user_word}") +prompt = f"請幫我翻譯這個詞:'{user_word}'。" + +response = client.models.generate_content( + model='gemini-3.1-flash-lite', + contents=prompt, + config={'system_instruction': system_prompt} +) + +print("\n[AI 老師回應]:") +print(response.text) + + +# 3. 結構化 JSON 輸出 (Structured Output) 範例 +print("\n" + "="*40) +print("進階挑戰:要求模型回傳 JSON 格式") +print("="*40) + +# 我們可以在 prompt 中要求,並透過 configuration 設定 JSON 輸出格式 +# TODO 3: +json_prompt = ( + f"請將中文單字 '{user_word}' 的翻譯結果整理成 JSON 格式,包含以下三個欄位:\n" + "- 'english': 英文單字\n" + "- 'kk_phonetic': KK音標\n" + "- 'example_sentence': 適合國中生理解的英文例句與中文翻譯" + "- '用法': 詞性與用法說明\n" + ) + +response = client.models.generate_content( + model='gemini-3.1-flash-lite', + contents=json_prompt, + config={"response_mime_type": "application/json"} +) +print("\n[AI 回傳的原始 JSON]:") +print(response.text) diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..5f819ed5e0570c50eae23f77a85ad0660170f673 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\250\255\350\250\210AI\350\247\222\350\211\262\350\210\207\345\240\264\346\231\257/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d2_2_prompt_engineering.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd2_2_prompt_engineering.py'), run_name="__main__") diff --git "a/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" new file mode 100644 index 0000000000000000000000000000000000000000..69cdb69578cc4ce89cad8c1538eb2191bd6e5200 --- /dev/null +++ "b/projects/2026_AI\347\207\2371/\346\233\276\351\272\222\346\264\213/\350\263\207\346\226\231\347\225\260\345\270\270\345\200\274\346\270\205\346\264\227/main.py" @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Telearn Arcade Hub 入口檔(自動產生) +實際執行: d1_2_data_cleaning.py +規格: projects/{班級}/{學生}/{專案}/main.py +""" +from pathlib import Path +import os +import runpy +import sys + +_ROOT = Path(__file__).resolve().parent +os.chdir(_ROOT) +if str(_ROOT) not in sys.path: + sys.path.insert(0, str(_ROOT)) + +# 執行原始學員腳本 +runpy.run_path(str(_ROOT / 'd1_2_data_cleaning.py'), run_name="__main__") diff --git a/requirements.txt b/requirements.txt index 61d4fde24fbe3b9d946508e60ff07c42d3f90eaf..7aba206d5653287c6d69babcb1e35863d18be12a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,12 +5,24 @@ starlette>=0.27.0,<1.0.0 jinja2>=3.1.2,<4.0.0 websockets>=11.0.3,<16.0.0 -# Student project runtime +# Student project runtime (Pygame / OpenCV / MediaPipe) numpy>=1.24.0,<3.0.0 opencv-python-headless>=4.7.0.0,<5.0.0 pygame-ce>=2.3.0,<3.0.0 mediapipe>=0.10.0,<0.11.0 requests>=2.28.0,<3.0.0 +# Day1 data science demos +pandas>=2.0.0,<3.0.0 +matplotlib>=3.7.0,<4.0.0 + +# Day4 web scraping demos +beautifulsoup4>=4.12.0,<5.0.0 +lxml>=4.9.0,<6.0.0 + +# Day2 / Day4 Gemini demos (new + legacy SDK) +google-genai>=1.0.0,<2.0.0 +google-generativeai>=0.8.0,<0.9.0 + # Optional ML extras (Keras / Teachable Machine demos): # pip install -r requirements-ml.txt