改成雙層config架構,調整app處理config資料, 增加inno setup的iss file, auto renew global config

This commit is contained in:
Mason Huang 2025-03-07 00:12:14 +08:00
parent 75e5925046
commit a8ed1ac592
4 changed files with 301 additions and 121 deletions

100
README.md
View File

@ -1,26 +1,7 @@
<!-- main page # Kneron Academy v2.0
1. -> example (python code) -> 讀 folder 檔案 -> 選 這個應用程式是一個基於 Python、PyQt5、OpenCV 以及 Kneron SDKkp開發的 AI 應用 APP使用者可以透過鏡頭、麥克風或上傳的方式經由 Kneron NPU 裝置進行實時運算。
-> 抓 camera -> -i image -m model
2. run code -->
<!-- config for python script 需要有以下的資料:
1. function name to display
2. script name
3. device availibility (considering if the script name doesn't consist dongle type)
4. model's name
5. input information (frames, images, voice etc.)
6. input parameters
7. output parameters -->
# Innovedus AI Playground
這個應用程式是一個 AI Playground用於通過相機鏡頭或使用者上傳圖片進行推論例如火災檢測。應用程式基於 Python、PyQt5、OpenCV 以及 Kneron SDKkp開發支援 Video 模式與 Image 模式。
---
## 目錄 ## 目錄
- [安裝設定](#安裝設定) - [安裝設定](#安裝設定)
- [專案架構](#專案架構) - [專案架構](#專案架構)
@ -74,8 +55,9 @@ project/
upload/ upload/
└── photos, videos, or mp3 files └── photos, videos, or mp3 files
utils/ utils/
└── plugins/ ├── config.json
├── mode1/ ├── REAMDE.md
│── mode1/
│ ├── model1/ │ ├── model1/
│ │ ├── script.py │ │ ├── script.py
│ │ ├── model_file(s) │ │ ├── model_file(s)
@ -93,19 +75,17 @@ utils/
├── script.py ├── script.py
├── model_file(s) ├── model_file(s)
└── config.json └── config.json
└──firmware\ firmware\
├── KLXXX/ ├── KLXXX/
│ ├── fw_scpu.bin │ ├── fw_scpu.bin
│ ├── fw_ncpu.bin │ ├── fw_ncpu.bin
│ ├── VERSION │ ├── VERSION
│ └── other files │ └── other files
── KLXXX/ ── KLXXX/
├── fw_scpu.bin ├── fw_scpu.bin
├── fw_ncpu.bin ├── fw_ncpu.bin
├── VERSION ├── VERSION
└── other files └── other files
└──config.json
└──REAMDE.md
``` ```
## 功能概述 ## 功能概述
@ -164,3 +144,69 @@ pyinstaller --onefile --windowed main.py --additional-hooks-dir=hooks --add-data
## APP資料加密 ## APP資料加密
目前預計使用 [pyarmor](https://github.com/dashingsoft/pyarmor) 進行加密 目前預計使用 [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"]
}
]
}
]
}
```

27
dist/test.iss vendored
View File

@ -1,12 +1,10 @@
; Script generated by the Inno Setup Script Wizard. ; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Kneron Academy" #define MyAppName "Kneron Academy"
#define MyAppVersion "2.0" #define MyAppVersion "2.0"
#define MyAppPublisher "Innovedus Inc." #define MyAppPublisher "Innovedus Inc."
#define MyAppURL "https://www.example.com/" #define MyAppURL "https://www.example.com/"
#define MyAppExeName "main.exe" #define MyAppExeName "main.exe"
[Setup] [Setup]
; 唯一的 AppId請勿在其他應用程式中重複使用 ; 唯一的 AppId請勿在其他應用程式中重複使用
AppId={{0894596D-D78B-4D8C-97CC-D90FE98E26E0}} AppId={{0894596D-D78B-4D8C-97CC-D90FE98E26E0}}
@ -28,37 +26,34 @@ PrivilegesRequired=lowest
OutputBaseFilename=mysetup OutputBaseFilename=mysetup
SolidCompression=yes SolidCompression=yes
WizardStyle=modern WizardStyle=modern
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks] [Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
; 定義配對元件,讓使用者選擇是否安裝預設的 Script 與 Model ; 定義配對元件,讓使用者選擇是否安裝預設的 Script 與 Model
[Components] [Components]
Name: "pair1"; Description: "Fire Detection"; Name: "pair1"; Description: "Fire Detection";
Name: "pair2"; Description: "Photo_quality"; Name: "pair2"; Description: "Photo_quality";
Name: "pair3"; Description: "Test Mode";
[Files] [Files]
; 安裝主要執行檔到 {app} 目錄 ; 安裝主要執行檔到 {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 --- ; --- 配對1 ---
; pair 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\AppData\Local\Kneron_Academy\utils\fire detection\yuan\*"; DestDir: "{localappdata}\Kneron_Academy\utils\fire detection\yuan"; Components: pair1; Flags: ignoreversion recursesubdirs createallsubdirs
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
; pair 2 ; 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\AppData\Local\Kneron_Academy\utils\photo quality\ruby\*"; DestDir: "{localappdata}\Kneron_Academy\utils\photo quality\ruby"; Components: pair2; Flags: ignoreversion recursesubdirs createallsubdirs
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 ; 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] [Dirs]
; 如有需要隱藏這些資料夾,設定隱藏屬性 ; 如有需要隱藏這些資料夾,設定隱藏屬性
Name: "{localappdata}\Kneron_Academy\utils\scripts"; Attribs: hidden Name: "{localappdata}\Kneron_Academy\utils\fire detection\yuan"; Attribs: hidden
Name: "{localappdata}\Kneron_Academy\utils\models"; Attribs: hidden Name: "{localappdata}\Kneron_Academy\utils\photo quality\ruby"; Attribs: hidden
Name: "{localappdata}\Kneron_Academy\utils\test mode\test"; Attribs: hidden
[Icons] [Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run] [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

View File

@ -4,12 +4,11 @@ APPDATA_PATH = os.environ.get("LOCALAPPDATA")
# 取得專案根目錄的絕對路徑並設定 UXUI_ASSETS 為絕對路徑 # 取得專案根目錄的絕對路徑並設定 UXUI_ASSETS 為絕對路徑
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "") UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "")
MODEL = os.path.join(APPDATA_PATH,"Kneron_Academy", "utils", "models", "") # 新版路徑結構 (不需要獨立的 models 和 scripts 資料夾)
SCRIPT = os.path.join(APPDATA_PATH, "Kneron_Academy", "utils", "scripts", "") UTILS_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "utils")
SCRIPT_CONFIG = os.path.join(APPDATA_PATH, "Kneron_Academy", "utils", "configs.json") SCRIPT_CONFIG = os.path.join(UTILS_DIR, "config.json")
UPLOAD_DIR = os.path.join(APPDATA_PATH, "Kneron_Academy", "uploads") 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 # Global Constants
APP_NAME = "Innovedus AI Playground" APP_NAME = "Innovedus AI Playground"
WINDOW_SIZE = (1200, 900) WINDOW_SIZE = (1200, 900)

View File

@ -8,8 +8,8 @@ from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal
from PyQt5.QtGui import QPixmap, QMovie, QImage from PyQt5.QtGui import QPixmap, QMovie, QImage
from ..config import (UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, SECONDARY_COLOR, from ..config import (UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, SECONDARY_COLOR,
BUTTON_STYLE, MASK_STYLE, PROJECT_ROOT, SCRIPT_CONFIG, SCRIPT, UPLOAD_DIR, BUTTON_STYLE, MASK_STYLE, PROJECT_ROOT, SCRIPT_CONFIG, UTILS_DIR , UPLOAD_DIR,
FW_DIR, DongleModelMap, DongleIconMap, MODEL) FW_DIR, DongleModelMap, DongleIconMap)
from ..services.device_service import check_available_device from ..services.device_service import check_available_device
@ -56,8 +56,9 @@ def qimage_to_numpy(qimage):
#──────────────────────────────────────────────────────────── #────────────────────────────────────────────────────────────
# 動態載入 inference 模組的函式 # 動態載入 inference 模組的函式
def load_inference_module(script_path): def load_inference_module(mode, model_name):
module_name = os.path.splitext(os.path.basename(script_path))[0] 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) spec = importlib.util.spec_from_file_location(module_name, script_path)
module = importlib.util.module_from_spec(spec) module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) spec.loader.exec_module(module)
@ -70,25 +71,29 @@ class InferenceWorkerThread(QThread):
# 傳出 inference 結果,型態可依需求調整(例如 dict 或 tuple # 傳出 inference 結果,型態可依需求調整(例如 dict 或 tuple
inference_result_signal = pyqtSignal(object) 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 陣列) frame_queue: 傳入的 frame 佇列(numpy 陣列)
inference_script_path: inference 模組的檔案路徑 mode: 模式名稱 ( 'face_recognition')
model_name: 模型名稱 ( 'face_detection')
min_interval: 最小 inference 間隔 () min_interval: 最小 inference 間隔 ()
mse_threshold: 當前後 frame 之均方誤差低於此值則視為相似 mse_threshold: 當前後 frame 之均方誤差低於此值則視為相似
""" """
super().__init__() super().__init__()
self.frame_queue = frame_queue 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.min_interval = min_interval
self.mse_threshold = mse_threshold self.mse_threshold = mse_threshold
self._running = True self._running = True
self.once_mode = once_mode # 新增旗標:如果 True則只做一次推論 self.once_mode = once_mode
self.last_inference_time = 0 self.last_inference_time = 0
self.last_frame = None self.last_frame = None
self.cached_result = 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): def run(self):
while self._running: while self._running:
@ -149,12 +154,13 @@ class MainWindow(QWidget):
self.video_writer = None self.video_writer = None
self.recorded_frames = [] self.recorded_frames = []
self.destination = None self.destination = None
# 目前選用的 tool 配置,初始為 None
self.current_tool_config = None self.current_tool_config = None
self.inference_worker = None self.inference_worker = None
# 建立 frame 佇列,限制最大數量
self.inference_queue = queue.Queue(maxsize=10) self.inference_queue = queue.Queue(maxsize=10)
# 確保目錄存在並更新配置
self.generate_global_config()
self.init_ui() self.init_ui()
def init_ui(self): # 初始化UI (暫時不需要修改) def init_ui(self): # 初始化UI (暫時不需要修改)
@ -479,28 +485,59 @@ class MainWindow(QWidget):
def select_tool(self, tool_config): def select_tool(self, tool_config):
print("選擇工具:", tool_config.get("display_name")) print("選擇工具:", tool_config.get("display_name"))
self.current_tool_config = tool_config 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", {}) input_info = tool_config.get("input_info", {})
tool_type = input_info.get("type", "video") tool_type = input_info.get("type", "video")
print("type:", tool_type) print("type:", tool_type)
once_mode = True if tool_type == "image" else False once_mode = True if tool_type == "image" else False
# 組合 input_params從 tool_config 中預設值) # 組合input_params
input_params = tool_config.get("input_parameters", {}).copy() input_params = tool_config.get("input_parameters", {}).copy()
# 處理設備相關設定
if hasattr(self, "selected_device") and self.selected_device: if hasattr(self, "selected_device") and self.selected_device:
input_params["usb_port_id"] = self.selected_device.get("usb_port_id", 0) 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") dongle = self.selected_device.get("dongle", "unknown")
print("選取的 dongle:", dongle) 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") scpu_path = os.path.join(FW_DIR, dongle, "fw_scpu.bin")
ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin") ncpu_path = os.path.join(FW_DIR, dongle, "fw_ncpu.bin")
input_params["scpu_path"] = scpu_path input_params["scpu_path"] = scpu_path
input_params["ncpu_path"] = ncpu_path input_params["ncpu_path"] = ncpu_path
else: else:
# 預設設備處理邏輯不變
if self.connected_devices and len(self.connected_devices) > 0: if self.connected_devices and len(self.connected_devices) > 0:
input_params["usb_port_id"] = self.connected_devices[0].get("usb_port_id", 0) input_params["usb_port_id"] = self.connected_devices[0].get("usb_port_id", 0)
print("Warning: 沒有特別選取 dongle, 預設使用第一個設備") print("Warning: 沒有特別選取 dongle, 預設使用第一個設備")
@ -508,11 +545,11 @@ class MainWindow(QWidget):
input_params["usb_port_id"] = 0 input_params["usb_port_id"] = 0
print("Warning: 沒有連接設備, 使用預設 usb_port_id 0") print("Warning: 沒有連接設備, 使用預設 usb_port_id 0")
# 若工具模式需要檔案輸入,則處理 file_path # 處理檔案輸入
if tool_type in ["image", "voice"]: if tool_type in ["image", "voice"]:
# 處理邏輯不變
if hasattr(self, "destination") and self.destination: if hasattr(self, "destination") and self.destination:
input_params["file_path"] = self.destination input_params["file_path"] = self.destination
# 讀取上傳的圖片並推入 inference_queue
uploaded_img = cv2.imread(self.destination) uploaded_img = cv2.imread(self.destination)
if uploaded_img is not None: if uploaded_img is not None:
if not self.inference_queue.full(): if not self.inference_queue.full():
@ -526,12 +563,11 @@ class MainWindow(QWidget):
input_params["file_path"] = "" input_params["file_path"] = ""
print("Warning: 需要檔案輸入,但尚未上傳檔案。") print("Warning: 需要檔案輸入,但尚未上傳檔案。")
# 從 config 讀取 model_info組合 model 路徑 # 添加模型檔案路徑
if "model_info" in tool_config: if "model_file" in tool_config:
model_name = tool_config["model_info"].get("name", "") model_file = tool_config["model_file"]
# 假設模型檔案存放在 "src\\utils\\models" 資料夾下,根據需要調整路徑 model_file_path = os.path.join(model_path, model_file)
model_path = os.path.join(MODEL, model_name) input_params["model"] = model_file_path
input_params["model"] = model_path
print("input_params:", input_params) print("input_params:", input_params)
@ -540,10 +576,11 @@ class MainWindow(QWidget):
self.inference_worker.stop() self.inference_worker.stop()
self.inference_worker = None self.inference_worker = None
# 建立新的 inference worker # 建立新的 inference worker (使用修改後的參數)
self.inference_worker = InferenceWorkerThread( self.inference_worker = InferenceWorkerThread(
self.inference_queue, self.inference_queue,
new_script_path, mode,
model_name,
min_interval=0.5, min_interval=0.5,
mse_threshold=500, mse_threshold=500,
once_mode=once_mode once_mode=once_mode
@ -551,7 +588,7 @@ class MainWindow(QWidget):
self.inference_worker.input_params = input_params self.inference_worker.input_params = input_params
self.inference_worker.inference_result_signal.connect(self.handle_inference_result) self.inference_worker.inference_result_signal.connect(self.handle_inference_result)
self.inference_worker.start() self.inference_worker.start()
print(f"Inference worker 已切換到模組:{new_script_path}") print(f"Inference worker 已切換到模組:{mode}/{model_name}")
if tool_type == "video": if tool_type == "video":
self.start_camera() self.start_camera()
@ -566,20 +603,21 @@ class MainWindow(QWidget):
if os.path.exists(SCRIPT_CONFIG): if os.path.exists(SCRIPT_CONFIG):
with open(SCRIPT_CONFIG, "r", encoding="utf-8") as f: with open(SCRIPT_CONFIG, "r", encoding="utf-8") as f:
config = json.load(f) config = json.load(f)
tools = config.get("tools", []) plugins = config.get("plugins", [])
# print("tools: ", tools)
else: else:
print("找不到 toolbox config 檔案,使用空的工具列表") # 若無配置檔,則嘗試自動生成
tools = [] plugins = self.generate_global_config().get("plugins", [])
if not plugins:
print("無法生成配置,使用空的工具列表")
# 建立工具箱介面 # 創建工具箱UI
toolbox_frame = QFrame(self) toolbox_frame = QFrame(self)
toolbox_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;") toolbox_frame.setStyleSheet(f"border: none; background: {SECONDARY_COLOR}; border-radius: 15px;")
toolbox_frame.setFixedHeight(450) toolbox_frame.setFixedHeight(450)
toolbox_frame.setFixedWidth(240) toolbox_frame.setFixedWidth(240)
toolbox_layout = QVBoxLayout(toolbox_frame) toolbox_layout = QVBoxLayout(toolbox_frame)
# 建立標題列 # 標題列
title_layout = QHBoxLayout() title_layout = QHBoxLayout()
title_container = QWidget() title_container = QWidget()
container_layout = QHBoxLayout(title_container) container_layout = QHBoxLayout(title_container)
@ -596,12 +634,33 @@ class MainWindow(QWidget):
title_layout.addWidget(title_container) title_layout.addWidget(title_container)
toolbox_layout.addLayout(title_layout) toolbox_layout.addLayout(title_layout)
# 根據 JSON 配置建立工具按鈕 # 建立工具按鈕 (分類顯示)
for tool in tools: for plugin in plugins:
name = tool.get("display_name", "Unnamed Tool") mode = plugin.get("mode", "")
button = QPushButton(name) display_name = plugin.get("display_name", "")
# 使用 lambda 捕捉 tool 設定,避免 late binding 問題
button.clicked.connect(lambda checked, t=tool: self.select_tool(t)) # 添加分類標題
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.setStyleSheet(BUTTON_STYLE)
button.setFixedHeight(40) button.setFixedHeight(40)
toolbox_layout.addWidget(button) toolbox_layout.addWidget(button)
@ -1141,3 +1200,84 @@ class MainWindow(QWidget):
self.overlay.hide() self.overlay.hide()
except Exception as e: except Exception as e:
print(f"Error in hide_device_popup: {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": []}