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 結構,找出所有包裹文章資訊的
標籤中,格式如 "£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: 尋找所有包裹文章資訊的
標籤中,格式如 "£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: 尋找所有包裹文章資訊的
標籤中,格式如 "£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: 尋找所有包裹文章資訊的