# --- Crash-safety header: ต้องอยู่บนสุดก่อน import ไลบรารีอื่นทั้งหมด --- import os os.environ["USE_TF"] = "0" # ไม่ให้ transformers โหลด TensorFlow ซ้ำ (เราใช้แค่ PyTorch ฝั่งแปลภาษา) os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" # ลด log ของ TensorFlow os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0" # ปิด oneDNN กัน crash บน CPU บางรุ่น os.environ["TOKENIZERS_PARALLELISM"] = "false" import sys sys.modules["triton"] = None # triton segfault ตอน import บน CPU-only Space: ทำให้ import ล้มเหลวแบบปกติ torch จะข้ามไปเอง import faulthandler faulthandler.enable() # ถ้า crash อีก จะพิมพ์ตำแหน่งที่พังลง log # --- ZeroGPU: ต้อง import spaces ก่อน torch และต้องมีฟังก์ชัน @spaces.GPU อย่างน้อย 1 ตัว --- try: import spaces @spaces.GPU def _zerogpu_placeholder(): """ฟังก์ชันเปล่า ใช้ผ่านเงื่อนไข ZeroGPU เท่านั้น (โมเดล TensorFlow ของเรารันบน CPU ตามปกติ)""" return None except ImportError: pass # รันนอก Hugging Face Spaces ไม่ต้องใช้ # --------------------------------------------------------------------------------------- import torch # import torch ก่อน tensorflow เพื่อเลี่ยง segfault # --------------------------------------------------------------------- import os import cv2 import numpy as np import tensorflow as tf import gradio as gr from gtts import gTTS import uuid from transformers import AutoTokenizer, AutoModelForSeq2SeqLM import torch import re # ***** นำเข้า MobileNet และ Preprocessing ที่ถูกต้อง ***** from tensorflow.keras.applications import MobileNet from tensorflow.keras.applications.mobilenet import preprocess_input as mobilenet_preprocess_input # ======================================================= # 1. CONFIGURATION & MODEL SETUP # ======================================================= # --- Audio Fix 1: กำหนดและสร้างโฟลเดอร์ Output สำหรับไฟล์เสียง --- OUTPUT_DIR = "appliance_audio_outputs" if not os.path.exists(OUTPUT_DIR): os.makedirs(OUTPUT_DIR) # -------------------------------------------------------------------------- # --- ชื่อ Class ภาษาอังกฤษ (Key) --- class_names_en = [ "Air Conditioner", "Electric Kettle", "Fan", "Fridge", "Iron", "Microwave", "Toaster", "TV", "Vacuum Cleaner", "Washing Machine", # --- 💡 NEW CLASSES ADDED HERE --- "Rice Cooker", "Water Heater", "Water Purifier", "Blender", "Cooktop", "Air Purifier", "Water Dispenser", "Hair Dryer", "Water Pump", "Computer", "Electric Frying Pan" ] # --- ชื่อ Class ภาษาไทย (Value) --- class_names_th = [ "เครื่องปรับอากาศ", "กาต้มน้ำไฟฟ้า", "พัดลม", "ตู้เย็น", "เตารีด", "ไมโครเวฟ", "เครื่องปิ้งขนมปัง", "โทรทัศน์", "เครื่องดูดฝุ่น", "เครื่องซักผ้า", # --- 💡 NEW CLASSES ADDED HERE --- "หม้อหุงข้าว", "เครื่องทำน้ำอุ่น", "เครื่องกรองน้ำ", "เครื่องปั่น", "เตาทำอาหาร", "เครื่องฟอกอากาศ", "เครื่องกดน้ำดื่ม", "เครื่องเป่าผม", "ปั๊มน้ำ", "คอมพิวเตอร์", "กระทะไฟฟ้า" ] # --- สร้าง Dictionary สำหรับการแปลชื่อ Class --- CLASS_NAME_MAP = dict(zip(class_names_en, class_names_th)) class_names = class_names_en # --- 💡 อัปเดตจำนวนคลาสทั้งหมด (21 Classes) --- NUM_CLASSES = len(class_names) # ---------------------------------------------- IMG_SIZE = (224, 224) MODEL_WEIGHTS_PATH = "final_best_MobileNet_weights.h5" # --- MarianMT Setup (Thai -> English Translation) --- MARIAN_MODEL_NAME = "Helsinki-NLP/opus-mt-th-en" MT_tokenizer = None MT_model = None try: print(f"🔄 Loading MarianMT Model: {MARIAN_MODEL_NAME}...") MT_tokenizer = AutoTokenizer.from_pretrained(MARIAN_MODEL_NAME) MT_model = AutoModelForSeq2SeqLM.from_pretrained(MARIAN_MODEL_NAME) print("✅ MarianMT Model loaded.") except Exception as e: print(f"❌ Error loading MarianMT model (Translation will be disabled): {e}") # --- ฟังก์ชันสร้างโครงสร้างโมเดล --- def create_electrical_appliance_model(): """สร้างโครงสร้างโมเดล MobileNet + Custom Head""" base_model = MobileNet( weights='imagenet', include_top=False, input_shape=(IMG_SIZE[0], IMG_SIZE[1], 3) ) for layer in base_model.layers: layer.trainable = False model = tf.keras.models.Sequential([ base_model, tf.keras.layers.GlobalAveragePooling2D(), # 💡 ใช้ NUM_CLASSES ใหม่ที่ถูกอัปเดตแล้ว tf.keras.layers.Dense(NUM_CLASSES, activation='softmax') ]) return model # 1. โหลดโมเดลด้วยการสร้างโครงสร้างและโหลด Weights model = create_electrical_appliance_model() print("🔄 Loading custom MobileNet weights...") try: # ⚠️ โหลด Weights ด้วยโครงสร้างใหม่ (21 Classes) - ต้องมั่นใจว่าไฟล์ Weights นี้เทรนด้วย 21 คลาสแล้ว model.load_weights(MODEL_WEIGHTS_PATH) print(f"✅ Model '{MODEL_WEIGHTS_PATH}' loaded successfully.") except Exception as e: print(f"❌ FATAL Model Error: Failed to load custom weights from {MODEL_WEIGHTS_PATH}. Error: {e}") # raise SystemExit(1) # คอมเมนต์ออกเพื่อให้โค้ดรันได้ แม้ไม่มีไฟล์ print("⚠️ Continuing without weights. Prediction will be random.") # ======================================================= # 2. DATA AND TRANSLATION FUNCTIONS # ======================================================= # --- Appliance Details Data (Thai Source) --- # 💡 เพิ่มรายละเอียดสำหรับคลาสใหม่ (ใช้ข้อมูลตัวอย่าง) APPLIANCE_DETAILS = { "Air Conditioner": { "power": "1,000 ถึง 3,000 วัตต์", "install": "ควรติดตั้งในที่ร่มและสูงจากพื้น 2.5 ม. ขึ้นไปเพื่อประสิทธิภาพที่ดี", "care": "ล้างแผ่นกรองทุก 1 เดือน และเรียกช่างล้างใหญ่ปีละ 2 ครั้ง", "warning": "ห้ามติดตั้งใกล้แสงแดดหรือความชื้น และควรมีสายดิน" }, "Electric Kettle": { "power": "1,500 ถึง 2,200 วัตต์", "install": "ใช้งานบนพื้นผิวเรียบและแห้ง ห่างจากมือเด็ก", "care": "ทำความสะอาดตะกรันด้านในด้วยน้ำส้มสายชูหรือน้ำมะนาวทุก 1-2 เดือน", "warning": "ห้ามเปิดเครื่องโดยไม่มีน้ำ และระวังน้ำร้อนลวก" }, "Fan": { "power": "50 ถึง 100 วัตต์", "install": "ตั้งในที่อากาศถ่ายเทสะดวก ห่างจากผ้าม่าน", "care": "ถอดใบพัดและตะแกรงล้างทำความสะอาดทุก 2-3 สัปดาห์", "warning": "ห้ามสอดมือหรือสิ่งของเข้าไปขณะพัดลมทำงาน" }, "Fridge": { "power": "100 ถึง 200 วัตต์", "install": "ตั้งห่างจากผนัง 10-15 ซม. ห้ามตั้งใกล้แหล่งความร้อน", "care": "ตั้งอุณหภูมิ 2-5 องศาเซลเซียส และละลายน้ำแข็งเมื่อเริ่มหนา", "warning": "ห้ามใช้ปลั๊กพ่วงร่วมกับเครื่องใช้ไฟฟ้ากำลังสูง" }, "Iron": { "power": "1,000 ถึง 2,000 วัตต์", "install": "ใช้งานบนโต๊ะที่มั่นคง และควรมีแผ่นรองรีดทนความร้อน", "care": "ทำความสะอาดหน้าเตาด้วยผ้าชุบน้ำหมาดๆ เมื่อเครื่องเย็นแล้ว", "warning": "ห้ามวางเตารีดคว่ำหน้าเตาในขณะร้อน" }, "Microwave": { "power": "800 ถึง 1,500 วัตต์", "install": "วางบนพื้นผิวเรียบ ห้ามวางใกล้เตาแก๊ส", "care": "เช็ดภายในหลังใช้งานทันที เพื่อป้องกันคราบฝังแน่น", "warning": "ห้ามใส่ภาชนะโลหะหรือวัสดุไวไฟ" }, "Toaster": { "power": "800 ถึง 1,500 วัตต์", "install": "ใช้งานบนพื้นผิวทนความร้อน ห่างจากวัสดุติดไฟ", "care": "ถอดถาดรองเศษขนมปังออกมาทิ้งและทำความสะอาดทุกครั้ง", "warning": "ห้ามใช้มือหรือโลหะแหย่ขณะเครื่องทำงาน" }, "TV": { "power": "50 ถึง 300 วัตต์", "install": "วางบนชั้นที่มั่นคง หรือติดตั้งกับผนังที่แข็งแรง", "care": "ใช้ผ้านุ่มเช็ดหน้าจอ ห้ามใช้สารเคมีที่มีแอลกอฮอล์", "warning": "หลีกเลี่ยงการวางใกล้น้ำหรือความชื้น" }, "Vacuum Cleaner": { "power": "1,000 ถึง 2,000 วัตต์", "install": "ควรเลือกหัวดูดที่เหมาะสมกับพื้นผิวที่ต้องการทำความสะอาด", "care": "ทิ้งฝุ่นและทำความสะอาดตัวกรอง (Filter) ทันทีหลังการใช้งาน", "warning": "ห้ามดูดของเหลวหรือของมีคม" }, "Washing Machine": { "power": "200 ถึง 500 วัตต์ (ซัก), 2,000 ถึง 3,000 วัตต์ (ทำน้ำร้อน)", "install": "ติดตั้งบนพื้นที่ได้ระดับ และมีก๊อกน้ำ/ท่อระบายน้ำเข้าถึง", "care": "ทำความสะอาดถังซักด้วยน้ำยาเฉพาะทางเดือนละครั้ง หรือใช้น้ำส้มสายชูช่วยกำจัดเชื้อรา", "warning": "ห้ามเปิดฝาเครื่องขณะกำลังปั่น" }, "Rice Cooker": { "power": "500 ถึง 1,000 วัตต์", "install": "วางบนพื้นผิวเรียบและแห้ง ห้ามวางใกล้ผนังมากเกินไป", "care": "เช็ดทำความสะอาดหม้อชั้นในด้วยผ้านุ่ม ห้ามใช้ฝอยขัด", "warning": "ห้ามล้างตัวเครื่องด้วยน้ำ" }, "Water Heater": { "power": "2,000 ถึง 4,500 วัตต์", "install": "ควรติดตั้งสายดิน และควรติดตั้งโดยช่างผู้ชำนาญการ", "care": "ตรวจสอบระบบตัดไฟ (ELCB) และทำความสะอาดฝักบัวทุก 6 เดือน", "warning": "ห้ามใช้หากระบบตัดไฟทำงานผิดปกติ" }, "Water Purifier": { "power": "0 ถึง 100 วัตต์ (ถ้ามีปั๊ม/ทำความเย็น)", "install": "ติดตั้งใกล้ก๊อกน้ำประปา และในที่ร่ม", "care": "เปลี่ยนไส้กรองตามอายุการใช้งานที่กำหนดอย่างเคร่งครัด", "warning": "ห้ามใช้งานเมื่อไส้กรองหมดอายุ" }, "Blender": { "power": "300 ถึง 1,000 วัตต์", "install": "ใช้บนพื้นผิวที่มั่นคง อย่าให้สายไฟพาดผ่านคมมีด", "care": "ทำความสะอาดโถปั่นทันทีหลังใช้งาน และไม่ควรปั่นเกินกำลังเครื่อง", "warning": "ห้ามเปิดฝาโถขณะเครื่องทำงาน" }, "Cooktop": { "power": "1,000 ถึง 3,000 วัตต์ (ต่อหัว)", "install": "ต้องใช้อุปกรณ์ทำครัวที่เหมาะสม (เช่น เตาแม่เหล็กไฟฟ้าต้องใช้ภาชนะโลหะ)", "care": "เช็ดทำความสะอาดคราบอาหารทันทีหลังเครื่องเย็น", "warning": "ระวังพื้นผิวร้อนหลังใช้งาน" }, "Air Purifier": { "power": "30 ถึง 100 วัตต์", "install": "ตั้งไว้กลางห้อง หรือในบริเวณที่อากาศถ่ายเทได้ดี", "care": "ดูดฝุ่น/เช็ดแผ่นกรองหยาบ และเปลี่ยนแผ่นกรอง HEPA ตามอายุการใช้งาน", "warning": "ห้ามบังช่องดูดและช่องระบายอากาศ" }, "Water Dispenser": { "power": "100 ถึง 500 วัตต์ (ทำน้ำเย็น), 500 ถึง 1,000 วัตต์ (ทำน้ำร้อน)", "install": "ตั้งในที่ร่ม ห่างจากผนัง 10-15 ซม.", "care": "ทำความสะอาดถังเก็บน้ำและช่องจ่ายน้ำอย่างสม่ำเสมอ", "warning": "ระวังน้ำร้อนอาจทำให้เกิดแผลไหม้" }, "Hair Dryer": { "power": "1,000 ถึง 2,000 วัตต์", "install": "ใช้งานในที่แห้ง ห้ามใช้ใกล้น้ำ", "care": "ทำความสะอาดตะแกรงกรองด้านหลังเครื่องเพื่อป้องกันฝุ่นอุดตัน", "warning": "ห้ามใช้งานขณะมือเปียก" }, "Water Pump": { "power": "200 ถึง 500 วัตต์", "install": "ควรติดตั้งในที่ร่ม มีการระบายอากาศดี และมีถังพักน้ำ", "care": "ตรวจสอบการรั่วซึม และไม่ควรปล่อยให้ปั๊มทำงานโดยไม่มีน้ำ", "warning": "ห้ามเดินเครื่องขณะไม่มีน้ำในระบบ" }, "Computer": { "power": "150 ถึง 500 วัตต์ (เครื่องเดสก์ท็อป)", "install": "ตั้งในบริเวณที่อากาศถ่ายเทสะดวก ห้ามวางชิดผนัง", "care": "ทำความสะอาดฝุ่นภายในเครื่องและพัดลมอย่างสม่ำเสมอ", "warning": "ห้ามปิดกั้นช่องระบายความร้อน" }, "Electric Frying Pan": { "power": "1,000 ถึง 1,500 วัตต์", "install": "ใช้งานบนพื้นผิวที่ทนความร้อนและมั่นคง", "care": "ห้ามนำตัวกระทะลงน้ำทั้งอัน ทำความสะอาดเฉพาะส่วนที่ถอดได้", "warning": "ระวังความร้อนสูงขณะใช้งาน" } # ----------------------------------------------------------- } # --- MarianMT Translation Function (ใช้โค้ดเดิม) --- def translate_th_to_en(text_to_translate: str) -> str: """ฟังก์ชันสำหรับแปลข้อความจากไทยเป็นอังกฤษโดยใช้ MarianMT Model""" if not text_to_translate or MT_model is None: return text_to_translate try: encoded_input = MT_tokenizer(text_to_translate, return_tensors="pt") translated_tokens = MT_model.generate( **encoded_input, max_length=100, num_beams=4 ) return MT_tokenizer.decode(translated_tokens.squeeze(), skip_special_tokens=True) except Exception as e: print(f"Translation Error: {e}") return text_to_translate # --- GTTs Text-to-Audio Function (แก้ไขให้ใช้ OUTPUT_DIR) --- def text_to_audio(text_to_speak, lang='th'): """แปลงข้อความเป็นไฟล์เสียง และคืนค่า path ของไฟล์เสียง""" # Audio Fix 1: ใช้ os.path.join เพื่อสร้าง Path ในโฟลเดอร์ output filename = os.path.join(OUTPUT_DIR, f"output_audio_{uuid.uuid4()}.mp3") try: tts = gTTS(text=text_to_speak, lang=lang, slow=False) tts.save(filename) return filename except Exception as e: print(f"Error generating audio: {e}") return None def generate_and_play_audio_btn(text_to_speak, lang_code): """สร้างไฟล์เสียงแล้วส่ง Path ไปยัง Audio Component""" if not text_to_speak or "Error" in text_to_speak or "กรุณาอัปโหลด" in text_to_speak: print("Error: Audio generation skipped due to empty or error text.") return None return text_to_audio(text_to_speak, lang=lang_code) # ---------------------------------------------------- # 3. CORE LOGIC PIPELINE # ---------------------------------------------------- def format_details_text(class_name_en, class_name_th, lang, details): confidence = f"{details['confidence']:.1f}%" class_name_display = class_name_th if lang == "th" else class_name_en warning_text = details.get('warning', 'ไม่มีข้อมูลข้อควรระวัง') if lang == "th": return ( f"⚡ เครื่องใช้ไฟฟ้าที่ตรวจพบ: {class_name_display} (ความมั่นใจ {confidence})\n" f"🔌 กำลังไฟฟ้าโดยประมาณ: {details['power']}\n\n" f"🛠️ การติดตั้ง/ตำแหน่งที่เหมาะสม:\n" f"- {details['install']}\n\n" f"🛁 การดูแลการใช้งาน/เคล็ดลับ:\n" f"- {details['care']}\n\n" f"⚠️ ข้อควรระวัง:\n" f"- {warning_text}" ) else: return ( f"⚡ Detected Appliance: {class_name_display} (Confidence {confidence})\n" f"🔌 Estimated Power Consumption: {details['power']}\n\n" f"🛠️ Installation / Proper Placement:\n" f"- {details['install']}\n\n" f"🛁 Maintenance / Usage Tips:\n" f"- {details['care']}\n\n" f"⚠️ Warnings:\n" f"- {warning_text}" ) # 🎯 แก้ไข Outputs: ต้องคืนค่า initial_detail_text เพื่อให้ Textbox หลักแสดงผลทันที def full_classification_pipeline(input_img_path): """ฟังก์ชันทำนายผลหลักที่ส่งออกเฉพาะ Class ที่มั่นใจสูงสุดค่าเดียว และเตรียมข้อมูลสำหรับ UI""" if input_img_path is None: error_msg = "กรุณาอัปโหลดรูปภาพ หรือใช้กล้องเพื่อทำนายผล" # คืนค่า 5 Outputs: Top Class, Text TH State, Text EN State, Class TH State, Detail Textbox return error_msg, error_msg, error_msg, "N/A", error_msg try: input_img = cv2.imread(input_img_path) if input_img is None: raise ValueError("Error reading image file (input_img is None).") img = cv2.resize(input_img, IMG_SIZE) img = np.array(img, dtype=np.float32) img = np.expand_dims(img, axis=0) img = mobilenet_preprocess_input(img) # 2. Prediction predictions = model.predict(img, verbose=0) top_class_id = int(np.argmax(predictions[0])) # ⚠️ ตรวจสอบ: class_names ถูกอัปเดตเป็น 21 คลาสแล้ว top_class_name_en = class_names[top_class_id] top_class_name_th = CLASS_NAME_MAP.get(top_class_name_en, top_class_name_en) top_confidence = float(predictions[0][top_class_id]) # 3. Get Details and Translate # ⚠️ ตรวจสอบ: APPLIANCE_DETAILS ถูกอัปเดตเป็น 21 คลาสแล้ว details_th = dict(APPLIANCE_DETAILS.get( top_class_name_en, { "power": "N/A", "install": "N/A", "care": "N/A", "warning": "ไม่มีข้อมูลข้อควรระวัง" } )) details_en = { "power": translate_th_to_en(details_th["power"]), "install": translate_th_to_en(details_th["install"]), "care": translate_th_to_en(details_th["care"]), "warning": translate_th_to_en(details_th.get("warning", "")) } # 4. Format Text Output (TH & EN) details_th['confidence'] = top_confidence * 100 details_en['confidence'] = top_confidence * 100 predicted_class_display = f"Top Class: {top_class_name_th} ({top_class_name_en}) - {top_confidence*100:.1f}%" full_text_th = format_details_text(top_class_name_en, top_class_name_th, "th", details_th) full_text_en = format_details_text(top_class_name_en, top_class_name_th, "en", details_en) # 🎯 คืนค่า Text TH ไปยัง detail_textbox เพื่อให้แสดงผลเริ่มต้นทันที initial_detail_text = full_text_th # คืนค่า 5 Outputs: (1) ชื่อคลาสที่มั่นใจสูงสุด, (2) Text TH รายละเอียด (State), (3) Text EN รายละเอียด (State), (4) ชื่อคลาส TH (State), (5) Detail Textbox (Output UI) return predicted_class_display, full_text_th, full_text_en, top_class_name_th, initial_detail_text except Exception as e: error_msg_display = f"❌ Error processing image data. Details: {e}" return error_msg_display, error_msg_display, error_msg_display, "N/A", error_msg_display # --- Language Switch Function (ใช้โค้ดเดิม) --- def switch_language_display(lang, text_th, text_en, top_class_str_all, class_name_th_val): """สลับ Textbox ที่แสดงผลและชื่อ Class เมื่อผู้ใช้กด Radio Button""" if not top_class_str_all or "Error" in top_class_str_all or "กรุณาอัปโหลดรูปภาพ" in top_class_str_all: return gr.update(), gr.update() # ค้นหาชื่อ Class EN class_name_en = next((k for k, v in CLASS_NAME_MAP.items() if v == class_name_th_val), "N/A") confidence_match = re.search(r'(- \d+\.\d+%)', top_class_str_all) confidence_str = confidence_match.group(1) if confidence_match else "" if lang == "Thai": new_top_class_display = f"Top Class: {class_name_th_val} ({class_name_en}) {confidence_str}" new_detail_text = text_th else: new_top_class_display = f"Top Class: {class_name_en} ({class_name_th_val}) {confidence_str}" new_detail_text = text_en return new_detail_text, new_top_class_display # ---------------------------------------------------- # 4. GRADIO UI DEFINITION # ---------------------------------------------------- with gr.Blocks(title="MobileNet Electrical Appliance Classifier with Translation") as demo: gr.Markdown("## ⚡ MobileNet Electrical Appliance Classifier (Top 1 Class & Details)") # Audio Fix 2: เปลี่ยน Textbox ที่ซ่อนไว้เป็น State Components text_for_audio_th = gr.State("") text_for_audio_en = gr.State("") text_class_name_th = gr.State("") with gr.Row(): # Input Section with gr.Column(scale=1): with gr.Tabs(): with gr.Tab("🖼️ อัปโหลดไฟล์"): file_input = gr.Image(type="filepath", label="Upload Image Here", sources=["upload", "clipboard"]) submit_file_btn = gr.Button("Submit File") with gr.Tab("💻 เว็บแคม"): pc_webcam_input = gr.Image(label="Webcam Input (PC/Mac)", type="filepath", sources=["webcam"]) submit_pc_webcam_btn = gr.Button("Capture & Predict") with gr.Tab("📱 กล้องมือถือ"): smartphone_camera_input = gr.Image(label="Smartphone Camera Input", type="filepath", sources=["webcam"]) submit_mobile_camera_btn = gr.Button("Capture & Predict (Mobile)") # Output Section with gr.Column(scale=2): top_class_display = gr.Textbox(label="ผลลัพธ์การจำแนก (Class ที่มั่นใจสูงสุด)", lines=1, max_lines=1) detail_textbox = gr.Textbox(label="ข้อมูลเชิงลึกและการดูแลรักษา | Appliance Details & Care", lines=10) with gr.Row(): language_choice = gr.Radio( ["Thai", "English"], value="Thai", label="เลือกภาษา | Select Language", interactive=True ) with gr.Row(): audio_output = gr.Audio(label="Audio Playback", type="filepath", interactive=False, autoplay=True) with gr.Row(): # 🎯 ตั้งให้เป็น interactive=True ตั้งแต่เริ่มต้น เนื่องจาก Audio Logic แยกออกมาจาก Pipeline หลัก audio_th_btn = gr.Button("🔊 ฟังคำอธิบาย (Thai)", interactive=True) audio_en_btn = gr.Button("🔊 Listen to explanation (English)", interactive=True) # --- 5. EVENT HANDLERS --- # 🎯 Output Targets: 5 Outputs จาก Pipeline (4 State, 1 Textbox UI) pipeline_outputs = [ top_class_display, text_for_audio_th, text_for_audio_en, text_class_name_th, detail_textbox ] all_inputs = [file_input, pc_webcam_input, smartphone_camera_input] all_submit_btns = [submit_file_btn, submit_pc_webcam_btn, submit_mobile_camera_btn] # เชื่อมโยงแต่ละปุ่มเข้ากับ Pipeline (ใช้ .click() เดียว) for i, btn in enumerate(all_submit_btns): btn.click( fn=full_classification_pipeline, inputs=[all_inputs[i]], # Outputs คือ 4 State Component และ 1 Textbox ที่แสดงผลเริ่มต้น outputs=pipeline_outputs ) # เชื่อมโยง Radio Button (เมื่อผู้ใช้เปลี่ยนภาษา) language_choice.change( fn=switch_language_display, inputs=[language_choice, text_for_audio_th, text_for_audio_en, top_class_display, text_class_name_th], outputs=[detail_textbox, top_class_display] ) # เชื่อมโยงปุ่มเสียง (ใช้ Logic เดิม) audio_th_btn.click( fn=generate_and_play_audio_btn, inputs=[text_for_audio_th, gr.State("th")], outputs=[audio_output] ) audio_en_btn.click( fn=generate_and_play_audio_btn, inputs=[text_for_audio_en, gr.State("en")], outputs=[audio_output] ) if __name__ == "__main__": demo.launch()