From a8ed1ac5927a3c6ff0b33950a22353aabb861c5d Mon Sep 17 00:00:00 2001 From: Mason Huang Date: Fri, 7 Mar 2025 00:12:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E6=88=90=E9=9B=99=E5=B1=A4config?= =?UTF-8?q?=E6=9E=B6=E6=A7=8B,=E8=AA=BF=E6=95=B4app=E8=99=95=E7=90=86confi?= =?UTF-8?q?g=E8=B3=87=E6=96=99,=20=E5=A2=9E=E5=8A=A0inno=20setup=E7=9A=84i?= =?UTF-8?q?ss=20file,=20auto=20renew=20global=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 154 +++++++++++++++++--------- dist/test.iss | 29 ++--- src/config.py | 9 +- src/views/mainWindows.py | 230 +++++++++++++++++++++++++++++++-------- 4 files changed, 301 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index faaa984..6ca58db 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,7 @@ - 抓 camera -> -i image -m model -2. run code --> +# Kneron Academy v2.0 +這個應用程式是一個基於 Python、PyQt5、OpenCV 以及 Kneron SDK(kp)開發的 AI 應用 APP,使用者可以透過鏡頭、麥克風或上傳的方式經由 Kneron NPU 裝置進行實時運算。 - - - - -# Innovedus AI Playground - -這個應用程式是一個 AI Playground,用於通過相機鏡頭或使用者上傳圖片進行推論(例如火災檢測)。應用程式基於 Python、PyQt5、OpenCV 以及 Kneron SDK(kp)開發,支援 Video 模式與 Image 模式。 - ---- - ## 目錄 - [安裝設定](#安裝設定) - [專案架構](#專案架構) @@ -74,38 +55,37 @@ project/ upload/ └── photos, videos, or mp3 files utils/ - └── plugins/ - ├── mode1/ - │ ├── model1/ - │ │ ├── script.py - │ │ ├── model_file(s) - │ │ └── config.json - │ └── model2/ - │ ├── script.py - │ ├── model_file(s) - │ └── config.json - └── mode2/ - ├── model1/ - │ ├── script.py - │ ├── model_file(s) - │ └── config.json - └── model2/ - ├── script.py - ├── model_file(s) - └── config.json - └──firmware\ - ├── KLXXX/ - │ ├── fw_scpu.bin - │ ├── fw_ncpu.bin - │ ├── VERSION - │ └── other files - ├── KLXXX/ - ├── fw_scpu.bin - ├── fw_ncpu.bin - ├── VERSION - └── other files - └──config.json - └──REAMDE.md + ├── config.json + ├── REAMDE.md + │── mode1/ + │ ├── model1/ + │ │ ├── script.py + │ │ ├── model_file(s) + │ │ └── config.json + │ └── model2/ + │ ├── script.py + │ ├── model_file(s) + │ └── config.json + └── mode2/ + ├── model1/ + │ ├── script.py + │ ├── model_file(s) + │ └── config.json + └── model2/ + ├── script.py + ├── model_file(s) + └── config.json +firmware\ + ├── KLXXX/ + │ ├── fw_scpu.bin + │ ├── fw_ncpu.bin + │ ├── VERSION + │ └── other files + └── KLXXX/ + ├── fw_scpu.bin + ├── fw_ncpu.bin + ├── VERSION + └── other files ``` ## 功能概述 @@ -163,4 +143,70 @@ pyinstaller --onefile --windowed main.py --additional-hooks-dir=hooks --add-data ``` ## APP資料加密 -目前預計使用 [pyarmor](https://github.com/dashingsoft/pyarmor) 進行加密 \ No newline at end of file +目前預計使用 [pyarmor](https://github.com/dashingsoft/pyarmor) 進行加密 + + +## Script & Model 的設定 +整個 utils folder 會分成兩層: global config 和 model config + +model config 範例如下 +``` json +{ + "display_name": "人臉偵測 (ResNet-18)", + "description": "使用ResNet-18架構的高精度人臉偵測模型,可即時標記影像中的人臉位置", + "model_file": "face_detection.nef", + "input_info": { + "type": "video", + "supported_formats": ["mp4", "avi", "webm"] + }, + "input_parameters": { + "threshold": 0.75, + "max_faces": 10, + "tracking": true + }, + "compatible_devices": ["KL520", "KL720"] +} +``` +global config 範例如下 +```json +{ + "plugins": [ + { + "mode": "face_recognition", + "display_name": "人臉辨識", + "models": [ + { + "name": "face_detection", + "display_name": "人臉偵測 (ResNet-18)", + "description": "基於ResNet-18的高精度人臉偵測", + "compatible_devices": ["KL520", "KL720"] + }, + { + "name": "age_gender", + "display_name": "年齡性別辨識 (VGG-Face)", + "description": "使用VGG-Face架構辨識人臉年齡與性別", + "compatible_devices": ["KL520", "KL720"] + } + ] + }, + { + "mode": "object_detection", + "display_name": "物體偵測", + "models": [ + { + "name": "yolo_v5", + "display_name": "物體偵測 (YOLOv5)", + "description": "使用YOLOv5進行實時物體偵測與分類", + "compatible_devices": ["KL720"] + }, + { + "name": "rcnn", + "display_name": "精確物體識別 (Faster R-CNN)", + "description": "以Faster R-CNN為基礎的高精度物體識別", + "compatible_devices": ["KL720"] + } + ] + } + ] +} +``` \ No newline at end of file diff --git a/dist/test.iss b/dist/test.iss index 61d9ef1..d94efb2 100644 --- a/dist/test.iss +++ b/dist/test.iss @@ -1,12 +1,10 @@ ; Script generated by the Inno Setup Script Wizard. ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! - #define MyAppName "Kneron Academy" #define MyAppVersion "2.0" #define MyAppPublisher "Innovedus Inc." #define MyAppURL "https://www.example.com/" #define MyAppExeName "main.exe" - [Setup] ; 唯一的 AppId,請勿在其他應用程式中重複使用 AppId={{0894596D-D78B-4D8C-97CC-D90FE98E26E0}} @@ -28,37 +26,34 @@ PrivilegesRequired=lowest OutputBaseFilename=mysetup SolidCompression=yes WizardStyle=modern - [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" - [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked - ; 定義配對元件,讓使用者選擇是否安裝預設的 Script 與 Model [Components] Name: "pair1"; Description: "Fire Detection"; Name: "pair2"; Description: "Photo_quality"; - +Name: "pair3"; Description: "Test Mode"; [Files] ; 安裝主要執行檔到 {app} 目錄 -Source: "C:\Users\mason\Code\demo_gui\dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion - +Source: "C:\Users\mason\Code\Kneron Academy 2.0\dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +; 安裝 config.json 檔案 (不需使用者勾選) +Source: "C:\Users\mason\AppData\Local\Kneron_Academy\utils\config.json"; DestDir: "{localappdata}\Kneron_Academy\utils"; Flags: ignoreversion ; --- 配對1 --- ; pair 1 -Source: "C:\Users\mason\Downloads\Kneron_Academy\utils\models\fire_detection_520.nef"; DestDir: "{localappdata}\Kneron_Academy\utils\models\fire_detection_520.nef"; Components: pair1; Flags: ignoreversion recursesubdirs -Source: "C:\Users\mason\Downloads\Kneron_Academy\utils\scripts\fire_detection_520.py"; DestDir: "{localappdata}\Kneron_Academy\utils\scripts\fire_detection_520.py"; Components: pair1; Flags: ignoreversion recursesubdirs +Source: "C:\Users\mason\AppData\Local\Kneron_Academy\utils\fire detection\yuan\*"; DestDir: "{localappdata}\Kneron_Academy\utils\fire detection\yuan"; Components: pair1; Flags: ignoreversion recursesubdirs createallsubdirs ; pair 2 -Source: "C:\Users\mason\Downloads\Kneron_Academy\utils\models\photo_scorer_520.nef"; DestDir: "{localappdata}\Kneron_Academy\utils\models\photo_scorer_520.nef"; Components: pair2; Flags: ignoreversion recursesubdirs -Source: "C:\Users\mason\Downloads\Kneron_Academy\utils\scripts\photo_quality_520.py"; DestDir: "{localappdata}\Kneron_Academy\utils\scripts\photo_quality_520.py"; Components: pair2; Flags: ignoreversion recursesubdirs - +Source: "C:\Users\mason\AppData\Local\Kneron_Academy\utils\photo quality\ruby\*"; DestDir: "{localappdata}\Kneron_Academy\utils\photo quality\ruby"; Components: pair2; Flags: ignoreversion recursesubdirs createallsubdirs +; pair 3 +Source: "C:\Users\mason\AppData\Local\Kneron_Academy\utils\test mode\test\*"; DestDir: "{localappdata}\Kneron_Academy\utils\test mode\test"; Components: pair3; Flags: ignoreversion recursesubdirs createallsubdirs [Dirs] ; 如有需要隱藏這些資料夾,設定隱藏屬性 -Name: "{localappdata}\Kneron_Academy\utils\scripts"; Attribs: hidden -Name: "{localappdata}\Kneron_Academy\utils\models"; Attribs: hidden +Name: "{localappdata}\Kneron_Academy\utils\fire detection\yuan"; Attribs: hidden +Name: "{localappdata}\Kneron_Academy\utils\photo quality\ruby"; Attribs: hidden +Name: "{localappdata}\Kneron_Academy\utils\test mode\test"; Attribs: hidden [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon - [Run] -Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent \ No newline at end of file diff --git a/src/config.py b/src/config.py index 1bdcfaf..b3eae69 100644 --- a/src/config.py +++ b/src/config.py @@ -4,12 +4,11 @@ APPDATA_PATH = os.environ.get("LOCALAPPDATA") # 取得專案根目錄的絕對路徑並設定 UXUI_ASSETS 為絕對路徑 PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "") -MODEL = os.path.join(APPDATA_PATH,"Kneron_Academy", "utils", "models", "") -SCRIPT = os.path.join(APPDATA_PATH, "Kneron_Academy", "utils", "scripts", "") -SCRIPT_CONFIG = os.path.join(APPDATA_PATH, "Kneron_Academy", "utils", "configs.json") +# 新版路徑結構 (不需要獨立的 models 和 scripts 資料夾) +UTILS_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "utils") +SCRIPT_CONFIG = os.path.join(UTILS_DIR, "config.json") UPLOAD_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "uploads") -FW_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "utils", "firmware") - +FW_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "firmware") # Global Constants APP_NAME = "Innovedus AI Playground" WINDOW_SIZE = (1200, 900) diff --git a/src/views/mainWindows.py b/src/views/mainWindows.py index a9f97d1..dc01712 100644 --- a/src/views/mainWindows.py +++ b/src/views/mainWindows.py @@ -8,8 +8,8 @@ from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal from PyQt5.QtGui import QPixmap, QMovie, QImage from ..config import (UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, SECONDARY_COLOR, - BUTTON_STYLE, MASK_STYLE, PROJECT_ROOT, SCRIPT_CONFIG, SCRIPT, UPLOAD_DIR, - FW_DIR, DongleModelMap, DongleIconMap, MODEL) + BUTTON_STYLE, MASK_STYLE, PROJECT_ROOT, SCRIPT_CONFIG, UTILS_DIR , UPLOAD_DIR, + FW_DIR, DongleModelMap, DongleIconMap) from ..services.device_service import check_available_device @@ -56,8 +56,9 @@ def qimage_to_numpy(qimage): #──────────────────────────────────────────────────────────── # 動態載入 inference 模組的函式 -def load_inference_module(script_path): - module_name = os.path.splitext(os.path.basename(script_path))[0] +def load_inference_module(mode, model_name): + script_path = os.path.join(UTILS_DIR, mode, model_name, "script.py") + module_name = f"{mode}_{model_name}" spec = importlib.util.spec_from_file_location(module_name, script_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) @@ -70,25 +71,29 @@ class InferenceWorkerThread(QThread): # 傳出 inference 結果,型態可依需求調整(例如 dict 或 tuple) inference_result_signal = pyqtSignal(object) - def __init__(self, frame_queue, inference_script_path, min_interval=0.5, mse_threshold=500, once_mode=False): + def __init__(self, frame_queue, mode, model_name, min_interval=0.5, mse_threshold=500, once_mode=False): """ frame_queue: 傳入的 frame 佇列(numpy 陣列) - inference_script_path: inference 模組的檔案路徑 + mode: 模式名稱 (如 'face_recognition') + model_name: 模型名稱 (如 'face_detection') min_interval: 最小 inference 間隔 (秒) mse_threshold: 當前後 frame 之均方誤差低於此值則視為相似 """ super().__init__() self.frame_queue = frame_queue - self.inference_script_path = inference_script_path + self.mode = mode + self.model_name = model_name self.min_interval = min_interval self.mse_threshold = mse_threshold self._running = True - self.once_mode = once_mode # 新增旗標:如果 True,則只做一次推論 + self.once_mode = once_mode self.last_inference_time = 0 self.last_frame = None self.cached_result = None - # 動態載入 inference 模組(假設介面函式為 inference(frame, params)) - self.inference_module = load_inference_module(self.inference_script_path) + + # 動態載入 inference 模組 + script_path = os.path.join(UTILS_DIR, mode, model_name, "script.py") + self.inference_module = load_inference_module(mode, model_name) def run(self): while self._running: @@ -149,12 +154,13 @@ class MainWindow(QWidget): self.video_writer = None self.recorded_frames = [] self.destination = None - # 目前選用的 tool 配置,初始為 None self.current_tool_config = None self.inference_worker = None - # 建立 frame 佇列,限制最大數量 self.inference_queue = queue.Queue(maxsize=10) + # 確保目錄存在並更新配置 + self.generate_global_config() + self.init_ui() def init_ui(self): # 初始化UI (暫時不需要修改) @@ -479,28 +485,59 @@ class MainWindow(QWidget): def select_tool(self, tool_config): print("選擇工具:", tool_config.get("display_name")) self.current_tool_config = tool_config - new_script_path = os.path.join(SCRIPT, tool_config.get("script")) - # 取得工具的 input type + # 獲取模式和模型名稱 + mode = tool_config.get("mode", "") + model_name = tool_config.get("model_name", "") + + # 構建模型路徑 + model_path = os.path.join(UTILS_DIR, mode, model_name) + + # 讀取特定模型的詳細配置 + model_config_path = os.path.join(model_path, "config.json") + detailed_config = {} + + if os.path.exists(model_config_path): + try: + with open(model_config_path, "r", encoding="utf-8") as f: + detailed_config = json.load(f) + # 合併基本配置和詳細配置 + tool_config = {**tool_config, **detailed_config} + except Exception as e: + print(f"Error reading model config: {e}") + + # 獲取工具的輸入類型 input_info = tool_config.get("input_info", {}) tool_type = input_info.get("type", "video") print("type:", tool_type) once_mode = True if tool_type == "image" else False - # 組合 input_params(從 tool_config 中預設值) + # 組合input_params input_params = tool_config.get("input_parameters", {}).copy() - + + # 處理設備相關設定 if hasattr(self, "selected_device") and self.selected_device: input_params["usb_port_id"] = self.selected_device.get("usb_port_id", 0) - # 直接使用設備的 model(在 parse_and_store_devices 已填入) dongle = self.selected_device.get("dongle", "unknown") print("選取的 dongle:", dongle) - # 利用 model 當作子資料夾名稱組合 firmware 路徑 + + # 檢查模型是否支援該設備 + compatible_devices = tool_config.get("compatible_devices", []) + if compatible_devices and dongle not in compatible_devices: + self.show_custom_message( + QMessageBox.Warning, + "設備不兼容", + f"所選模型不支援 {dongle} 設備。\n支援的設備: {', '.join(compatible_devices)}" + ) + return + + # 處理韌體路徑 scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin") ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin") input_params["scpu_path"] = scpu_path input_params["ncpu_path"] = ncpu_path else: + # 預設設備處理邏輯不變 if self.connected_devices and len(self.connected_devices) > 0: input_params["usb_port_id"] = self.connected_devices[0].get("usb_port_id", 0) print("Warning: 沒有特別選取 dongle, 預設使用第一個設備") @@ -508,11 +545,11 @@ class MainWindow(QWidget): input_params["usb_port_id"] = 0 print("Warning: 沒有連接設備, 使用預設 usb_port_id 0") - # 若工具模式需要檔案輸入,則處理 file_path + # 處理檔案輸入 if tool_type in ["image", "voice"]: + # 處理邏輯不變 if hasattr(self, "destination") and self.destination: input_params["file_path"] = self.destination - # 讀取上傳的圖片並推入 inference_queue uploaded_img = cv2.imread(self.destination) if uploaded_img is not None: if not self.inference_queue.full(): @@ -526,12 +563,11 @@ class MainWindow(QWidget): input_params["file_path"] = "" print("Warning: 需要檔案輸入,但尚未上傳檔案。") - # 從 config 讀取 model_info,組合 model 路徑 - if "model_info" in tool_config: - model_name = tool_config["model_info"].get("name", "") - # 假設模型檔案存放在 "src\\utils\\models" 資料夾下,根據需要調整路徑 - model_path = os.path.join(MODEL, model_name) - input_params["model"] = model_path + # 添加模型檔案路徑 + if "model_file" in tool_config: + model_file = tool_config["model_file"] + model_file_path = os.path.join(model_path, model_file) + input_params["model"] = model_file_path print("input_params:", input_params) @@ -540,10 +576,11 @@ class MainWindow(QWidget): self.inference_worker.stop() self.inference_worker = None - # 建立新的 inference worker + # 建立新的 inference worker (使用修改後的參數) self.inference_worker = InferenceWorkerThread( self.inference_queue, - new_script_path, + mode, + model_name, min_interval=0.5, mse_threshold=500, once_mode=once_mode @@ -551,7 +588,7 @@ class MainWindow(QWidget): self.inference_worker.input_params = input_params self.inference_worker.inference_result_signal.connect(self.handle_inference_result) self.inference_worker.start() - print(f"Inference worker 已切換到模組:{new_script_path}") + print(f"Inference worker 已切換到模組:{mode}/{model_name}") if tool_type == "video": self.start_camera() @@ -566,20 +603,21 @@ class MainWindow(QWidget): if os.path.exists(SCRIPT_CONFIG): with open(SCRIPT_CONFIG, "r", encoding="utf-8") as f: config = json.load(f) - tools = config.get("tools", []) - # print("tools: ", tools) + plugins = config.get("plugins", []) else: - print("找不到 toolbox config 檔案,使用空的工具列表") - tools = [] + # 若無配置檔,則嘗試自動生成 + plugins = self.generate_global_config().get("plugins", []) + if not plugins: + print("無法生成配置,使用空的工具列表") - # 建立工具箱介面 + # 創建工具箱UI toolbox_frame = QFrame(self) toolbox_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;") toolbox_frame.setFixedHeight(450) toolbox_frame.setFixedWidth(240) toolbox_layout = QVBoxLayout(toolbox_frame) - # 建立標題列 + # 標題列 title_layout = QHBoxLayout() title_container = QWidget() container_layout = QHBoxLayout(title_container) @@ -596,20 +634,41 @@ class MainWindow(QWidget): title_layout.addWidget(title_container) toolbox_layout.addLayout(title_layout) - # 根據 JSON 配置建立工具按鈕 - for tool in tools: - name = tool.get("display_name", "Unnamed Tool") - button = QPushButton(name) - # 使用 lambda 捕捉 tool 設定,避免 late binding 問題 - button.clicked.connect(lambda checked, t=tool: self.select_tool(t)) - button.setStyleSheet(BUTTON_STYLE) - button.setFixedHeight(40) - toolbox_layout.addWidget(button) + # 建立工具按鈕 (分類顯示) + for plugin in plugins: + mode = plugin.get("mode", "") + display_name = plugin.get("display_name", "") + + # 添加分類標題 + category_label = QLabel(display_name) + category_label.setStyleSheet("color: white; font-size: 16px; font-weight: bold; margin-top: 10px;") + toolbox_layout.addWidget(category_label) + + # 添加該分類下的所有模型按鈕 + for model in plugin.get("models", []): + model_name = model.get("name", "") + display_name = model.get("display_name", "") + + # 建立工具配置 + tool_config = { + "mode": mode, + "model_name": model_name, + "display_name": display_name, + "description": model.get("description", ""), + "compatible_devices": model.get("compatible_devices", []) + } + + # 建立按鈕 + button = QPushButton(display_name) + button.clicked.connect(lambda checked, t=tool_config: self.select_tool(t)) + button.setStyleSheet(BUTTON_STYLE) + button.setFixedHeight(40) + toolbox_layout.addWidget(button) layout.addWidget(toolbox_frame) except Exception as e: print(f"Error in create_ai_toolbox: {e}") - + def create_canvas_area(self): try: # Create frame container for canvas @@ -1141,3 +1200,84 @@ class MainWindow(QWidget): self.overlay.hide() except Exception as e: print(f"Error in hide_device_popup: {e}") + + def generate_global_config(self): + """掃描目錄結構並生成全局配置檔案""" + try: + config = {"plugins": []} + + # 確認 utils 目錄存在 + if not os.path.exists(UTILS_DIR): + os.makedirs(UTILS_DIR, exist_ok=True) + print(f"已建立 UTILS_DIR: {UTILS_DIR}") + + # 列出 utils 目錄下的所有項目以進行偵錯 + print(f"UTILS_DIR 內容: {os.listdir(UTILS_DIR) if os.path.exists(UTILS_DIR) else '目錄不存在'}") + + # 掃描模式目錄(第一層子目錄) + mode_dirs = [d for d in os.listdir(UTILS_DIR) + if os.path.isdir(os.path.join(UTILS_DIR, d)) and not d.startswith('_')] + + print(f"找到的模式目錄: {mode_dirs}") + + for mode_name in mode_dirs: + mode_path = os.path.join(UTILS_DIR, mode_name) + + mode_info = { + "mode": mode_name, + "display_name": mode_name.replace("_", " ").title(), + "models": [] + } + + # 列出該模式目錄下的所有項目以進行偵錯 + print(f"模式 {mode_name} 的內容: {os.listdir(mode_path)}") + + # 掃描模型目錄(第二層子目錄) + model_dirs = [d for d in os.listdir(mode_path) + if os.path.isdir(os.path.join(mode_path, d)) and not d.startswith('_')] + + print(f"在模式 {mode_name} 中找到的模型: {model_dirs}") + + for model_name in model_dirs: + model_path = os.path.join(mode_path, model_name) + + # 檢查模型配置檔案 + model_config_path = os.path.join(model_path, "config.json") + if os.path.exists(model_config_path): + try: + with open(model_config_path, "r", encoding="utf-8") as f: + model_config = json.load(f) + + print(f"已成功讀取模型配置: {model_config_path}") + + model_summary = { + "name": model_name, + "display_name": model_config.get("display_name", model_name.replace("_", " ").title()), + "description": model_config.get("description", ""), + "compatible_devices": model_config.get("compatible_devices", []) + } + + mode_info["models"].append(model_summary) + except Exception as e: + print(f"讀取模型配置時發生錯誤 {model_config_path}: {e}") + else: + print(f"未找到模型配置檔: {model_config_path}") + # 可選:自動創建模板配置檔 + self.create_model_config_template(model_path) + + # 只添加含有模型的模式 + if mode_info["models"]: + config["plugins"].append(mode_info) + + # 寫入配置檔 + os.makedirs(os.path.dirname(SCRIPT_CONFIG), exist_ok=True) + with open(SCRIPT_CONFIG, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2, ensure_ascii=False) + + print(f"全局配置已生成: {SCRIPT_CONFIG}") + return config + except Exception as e: + print(f"生成全局配置時發生錯誤: {e}") + import traceback + traceback.print_exc() + return {"plugins": []} \ No newline at end of file