Compare commits

..

3 Commits

Author SHA1 Message Date
69b25f89e2 Merge branch 'main' of https://gitea.innovedus.com/abin/KNEO-Academy 2026-04-07 14:38:27 +08:00
1e42293896 Add autoflow 2026-04-07 14:37:04 +08:00
7e323cf3e1 Fix: resolve 5 bugs found during project onboarding health check
- custom_inference_worker: reuse existing device_group from DeviceController
  to avoid double kp.connect_devices() conflict on same USB port
- custom_inference_worker: add TYPE_CHECKING guard for kp type annotations
  to prevent potential NameError at import time
- utilities_screen: replace missing back_arrow.png with text arrow (←)
- utilities_screen: add set_device_controller() so AppController can inject
  MainWindow's shared DeviceController instance
- main.py: wire UtilitiesScreen to share MainWindow's DeviceController
- video_thread: emit camera_error_signal on failure and max-retry exhaustion
- media_controller: connect camera_error_signal and display error on canvas
- media_panel: fix pause button using wrong delete icon; use video_normal SVG

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:33:37 +08:00
13 changed files with 3185 additions and 19 deletions

View File

@ -0,0 +1,173 @@
# 專案健檢報告 — KNEO Academy
## 基本資訊
- **專案名稱**KNEO AcademyInnovedus AI Playground
- **版本**v2.0
- **程式碼來源**:本地路徑 `C:\Users\sungs\Documents\abin\KNEO-Academy`
- **主要語言**Python 3.12
- **最後更新時間**2026-04-04
- **Git branch**main
---
## 技術堆疊
| 層級 | 技術 | 版本 |
|------|------|------|
| GUI 框架 | PyQt5 | 5.15.11 |
| 電腦視覺 | OpenCV | 4.10.0.84 |
| AI 推論 SDK | KneronPLUS | 3.1.2 |
| AI 推論(通用) | PyTorch, TensorFlow, ONNX Runtime | 最新 |
| 音訊 | librosa, sounddevice | — |
| 打包 | PyInstaller | 6.12.0 |
| 加密 | PyArmor | — |
---
## 專案結構概覽
```
KNEO-Academy/
├── main.py # Entry pointAppControllerQStackedWidget 管理頁面)
├── src/
│ ├── config.py # 全域常數、路徑、顏色、DeviceType enum
│ ├── controllers/
│ │ ├── device_controller.py # Kneron dongle 掃描、連接、韌體上傳
│ │ ├── inference_controller.py # 推論工具選擇、model 載入、queue 管理
│ │ └── media_controller.py # 相機、影片捕捉
│ ├── models/
│ │ ├── inference_worker.py # 標準推論 worker thread動態載入 script.py
│ │ ├── custom_inference_worker.py # 自定義模型推論 workerYOLOv5 後處理)
│ │ └── video_thread.py # 相機影像擷取 thread
│ ├── views/
│ │ ├── mainWindows.py # 主應用視窗(推論結果顯示、工具選擇)
│ │ ├── selection_screen.py # 首頁選擇畫面
│ │ ├── login_screen.py # 工具程式登入頁
│ │ └── utilities_screen.py # 裝置管理工具頁
│ ├── services/
│ │ ├── device_service.py # 掃描 Kneron 裝置(含 timeout 機制)
│ │ ├── file_service.py # 檔案上傳服務
│ │ └── script_service.py # 推論腳本執行服務
│ └── utils/
│ ├── config_utils.py # 設定工具
│ └── image_utils.py # QImage ↔ NumPy 轉換
├── uxui/ # 靜態 UI 資源PNG、SVG、GIF
├── dist/
│ └── test.iss # Inno Setup 安裝包設定
├── flowchart.md # 裝置連接流程設計圖Mermaid
└── env.txt # pip 套件清單
```
**資料目錄(執行期,存在 %LOCALAPPDATA%/Kneron_Academy/**
```
uploads/ # 使用者上傳的圖片/影片
utils/
config.json # 全域 plugin 設定
{mode}/{model}/
script.py # 推論腳本
config.json # 模型設定
*.nef # 模型檔
firmware/
{device}/
fw_scpu.bin
fw_ncpu.bin
```
---
## 應用頁面流程
```
SelectionScreen首頁
├── → LoginScreen → UtilitiesScreen裝置管理工具需登入
└── → MainWindowDemo AI App直接進入
```
---
## 主要功能清單
| 功能 | 描述 | 狀態 |
|------|------|------|
| 頁面路由 | QStackedWidget 管理多頁面切換 | ✅ 完成 |
| 裝置掃描 | 掃描連接的 Kneron dongleKL520 / KL720 | ✅ 完成 |
| 裝置連接 | 連接裝置並上傳 firmware | ✅ 完成 |
| 裝置中斷 | 安全中斷連接 | ✅ 完成 |
| Video 推論 | 相機即時推論QThread + queue | ✅ 完成 |
| Image 推論 | 上傳圖片單次推論 | ✅ 完成 |
| 動態 Script 載入 | 從 utils/ 目錄動態 import script.py | ✅ 完成 |
| 自定義模型推論 | 上傳 .nef + firmware使用 YOLOv5 後處理 | ✅ 完成 |
| Plugin 系統 | 透過 config.json 定義 mode/model 結構 | ✅ 完成 |
| APP 打包 | PyInstaller 打包 + Inno Setup 安裝包 | ✅ 有設定 |
| 登入驗證 | Server 驗證(詳細流程見 flowchart.md | 🔄 設計中(有 flowchart |
| Dongle 授權管理 | KN 號碼查詢、授權卡驗證 | 🔄 設計中(有 flowchart |
---
## 文件完整度
| 文件類型 | 狀態 | 位置 | 備註 |
|---------|------|------|------|
| README | ✅ 有 | `README.md` | 完整,含安裝、架構、功能說明 |
| 流程圖 | ⚠️ 部分 | `flowchart.md` | 僅有裝置連接/授權流程,缺主 App 完整流程 |
| PRD / 需求文件 | ❌ 無 | — | 無正式產品需求文件 |
| 架構設計文件 | ❌ 無 | — | 無正式架構文件 |
| API 文件 | ❌ 無 | — | 無script.py 介面未有規格文件) |
| 設計稿 | ⚠️ 部分 | `uxui/` | 有 UI 資源圖,無 Wireframe 或設計規格 |
| TDD技術設計文件 | ❌ 無 | — | 無 |
| 測試文件 | ❌ 無 | — | README 提到 tests/ 目錄,但實際不存在 |
| 部署文件 | ⚠️ 部分 | `dist/test.iss` | 有 Inno Setup 設定,無完整部署指南 |
---
## 程式碼健康度
| 項目 | 狀態 | 備註 |
|------|------|------|
| 測試覆蓋率 | ❌ 無測試 | tests/ 目錄不存在 |
| 程式碼組織 | ✅ 良好 | MVC 架構清晰,職責分明 |
| Docstring 完整度 | ✅ 良好 | 主要類別和方法均有完整 docstring |
| 錯誤處理 | ⚠️ 部分 | 多處使用 try/except但部分只 print 不處理 |
| Thread 安全 | ⚠️ 待確認 | Queue 管理有,但 UI 更新路徑需確認 |
| 技術債 | ⚠️ 少量 | debug print 語句散落在 controller 中 |
---
## 基礎設施
| 項目 | 狀態 | 備註 |
|------|------|------|
| Docker | ❌ 無 | 桌面應用,暫不需要 |
| CI/CD | ❌ 無 | 無自動化建置/測試流程 |
| 打包 | ✅ 有 | PyInstaller + Inno Setup |
| 加密 | ⚠️ 計畫中 | PyArmor 已列在計畫中 |
| 監控 | ❌ 無 | 無 |
---
## 目前正在修改的檔案git status
以下檔案有未提交的修改:
- `src/controllers/device_controller.py`
- `src/controllers/inference_controller.py`
- `src/models/custom_inference_worker.py`
- `src/models/video_thread.py`
- `src/services/device_service.py`
- `src/views/utilities_screen.py`
---
## 缺失項目摘要
**高優先:**
- PRD / 需求文件(產品功能邊界不清晰)
- 測試(完全沒有自動化測試)
**中優先:**
- 架構文件 / TDDplugin script.py 介面規格未文件化)
- 完整流程圖(主 App 推論流程尚未有流程圖)
**低優先:**
- 部署完整指南
- 設計規格文件Wireframe + Design Tokens

625
.autoflow/02-prd/PRD.md Normal file
View File

@ -0,0 +1,625 @@
# KNEO AcademyInnovedus AI Playgroundv2.0 — 產品需求文件PRD
> **文件性質說明**:本 PRD 為從既有程式碼與文件反向整理而成,已對照 README.md、flowchart.md、主要 controller、view 和 service 原始碼推導。標注「⚠️ 待確認」的項目為推斷內容,請產品負責人核實。
---
## 1. 產品概述
### 1.1 產品定位
**KNEO Academy**(對外名稱:**Innovedus AI Playground**)是一款 Windows 桌面應用程式,讓擁有 Kneron NPU USB Dongle 硬體的客戶能夠在本機端執行 AI 推論,無需雲端服務、無需自行撰寫程式碼。
**核心價值主張**
- 即插即用的 Edge AI 體驗——插上 Kneron dongle 即可執行 AI 模型
- 支援多種推論模式(即時攝影機、圖片上傳、自定義模型)
- 透過 Plugin 系統讓第三方或 Kneron 官方模型能被彈性載入
- 提供硬體管理工具(韌體更新、驅動安裝、裝置授權)
### 1.2 目標使用者
**主要使用者Persona A企業/研發客戶**
- 角色:使用 Kneron NPU 硬體進行 AI PoC 或產品評估的工程師、研究人員
- 目標:快速驗證 Kneron NPU 在特定任務上的推論效果
- 技術素養:中等(懂 AI 概念,不一定熟悉 SDK 整合)
- 痛點Kneron SDK 入門門檻高,想要一個能快速展示能力的工具
**次要使用者Persona B內部 Demo / 業務展示**
- 角色Kneron / Innovedus 業務或技術支援人員
- 目標:向潛在客戶展示 Kneron NPU 的 AI 推論能力
- 技術素養:低至中等
- 痛點:需要一個無需開發、開箱即用的展示工具
**⚠️ 待確認**:上述 Persona 定義是從產品功能推斷。實際目標客群請與產品負責人確認。
### 1.3 應用名稱對照
| 名稱 | 用途 |
|------|------|
| KNEO Academy | 專案內部/開發用名稱 |
| Innovedus AI Playground | 對外正式名稱(`APP_NAME` 常數定義) |
---
## 2. 支援的硬體裝置
應用程式目前支援以下 Kneron NPU Dongle 型號(來自 `config.py``DeviceType` Enum
| 裝置型號 | Product IDHex | 支援狀態 |
|---------|-----------------|---------|
| KL520 | 0x100 | ✅ 已支援(有 icon、model map |
| KL720 | 0x720 | ✅ 已支援(有 icon、model map |
| KL720_L | 0x200 | ⚠️ Enum 有定義,但無 DongleModelMap 對應,**可能不完整** |
| KL530 | — | ⚠️ Enum 有定義,但無 DongleModelMap 對應 |
| KL832 | — | ⚠️ Enum 有定義,但無 DongleModelMap 對應 |
| KL730 | — | ⚠️ Enum 有定義,但無 DongleModelMap 對應 |
| KL630 | — | ⚠️ Enum 有定義,但無 DongleModelMap 對應 |
| KL540 | — | ⚠️ Enum 有定義,但無 DongleModelMap 對應 |
**⚠️ 待確認**KL520 和 KL720 以外的裝置是否已在計畫中支援?`DongleModelMap` 僅有這兩型。
---
## 3. 應用程式頁面架構
### 3.1 頁面流程圖
```
應用程式啟動
SelectionScreen首頁 / 入口選擇畫面)
├── [Demo App 按鈕] ──────────────────→ MainWindowAI Demo 推論主視窗)
└── [Utilities 按鈕] → LoginScreen登入畫面
├── [登入成功] → UtilitiesScreen裝置管理工具
└── [返回] → SelectionScreen
```
### 3.2 各頁面功能摘要
| 頁面 | Class 名稱 | 主要用途 | 需要登入 |
|------|-----------|---------|---------|
| 首頁 | `SelectionScreen` | 選擇進入 Demo App 或 Utilities 工具 | 否 |
| 登入畫面 | `LoginScreen` | Server 驗證,作為進入 Utilities 的門禁 | — |
| 裝置管理工具 | `UtilitiesScreen` | 裝置掃描、韌體更新、驅動安裝、已購項目 | 是 |
| AI Demo 主視窗 | `MainWindow` | AI 推論執行、攝影機顯示、工具選擇 | 否 |
---
## 4. 功能需求
### 4.1 SelectionScreen首頁選擇畫面
**功能描述**:應用程式的入口,提供兩個主要路徑。
**功能清單**
- 顯示應用程式 Logo 與名稱
- 「Demo App」按鈕直接進入 AI 推論主視窗(不需登入)
- 「Utilities」按鈕跳轉到登入畫面登入後才能進入裝置管理工具
**⚠️ 待確認**:首頁是否有其他 UI 元素(介紹文字、版本號等)?
---
### 4.2 LoginScreen登入畫面
**功能描述**:透過 Server 驗證機制保護 Utilities 功能的存取。
**功能清單**
- 帳號密碼輸入欄位
- 登入按鈕 → 觸發 Server 驗證
- 返回按鈕 → 回到 SelectionScreen
**登入結果處理**
| 情境 | 行為 |
|------|------|
| 驗證成功 | 發出 `login_success` signal → 進入 UtilitiesScreen |
| 帳密錯誤 | 顯示「無效用戶名或密碼」提示 |
| 多次錯誤 | Server 停止服務,顯示提示訊息 |
| 返回 | 發出 `back_to_selection` signal → 回到 SelectionScreen |
**⚠️ 待確認**
- 登入 Server 的端點 URL 在哪裡設定?(程式碼中未見明確設定)
- 是否有記住密碼/Token 快取機制?
---
### 4.3 UtilitiesScreen裝置管理工具頁面
**功能描述**:登入後才能存取的裝置管理介面,包含兩個子頁面。
#### 4.3.1 Utilities 子頁面(裝置管理)
**功能清單**
**裝置掃描與列表**
- 掃描當前連接的 Kneron dongle 裝置(呼叫 `check_available_device()`,含 5 秒 timeout
- 以表格(`QTableWidget`顯示裝置清單欄位包含裝置型號、KN Number、狀態
- 「Refresh」按鈕重新掃描裝置
- 未偵測到裝置時顯示提示
**裝置連接操作**
- 選擇裝置後可執行連接(透過 `DeviceController.connect_device()`
- 連接過程:
1. 驗證 firmware 檔案是否存在(`%LOCALAPPDATA%/Kneron_Academy/firmware/{device}/`
2. 呼叫 `kp.core.connect_devices()`
3. 上傳 firmware`fw_scpu.bin` + `fw_ncpu.bin`
- 中斷連接按鈕(`DeviceController.disconnect_device()`
**Firmware 管理**
- 顯示目前 Firmware 版本
- 偵測到版本需要更新時,提示使用者是否更新
- 支援從本機載入 firmware 檔案進行更新
**Driver 管理**(⚠️ 設計中,尚未完整實作)
- 偵測 Kneron USB driver 是否已安裝
- 尚未安裝時:提示安裝,詢問使用者是否執行安裝程序
- 安裝失敗處理:顯示錯誤訊息
**Dongle 授權管理**(⚠️ 設計中,尚未完整實作)
- 查詢 KN NumberDongle 唯一識別號)
- 授權卡Authorization Card驗證流程
- 已授權裝置:顯示已授權提示
- 未授權裝置:詢問是否啟動授權,執行授權流程
- **詳細流程見 `flowchart.md`**
**狀態顯示**
- `QProgressBar`:顯示操作進度(連接、韌體上傳等)
- `QLabel`status_label顯示目前狀態訊息
#### 4.3.2 Purchased Items 子頁面(已購項目)
**功能描述**:顯示用戶已購買的 AI 模型,可以下載到本機。
**功能清單**
- 表格顯示已購買項目欄位Select勾選、Product產品名、Model模型名、Current Version版本、Compatible Dongles相容裝置
- 多選Checkbox功能
- 「Refresh Items」按鈕重新從 Server 取得已購列表
- 「Download Selected」按鈕下載所選項目到本機
**⚠️ 待確認**
- 目前程式碼中有 `populate_mock_purchased_items()` 方法,顯示此功能使用 Mock 資料,**尚未對接真實 API**
- 下載後的模型存放路徑?(推斷為 `%LOCALAPPDATA%/Kneron_Academy/utils/`
- 購買平台/Store 是否已確定?
---
### 4.4 MainWindowAI Demo 推論主視窗)
**功能描述**:核心的 AI 推論展示視窗,整合攝影機顯示、工具選擇與推論結果呈現。
#### 4.4.1 畫面布局組件
| 組件 | Class 名稱 | 功能 |
|------|-----------|------|
| 攝影機顯示區 | `CanvasArea` | 顯示攝影機畫面和推論結果Bounding Box 等) |
| 裝置清單 | `DeviceList` | 顯示可連接的 dongle讓使用者選擇 |
| 裝置連接彈窗 | `DevicePopup` | 彈出式裝置連接確認介面 |
| 媒體控制面板 | `MediaPanel` | 攝影機開啟/暫停、圖片上傳按鈕 |
| AI 工具箱 | `Toolbox` | 顯示可選擇的 AI 模型(來自 config.json |
| 自定義模型上傳 | `CustomModelBlock` | 上傳自定義 .nef 模型的 UI |
#### 4.4.2 AI 推論模式
##### 模式一Video 模式(即時攝影機推論)
**流程**
1. 使用者選擇 Video 類型的 AI 工具
2. 呼叫 `MediaController.start_camera()` 啟動相機
3. `VideoThread` 持續擷取影像幀640×480, 30fps轉為 QImage 發出 signal
4. 影像幀轉為 NumPy 陣列後,透過 `InferenceController.add_frame_to_queue()` 放入推論 queue最大容量5 幀)
5. `InferenceWorkerThread` 從 queue 取出影像幀,呼叫對應的 `script.py` 執行推論
6. 推論結果透過 `inference_result_signal` 傳回主視窗
7. 主視窗在 `CanvasArea` 上繪製 Bounding Box 等視覺化結果
**效能優化機制**
- MSEMean Squared Error幀差異偵測若連續幀的 MSE 低於 `mse_threshold=500`,沿用上一次推論結果(避免重複計算靜止畫面)
- 最短推論間隔:`min_interval=2` 秒(標準模式)
- 模型超時設定:`MODEL_TIMEOUT = 5000` ms`config.py`
##### 模式二Image 模式(上傳圖片單次推論)
**流程**
1. 使用者選擇 Image 類型的 AI 工具
2. 使用者透過 `MediaPanel` 上傳圖片(存入 `%LOCALAPPDATA%/Kneron_Academy/uploads/`
3. 使用 `cv2.imread()` 讀取圖片,放入推論 queue`once_mode=True`,只推論一次)
4. `InferenceWorkerThread` 執行推論並回傳結果
5. 切換到 Image 模式時,相機信號會暫時斷開(非停止),以避免 Video 模式的幀繼續進入 queue
**注意**:切回 Video 工具時,相機信號會重新連接,無需重新開啟相機。
##### 模式三Custom Model使用者自定義模型
**流程**
1. 使用者透過 `CustomModelBlock` 上傳以下三個檔案:
- `.nef` 模型檔
- `fw_scpu.bin`SCPU firmware
- `fw_ncpu.bin`NCPU firmware
- 可選自定義標籤清單class labels
2. `InferenceController.select_custom_tool()` 被呼叫
3. 啟動 `CustomInferenceWorkerThread`(與標準 worker 不同的實作)
4. Custom worker 會在首次推論時自行完成:
- 連接裝置(`kp.core.connect_devices()`
- 上傳 firmware
- 上傳模型(`kp.core.load_model_from_file()`
5. 預處理:影像縮放至 640×640轉為 BGR565 格式
6. 後處理:使用 YOLOv5 演算法NMS threshold=0.5)解析輸出
7. 若有提供自定義標籤,使用自定義標籤;否則使用預設 COCO 80 類別
**Custom Model 預設行為**
- 輸入類型Video即時攝影機
- 後處理演算法:固定使用 YOLOv5**⚠️ 待確認**:是否計畫支援其他後處理演算法?)
- 預設偵測閾值:`thresh=0.2`
- 推論間隔:`min_interval=0.5` 秒(比標準模式更頻繁)
#### 4.4.3 裝置相容性檢查
當使用者選擇 AI 工具時:
- 讀取 `config.json` 中的 `compatible_devices` 欄位
- 比對目前選中的 dongle 型號
- 若不相容,彈出警告對話框並阻止推論啟動
---
### 4.5 Plugin 系統Script & Model 配置)
**功能描述**:透過設定檔驅動的 Plugin 架構,讓 AI 模型能被動態載入,無需修改應用程式原始碼。
#### 4.5.1 檔案結構
```
%LOCALAPPDATA%/Kneron_Academy/
├── uploads/ # 使用者上傳的圖片/影片
├── utils/
│ ├── config.json # 全域 plugin 設定(必要)
│ └── {mode}/ # 推論模式資料夾e.g., face_recognition
│ └── {model}/ # 模型資料夾e.g., face_detection
│ ├── config.json # 模型設定(必要)
│ ├── script.py # 推論腳本(必要)
│ └── *.nef # 模型檔(必要)
└── firmware/
└── {device}/ # 裝置型號資料夾e.g., KL520
├── fw_scpu.bin
└── fw_ncpu.bin
```
#### 4.5.2 全域 config.json 格式
```json
{
"plugins": [
{
"mode": "face_recognition",
"display_name": "人臉辨識",
"models": [
{
"name": "face_detection",
"display_name": "人臉偵測 (ResNet-18)",
"description": "基於ResNet-18的高精度人臉偵測",
"compatible_devices": ["KL520", "KL720"]
}
]
}
]
}
```
#### 4.5.3 模型 config.json 格式
```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"]
}
```
**`input_info.type` 的有效值**
- `"video"` — 啟動攝影機進行即時推論
- `"image"` — 使用者上傳圖片進行單次推論
- `"voice"` — 音訊輸入模式(⚠️ **程式碼中有對應處理路徑但未見完整實作**
#### 4.5.4 script.py 介面規範
`InferenceWorkerThread` 會動態 import 各模型的 `script.py`
**⚠️ 待確認(重要)**`script.py` 的標準介面(函數名稱、參數格式、回傳格式)目前無正式文件。
`InferenceWorkerThread` 的呼叫方式推斷:
- `script.py` 應提供一個可被 worker 呼叫的推論函數
- 接收 `input_params` 字典(包含 `usb_port_id``device_group``model``scpu_path``ncpu_path``file_path` 等)
- 回傳 Bounding Box 結果,格式為:
```json
{
"num_boxes": 2,
"bounding boxes": [[x1, y1, x2, y2], [x3, y3, x4, y4]],
"results": ["label1", "label2"]
}
```
---
## 5. 非功能需求
### 5.1 效能需求
| 項目 | 規格 | 來源 |
|------|------|------|
| 推論 Queue 大小 | 最大 5 幀 | `InferenceController.__init__` |
| 標準模式最短推論間隔 | 2 秒 | `InferenceController.select_tool()` |
| Custom Model 最短推論間隔 | 0.5 秒 | `InferenceController.select_custom_tool()` |
| 模型推論超時 | 5,000 ms | `config.py MODEL_TIMEOUT` |
| 裝置掃描超時 | 5 秒 | `device_service.check_available_device()` |
| 攝影機開啟超時 | 5 秒 / 最多 3 次嘗試 | `VideoThread._camera_timeout` |
| 攝影機解析度 | 640×480 | `VideoThread.run()` |
| 攝影機目標幀率 | 30 fps | `VideoThread.run()` |
| Custom Model 輸入大小 | 640×640 | `custom_inference_worker.preprocess_frame()` |
| 模型推論設備超時Custom | 5,000 ms | `CustomInferenceWorkerThread.initialize_device()` |
### 5.2 平台相容性
| 項目 | 規格 |
|------|------|
| 作業系統 | Windows使用 `%LOCALAPPDATA%`PowerInstaller 打包) |
| Python 版本 | Python 3.12 |
| 視窗尺寸 | 1200×900 像素(固定,`WINDOW_SIZE` 常數) |
**⚠️ 待確認**:是否計畫支援 macOS 或 Linux目前路徑處理和打包設定為 Windows 專屬。
### 5.3 應用程式打包
| 項目 | 工具 | 狀態 |
|------|------|------|
| 打包工具 | PyInstaller 6.12.0 | ✅ 已設定 |
| 安裝包製作 | Inno Setup`dist/test.iss` | ✅ 有設定檔 |
| 程式碼加密 | PyArmor | ⚠️ 計畫中 |
**打包注意事項**
- 需要包含 KneronPLUS SDK 的 `kp/lib` 資料夾
- 需包含 `uxui/` 資源目錄和 `src/` 目錄
### 5.4 資料儲存
所有執行期資料存放於 `%LOCALAPPDATA%/Kneron_Academy/`
- **uploads/**:使用者上傳的圖片/影片
- **utils/**Plugin 設定與模型檔案
- **firmware/**:裝置韌體檔案
**⚠️ 待確認**:安裝包是否會預先建立這些目錄,或由應用程式首次執行時自動建立?
---
## 6. 裝置連接與授權流程(詳細)
根據 `flowchart.md` 的設計(⚠️ 此流程尚未完整實作於程式碼中,為設計規格):
```
登入畫面
Server 驗證(帳密驗證)
├── 成功 → 進入 Dongle 模組畫面
└── 失敗 → 顯示錯誤(帳密錯誤 / 服務被暫停)
Dongle 模組畫面
連接 Dongle
├── 未偵測到 → 顯示「未偵測到裝置」提示
└── 偵測到 → 檢查 Driver 是否安裝
Driver 檢查
├── 已安裝 → 檢查 FW 版本
└── 未安裝 → 詢問是否安裝 Driver
├── 使用者同意 → 執行安裝(可能失敗)
└── 失敗 → 顯示錯誤
FW 版本檢查
├── 版本符合 → 取得 Dongle KN Number
└── 需要更新 → 詢問是否更新 FW
├── 使用者同意 → 下載並安裝 FW
└── 失敗 → 顯示錯誤
取得 KN Number
└── 成功 → 檢查授權狀態
授權狀態檢查
├── 已授權 → 顯示已授權提示 → 回到 App 主頁
└── 未授權 → 詢問是否啟動授權
├── 使用者同意 → 執行授權流程
│ ├── 成功 → 顯示成功提示 → 回到主頁
│ └── 失敗 → 顯示失敗提示
└── 使用者不重試 → 顯示提示 → 回到主頁
```
---
## 7. 已知限制與未完成功能
以下為從程式碼分析發現的限制與尚未完成的功能:
### 7.1 尚未完整實作
| 功能 | 狀態 | 說明 |
|------|------|------|
| 登入 Server 驗證 | 🔄 部分實作 | `LoginScreen` 存在,但 Server 端點設定不明確 |
| Driver 自動安裝 | 🔄 設計中 | `flowchart.md` 有設計,程式碼中未見完整實作 |
| Dongle 授權管理 | 🔄 設計中 | `flowchart.md` 有設計,程式碼中未見完整實作 |
| 音訊voice推論模式 | 🔄 設計中 | `config.py` 有引入 `librosa``sounddevice`,但推論路徑不完整 |
| Purchased Items API 對接 | 🔄 Mock 資料 | `utilities_screen.py``populate_mock_purchased_items()` 方法,使用假資料 |
| 程式碼加密PyArmor | ⏳ 計畫中 | 僅列在 `env.txt`,尚未見到具體加密流程 |
| KL520/KL720 以外裝置支援 | ⏳ 計畫中 | `DeviceType` enum 有定義,但 `DongleModelMap` 僅有這兩型 |
### 7.2 已知技術問題
| 問題 | 嚴重度 | 說明 |
|------|--------|------|
| Debug print 語句殘留 | 低 | 多個 controller 中有大量 `print()` debug 訊息 |
| 錯誤處理不完整 | 中 | 部分 try/except 只 print 不處理(如 `device_controller.py` |
| 測試覆蓋率為零 | 高 | 完全沒有自動化測試 |
| UI 更新 Thread 安全性 | 中 | 待確認推論結果回傳到主線程的路徑是否完全使用 Qt Signal |
| `custom_inference_worker.py` 中引用未導入的 `kp` | 中 | `_boxes_scale()``post_process_yolo_v5()` 在模組頂層使用 `kp.HwPreProcInfo` 等型別,但 `import kp` 在函數內才執行 |
### 7.3 設計假設(未驗證)
- Custom Model 後處理固定使用 YOLOv5未提供自定義後處理的擴充機制
- 應用程式假設攝影機 index 為 0第一個相機無法選擇其他相機
- 應用程式固定使用 Windows 路徑格式,不支援跨平台
---
## 8. 功能優先級RICE 分析)
> ⚠️ 以下 RICE 分數為從現有程式碼功能狀態推斷Effort 值基於程式碼完成度估算。**請與產品負責人及開發團隊確認 Reach、Impact 和 Confidence 的實際數值。**
| 功能 | Reach | Impact | Confidence | Effort人天 | RICE 分數 | 階段 |
|------|-------|--------|------------|--------------|----------|------|
| Video 模式推論 | 100% | 3 | 90% | — | 高 | ✅ 已完成 |
| Image 模式推論 | 90% | 2 | 90% | — | 高 | ✅ 已完成 |
| Plugin 系統config.json | 100% | 3 | 85% | — | 高 | ✅ 已完成 |
| Custom Model 推論 | 60% | 3 | 80% | — | 中高 | ✅ 已完成 |
| 裝置掃描 / 連接 | 100% | 3 | 90% | — | 高 | ✅ 已完成 |
| 登入驗證(完整實作) | 100% | 3 | 70% | 5 | 高 | 🔄 進行中 |
| Dongle 授權管理 | 100% | 3 | 65% | 10 | 高 | 🔄 設計中 |
| Purchased Items API 對接 | 80% | 2 | 70% | 7 | 中 | 🔄 設計中 |
| Driver 自動安裝 | 70% | 2 | 60% | 5 | 中 | 🔄 設計中 |
| 音訊推論模式 | 40% | 2 | 50% | 10 | 低中 | ⏳ 待實作 |
| 多攝影機選擇 | 30% | 1 | 70% | 3 | 低 | ⏳ 未排期 |
| 跨平台支援macOS/Linux | 50% | 2 | 50% | 20 | 中 | ⏳ 未排期 |
---
## 9. 輸入資料格式規範
### 9.1 攝影機影像
- 擷取方式OpenCV `cv2.VideoCapture`DirectShow backendWindows 優先)
- 解析度640×480
- 格式:`QImage.Format_RGB888`(透過 `VideoThread` 發出),後轉為 NumPy 陣列 `(height, width, 3)`,通道順序 RGB888
### 9.2 上傳圖片
- 讀取方式:`cv2.imread()`
- 格式NumPy 陣列BGR 格式OpenCV 預設)
- 儲存位置:`%LOCALAPPDATA%/Kneron_Academy/uploads/`
### 9.3 Custom Model 輸入(預處理後)
- 目標大小640×640
- 格式BGR565`cv2.COLOR_BGR2BGR565`
### 9.4 input_params 字典格式
`InferenceWorkerThread` 接收的 `input_params` 標準結構:
```python
{
"usb_port_id": 32, # Dongle USB port ID
"device_group": <kp.DeviceGroup>, # 已連接的裝置群組物件
"scpu_path": "...\\firmware\\KL520\\fw_scpu.bin",
"ncpu_path": "...\\firmware\\KL520\\fw_ncpu.bin",
"fw_folder": "...\\firmware", # 全域 firmware 目錄
"file_path": "...\\uploads\\image.jpg", # 圖片模式使用
"model": "...\\utils\\{mode}\\{model}\\{model_file}.nef",
"model_descriptor": <kp.ModelDescriptor>, # 已上傳的模型描述符
# 以下為 config.json 中的 input_parameters 欄位(由模型設定決定)
"threshold": 0.75,
"max_faces": 10,
...
}
```
---
## 10. 推論結果輸出格式
`script.py` 回傳或 `CustomInferenceWorkerThread` 發出的標準推論結果:
```json
{
"num_boxes": 2,
"bounding boxes": [[x1, y1, x2, y2], [x3, y3, x4, y4]],
"results": ["label1", "label2"]
}
```
其中座標為絕對像素值,對應原始影像尺寸(非縮放後的模型輸入尺寸)。
---
## 11. 安裝需求
### 11.1 開發環境安裝
```shell
# 1. 安裝 KneronPLUS SDK
cd ./external/kneron_plus_{version}/package/{platform}/
pip install KneronPLUS-{version}-py3-none-any.whl
# 2. 安裝其他依賴
pip install PyQt5 opencv-python pyinstaller pyarmor
# 3. 執行應用程式
python main.py
```
### 11.2 打包指令
```shell
pyinstaller --onefile --windowed main.py \
--additional-hooks-dir=hooks \
--add-data "uxui;uxui" \
--add-data "src;src" \
--add-data "{conda_env}\\Lib\\site-packages\\kp\\lib;kp\\lib"
```
---
## 12. 風險與緩解措施
| 風險 | 可能性 | 影響 | 緩解措施 |
|------|--------|------|----------|
| `script.py` 介面無標準文件,第三方難以開發 Plugin | 高 | 高 | 盡速制定並文件化 `script.py` 介面規範 |
| Purchased Items 功能仍是 Mock 資料,影響客戶體驗 | 中 | 高 | 確認後端 API 規格,完成對接 |
| 缺乏自動化測試,難以安全重構 | 高 | 中 | 補齊關鍵路徑的 unit test推論 queue、裝置連接 |
| `custom_inference_worker.py``kp` 引入問題,可能在非 kp 環境崩潰 | 中 | 中 | 修復模組頂層的型別引用問題 |
| 硬體相依性強,無 dongle 時無法測試核心功能 | 高 | 中 | 建立 Mock/Stub 的 kp 測試層 |
| Windows 專屬設計,未來擴展平台成本高 | 低 | 中 | 現階段維持 Windows 優先,抽象化路徑處理為未來鋪路 |
---
## 附錄 A程式碼結構快速參照
| 模組 | 路徑 | 功能 |
|------|------|------|
| `AppController` | `main.py` | 應用程式入口,管理 QStackedWidget 頁面切換 |
| `SelectionScreen` | `src/views/selection_screen.py` | 首頁選擇畫面 |
| `LoginScreen` | `src/views/login_screen.py` | 登入驗證畫面 |
| `UtilitiesScreen` | `src/views/utilities_screen.py` | 裝置管理工具頁面 |
| `MainWindow` | `src/views/mainWindows.py` | AI 推論主視窗 |
| `DeviceController` | `src/controllers/device_controller.py` | 裝置掃描、連接、中斷 |
| `InferenceController` | `src/controllers/inference_controller.py` | 推論工具選擇、queue 管理 |
| `MediaController` | `src/controllers/media_controller.py` | 攝影機操作 |
| `InferenceWorkerThread` | `src/models/inference_worker.py` | 標準推論 Worker動態載入 script.py |
| `CustomInferenceWorkerThread` | `src/models/custom_inference_worker.py` | Custom Model 推論 WorkerYOLOv5 後處理) |
| `VideoThread` | `src/models/video_thread.py` | 攝影機影像擷取 Thread |
| `check_available_device()` | `src/services/device_service.py` | 含 timeout 的裝置掃描服務 |
| `DeviceType`, `DongleModelMap` | `src/config.py` | 支援裝置型號列舉與對應表 |
---
*本 PRD 版本v1.0(從程式碼反推)*
*建立日期2026-04-04*
*建立者PM AgentAutoflow*
*狀態:待產品負責人確認標注「⚠️ 待確認」的項目*

View File

@ -0,0 +1,729 @@
# KNEO AcademyInnovedus AI Playground設計規格文件
> **文件性質說明**:本設計規格從既有程式碼反向整理,涵蓋 `src/config.py`、各 View 檔案及 `uxui/` 目錄下的 UI 資源。標注「⚠️ 待確認」的項目為推斷內容或程式碼中資訊不足的部分。
---
## ① Design Tokens
> 以下 Token 直接從 `src/config.py` 及各 View 的 `setStyleSheet()` 提取。
### 1.1 色彩系統Color Tokens
#### Reference Tokens原始值
| Token 名稱 | 色碼 | 說明 |
|-----------|------|------|
| color.navy.900 | `#143058` | 深海軍藍MainWindow 背景) |
| color.blue.600 | `#005ED7` | 亮藍SecondaryColorPopup / 元件背景) |
| color.blue.500 | `#3498DB` | 標準藍UtilitiesScreen 按鈕、表格 Header |
| color.blue.400 | `#2980B9` | 藍 Hover 色 |
| color.blue.300 | `#1F618D` | 藍 Pressed 色 |
| color.slate.800 | `#2C3E50` | 深石板藍LoginScreen/UtilitiesScreen Header 背景) |
| color.slate.700 | `#34495E` | 石板藍SelectionScreen Header、按鈕文字 |
| color.gray.100 | `#F8F9FA` | 極淺灰(進度區塊背景) |
| color.gray.50 | `#F5F7FA` | 淺灰LoginScreen / UtilitiesScreen 頁面背景) |
| color.gray.200 | `#E0E0E0` | 邊框灰(卡片、輸入框邊框) |
| color.gray.400 | `#BDC3C7` | 中灰(非選中導航按鈕文字) |
| color.gray.500 | `#95A5A6` | 灰Footer 文字、Back 按鈕) |
| color.gray.600 | `#7F8C8D` | 深灰(描述文字、次要標籤) |
| color.green.500 | `#2ECC71` | 標準綠Download 按鈕) |
| color.green.600 | `#27AE60` | 綠 Hover |
| color.green.700 | `#1E8449` | 綠 Pressed |
| color.green.400 | `#4CAF50` | 成功狀態綠Custom Model 上傳成功) |
| color.orange.500 | `#F39C12` | 橘Firmware Update 按鈕) |
| color.orange.600 | `#D35400` | 橘 Hover |
| color.orange.700 | `#A04000` | 橘 Pressed |
| color.purple.500 | `#9B59B6` | 紫Install Driver 按鈕) |
| color.purple.600 | `#8E44AD` | 紫 Hover |
| color.purple.700 | `#7D3C98` | 紫 Pressed |
| color.red.500 | `#E74C3C` | 紅(錯誤訊息文字) |
| color.red.400 | `#ff6b6b` | 亮紅Stop 按鈕文字及邊框) |
| color.white | `#FFFFFF` | 純白 |
| color.black | `#000000` | 純黑Canvas 背景) |
| color.selection | `#F8F9FA` | 選取頁面背景 |
#### Semantic Tokens語義映射
| Semantic Token | Reference Token | 說明 |
|---------------|----------------|------|
| color.bg.primary | color.navy.900`#143058` | MainWindow 頁面背景 |
| color.bg.light | color.gray.50`#F5F7FA` | Login / Utilities 頁面背景 |
| color.bg.selection | color.selection`#F8F9FA` | SelectionScreen 頁面背景 |
| color.bg.card | color.white | 卡片、表單容器背景 |
| color.bg.header.dark | color.slate.800`#2C3E50` | Login / Utilities Header 背景 |
| color.bg.header.alt | color.slate.700`#34495E` | SelectionScreen Header 背景 |
| color.bg.component | color.blue.600`#005ED7` | 側邊欄元件背景Device Panel、Custom Model Block、Media Panel、Popup |
| color.bg.canvas | color.black | Camera / 推論顯示區域背景 |
| color.bg.mask | `rgba(0, 0, 0, 0.7)` | Device Popup 蒙版 |
| color.border.default | color.gray.200`#E0E0E0` | 卡片、輸入框、表格邊框 |
| color.text.primary | color.slate.700`#34495E` | 主要文字(深色背景上) |
| color.text.secondary | color.gray.600`#7F8C8D` | 次要說明文字 |
| color.text.on-dark | color.white | 深色背景上的文字 |
| color.text.placeholder | color.gray.500`#95A5A6` | 輸入框 Placeholder |
| color.text.error | color.red.500`#E74C3C` | 錯誤訊息 |
| color.text.success | color.green.400`#4CAF50` | 成功狀態訊息 |
| color.text.footer | color.gray.500`#95A5A6` | Footer 版權文字 |
| color.action.primary | color.blue.500`#3498DB` | 主要 CTA 按鈕Login、Refresh、Utilities Tab |
| color.action.primary.hover | color.blue.400`#2980B9` | |
| color.action.primary.pressed | color.blue.300`#1F618D` | |
| color.action.success | color.green.500`#2ECC71` | 下載、Register 按鈕 |
| color.action.warning | color.orange.500`#F39C12` | Firmware Update 按鈕 |
| color.action.danger | color.red.400`#ff6b6b` | Stop 按鈕 |
| color.action.neutral | color.gray.500`#95A5A6` | Back 按鈕(中性) |
| color.action.special | color.purple.500`#9B59B6` | Install Driver 按鈕 |
| color.input.focus-border | color.blue.500`#3498DB` | 輸入框 Focus 狀態邊框 |
| color.table.header | color.blue.500`#3498DB` | 表格 Header 背景 |
| color.table.selected | color.blue.500`#3498DB` | 表格選中列背景 |
---
### 1.2 尺寸系統Size Tokens
#### 視窗尺寸
| Token | 數值 | 說明 |
|-------|------|------|
| window.width | 1200px | 固定視窗寬度(`WINDOW_SIZE` |
| window.height | 900px | 固定視窗高度(`WINDOW_SIZE` |
| popup.ratio | 0.67 | Device Popup 相對視窗的寬高比(`POPUP_SIZE_RATIO` |
| popup.width | ~804px | 計算值1200 × 0.67 |
| popup.height | ~603px | 計算值900 × 0.67 |
#### 佈局尺寸
| Token | 數值 | 說明 |
|-------|------|------|
| layout.left-panel.width | 260px | MainWindow 左側面板固定寬度 |
| layout.canvas.width | 900px | Camera Canvas 固定寬度 |
| layout.canvas.height | 750px | Camera Canvas 固定高度 |
| layout.canvas.inner.width | 880px | Canvas Label 最小寬度 |
| layout.canvas.inner.height | 730px | Canvas Label 最小高度 |
| layout.header.height | 60px | UtilitiesScreen Header 固定高度 |
| layout.header.height.alt | 100px | LoginScreen / SelectionScreen Header 固定高度 |
#### 元件尺寸
| Token | 數值 | 說明 |
|-------|------|------|
| component.device-panel.width | 240px | Device Panel 固定寬度 |
| component.device-panel.height | 200px | Device Panel 固定高度 |
| component.custom-model.width | 240px | Custom Model Block 固定寬度 |
| component.custom-model.height | 270px | Custom Model Block 固定高度 |
| component.media-panel.width | 90px | Media Panel 固定寬度 |
| component.media-panel.height | 290px | Media Panel 固定高度 |
| component.selection-card.min-width | 300px | SelectionScreen 選項卡片最小寬度 |
| component.selection-card.min-height | 250px | SelectionScreen 選項卡片最小高度 |
#### 按鈕尺寸
| Token | 數值 | 說明 |
|-------|------|------|
| button.standard.min-height | 40px | 標準操作按鈕最小高度UtilitiesScreen |
| button.login.min-height | 45px | 登入頁按鈕最小高度 |
| button.icon.size | 50×50px | Media Panel 圖示按鈕尺寸 |
| button.icon.inner | 40×40px | Media Panel SVG 圖示尺寸 |
| button.popup.size | 150×45px | Popup 中 Refresh / Done 按鈕尺寸 |
| button.detail.size | 72×30px | Device Panel 中 Details 按鈕尺寸 |
| button.back.size | 40×40px | Header Back 按鈕尺寸 |
| button.file-upload.size | 28×22px | Custom Model 上傳按鈕("...")尺寸 |
| button.custom-model-action.height | 32px | Custom Model Stop / Run 按鈕高度 |
#### 圖示尺寸
| Token | 數值 | 說明 |
|-------|------|------|
| icon.header.window | 35×35px | Popup 視窗 title 圖示 |
| icon.nav | 20×20px | Device Panel title 圖示 |
| icon.title.custom-model | 28×28px | Custom Model Block title 圖示 |
| icon.card.selection | 64×64px | SelectionScreen 卡片圖示 |
| icon.dongle.popup | 30×30px | Device Popup 中裝置圖示 |
| icon.dongle.list | 30×30px | Device List 中裝置圖示容器 |
| icon.logo.main | 104×40px | MainWindow / UtilitiesScreen 縮小 Logo |
| icon.logo.selection | 150×60px | SelectionScreen / LoginScreen 大型 Logo |
| icon.combo-arrow | 12×12px | QComboBox 下拉箭頭圖示 |
---
### 1.3 間距系統Spacing Tokens
| Token | 數值 | 使用場景 |
|-------|------|---------|
| spacing.xs | 2px | FileUploadRow 上下 margin |
| spacing.sm | 5px | List item padding、button spacing |
| spacing.md | 10px | 容器內部間距、部分 Padding |
| spacing.lg | 15px | 區塊內部間距Device Panel、Custom Model Block |
| spacing.xl | 20px | 頁面主要 MarginPopup、MainWindow canvas |
| spacing.xxl | 30px | 表單容器 PaddingLogin、SelectionScreen |
| spacing.form | 40px | SelectionScreen 主要 Margin |
| spacing.card | 40px | SelectionScreen 兩張卡片之間的間距 |
| spacing.nav | 10px | Header 導航按鈕之間的間距 |
---
### 1.4 圓角系統Border Radius Tokens
| Token | 數值 | 使用場景 |
|-------|------|---------|
| radius.sm | 3px | 檔案上傳標籤、小型 UI 元素 |
| radius.md | 5px | 按鈕UtilitiesScreen、輸入框、表格 |
| radius.lg | 8px | 卡片區塊Device Section、Purchased Items、Status Section |
| radius.xl | 10px | 主要卡片容器SelectionScreen、LoginScreen form |
| radius.xxl | 15px | 選項卡片SelectionScreen、側邊欄元件Device Panel、Custom Model Block |
| radius.pill | 20px | Popup、Media Panel大圓角 |
---
### 1.5 字型系統Typography Tokens
> **注意**:程式碼中未定義全域字型 family使用 PyQt5 系統預設字型Windows 環境下通常為 Segoe UI。以下為從程式碼提取的字型大小與字重規格。
| Token | 數值 | 使用場景 |
|-------|------|---------|
| font.size.xs | 10px | 檔案名稱標籤FileUploadRow、ComboBox 箭頭 |
| font.size.sm | 11px | Custom Model 小型標籤、狀態訊息、Run 按鈕文字 |
| font.size.body | 12px | Footer 文字、Device Popup 狀態標籤 |
| font.size.md | 14px | 表單標籤、描述文字、按鈕文字UtilitiesScreen、輸入框、KN 號碼標籤 |
| font.size.lg | 16px | 選項卡片描述、Device 標題標籤、Device LabelPopup 中、Custom Model title |
| font.size.xl | 18px | 區塊標題Device Connection、Device Status、Purchased Items |
| font.size.xxl | 20px | Device Panel 標題 |
| font.size.xxxl | 22px | 選項卡片標題 |
| font.size.display | 24px | Canvas 載入文字、UtilitiesScreen Header title |
| font.size.h2 | 28px | LoginScreen title、SelectionScreen APP title |
| font.size.h1 | 32px | Device Popup title |
| font.weight.normal | normal | 一般文字 |
| font.weight.bold | bold | 標題、按鈕文字、重要標籤 |
---
### 1.6 按鈕狀態樣式Button State Tokens
#### 全域按鈕樣式(`BUTTON_STYLE` — 深色背景通用)
```
背景透明transparent
文字顏色:白色
邊框2px solid white
圓角15px
Padding5px 10px
Hoverbackground-color: rgba(255, 255, 255, 50)(約 20% 白色透明)
Pressedbackground-color: rgba(255, 255, 255, 100)(約 39% 白色透明)
```
#### 主要 CTA 按鈕(藍色,淺色背景場景)
```
背景:#3498DB
文字:白色
邊框none或 2px solid #2980B9
圓角5px
MinHeight40-45px
Padding10px 15px
Hover#2980B9
Pressed#1F618D
Disabled#3498DB(同 Default視覺不變
```
#### 成功/下載按鈕(綠色)
```
背景:#2ECC71
文字:白色
邊框2px solid #27AE60
圓角5px
Hover#27AE60
Pressed#1E8449
```
#### 警告按鈕(橘色 Firmware Update
```
背景:#F39C12
文字:白色
邊框2px solid #D35400
圓角5px
Hover#D35400
Pressed#A04000
```
#### 特殊功能按鈕(紫色 Install Driver
```
背景:#9B59B6
文字:白色
邊框2px solid #8E44AD
圓角5px
Hover#8E44AD
Pressed#7D3C98
```
#### 中性返回按鈕(灰色)
```
背景:#95A5A6
文字:白色
圓角5px
Hover#7F8C8D
Pressed#616A6B
```
#### Custom Model — Run 按鈕(綠色半透明)
```
背景rgba(76, 175, 80, 0.3)
文字:白色
邊框1px solid #4CAF50
圓角8px
高度32px
Hoverrgba(76, 175, 80, 0.5)
Disabledrgba(128, 128, 128, 0.2),文字 #666,邊框 #666
```
#### Custom Model — Stop 按鈕(紅色透明)
```
背景:透明
文字:#ff6b6b
邊框1px solid #ff6b6b
圓角8px
高度32px
Hoverrgba(255, 107, 107, 0.2)
```
#### Media Panel 圖示按鈕
```
背景:透明
邊框1px transparent
圓角10px
尺寸50×50px
Hoverrgba(255, 255, 255, 50)
Pressedrgba(255, 255, 255, 100)
```
#### Header Back 按鈕(圖示透明按鈕)
```
背景:透明
邊框none
尺寸40×40px
Hoverrgba(255, 255, 255, 0.1),圓角 20px
```
---
## ② UI 資源清單
> 路徑:`uxui/`
### 2.1 PNG 圖片(`Assets_png/`
| 檔名 | 推斷用途 | 使用位置 |
|------|---------|---------|
| `kneron_logo.png` | Kneron / Innovedus 品牌 Logo | MainWindow 歡迎畫面、所有頁面 Header、MainWindow 左側面板 |
| `ic_dongle_520.png` | KL520 Dongle 裝置圖示 | Device Popup、Device List裝置列表項 |
| `ic_dongle_720.png` | KL720 Dongle 裝置圖示 | Device Popup、Device List裝置列表項 |
> **⚠️ 待確認**:程式碼中 `create_header()` 呼叫 `Assets_png/back_arrow.png` 作為 Back 按鈕圖示,但此檔案**不在 `Assets_png/` 目錄中**。此按鈕目前可能顯示為空白或破圖。
---
### 2.2 SVG 圖示(`Assets_svg/`
#### 功能按鈕圖示(`bt_function_*`
| 檔名 | 狀態 | 推斷用途 |
|------|------|---------|
| `bt_function_camera_disabled.svg` | disabled | 相機功能按鈕(停用) |
| `bt_function_mic_disabled.svg` | disabled | 麥克風功能按鈕(停用) |
| `bt_function_mic_hover.svg` | hover | 麥克風功能按鈕 |
| `bt_function_mic_normal.svg` | normal | 麥克風功能按鈕 |
| `bt_function_mic_pressed.svg` | pressed | 麥克風功能按鈕 |
| `bt_function_recording_hover.svg` | hover | 錄製功能按鈕 |
| `bt_function_recording_normal.svg` | normal | 錄製功能按鈕 |
| `bt_function_recording_pressed.svg` | pressed | 錄製功能按鈕 |
| `bt_function_screencapture_disabled.svg` | disabled | 螢幕截圖按鈕(停用) |
| `bt_function_screencapture_hover.svg` | hover | 螢幕截圖按鈕 |
| `bt_function_screencapture_normal.svg` | normal | 螢幕截圖按鈕Media Panel 第一個按鈕) |
| `bt_function_screencapture_pressed.svg` | pressed | 螢幕截圖按鈕 |
| `bt_function_upload_disabled.svg` | disabled | 上傳檔案按鈕(停用) |
| `bt_function_upload_hover.svg` | hover | 上傳檔案按鈕 |
| `bt_function_upload_normal.svg` | normal | 上傳檔案按鈕Media Panel 第二個按鈕) |
| `bt_function_upload_pressed.svg` | pressed | 上傳檔案按鈕 |
| `bt_function_video_hover.svg` | hover | 影片/恢復播放按鈕(暫停→恢復時切換) |
| `bt_function_video_normal.svg` | normal | 影片按鈕 |
| `bt_function_video_pressed.svg` | pressed | 影片按鈕 |
> **⚠️ 待確認**:目前 Media Panel「暫停/恢復」按鈕使用 `btn_result_image_delete_hover.svg` 作為暫停狀態圖示,切換後顯示 `bt_function_video_hover.svg`。此對應關係看起來不是最終設計,圖示使用可能有誤。
#### 對話框按鈕(`btn_dialog_*`
| 檔名 | 狀態 | 推斷用途 |
|------|------|---------|
| `btn_dialog_customization_delete_hover.svg` | hover | 自定義對話框刪除按鈕 |
| `btn_dialog_customization_delete_normal.svg` | normal | 自定義對話框刪除按鈕 |
| `btn_dialog_customization_delete_pressed.svg` | pressed | 自定義對話框刪除按鈕 |
| `btn_dialog_customization_upload_hover.svg` | hover | 自定義對話框上傳按鈕 |
| `btn_dialog_customization_upload_normal.svg` | normal | 自定義對話框上傳按鈕 |
| `btn_dialog_customization_upload_pressed.svg` | pressed | 自定義對話框上傳按鈕 |
| `btn_dialog_device_disconnect_hover.svg` | hover | 裝置斷線按鈕 |
| `btn_dialog_device_disconnect_normal.svg` | normal | 裝置斷線按鈕 |
| `btn_dialog_device_disconnect_pressed.svg` | pressed | 裝置斷線按鈕 |
> **⚠️ 待確認**`btn_dialog_*` 系列圖示在目前的程式碼中**找不到對應的呼叫位置**(自定義對話框的刪除/上傳、裝置斷線按鈕),可能屬於尚未實作的功能 UI或被移除但資源保留。
#### 結果區域按鈕(`btn_result_*`
| 檔名 | 狀態 | 推斷用途 |
|------|------|---------|
| `btn_result_edit_hover.svg` | hover | 推論結果編輯按鈕 |
| `btn_result_edit_normal.svg` | normal | 推論結果編輯按鈕 |
| `btn_result_edit_pressed.svg` | pressed | 推論結果編輯按鈕 |
| `btn_result_image_delete_hover.svg` | hover | 推論結果圖片刪除按鈕(目前被借用為暫停圖示) |
| `btn_result_image_delete_normal.svg` | normal | 推論結果圖片刪除按鈕 |
| `btn_result_image_delete_pressed.svg` | pressed | 推論結果圖片刪除按鈕 |
> **⚠️ 待確認**`btn_result_*` 在程式碼中僅 `btn_result_image_delete_hover.svg` 被使用(且用途有疑問),其他均未見使用。
#### 設定與下載(單一圖示)
| 檔名 | 推斷用途 | 使用位置 |
|------|---------|---------|
| `btn_setting.svg` | 設定按鈕 | ⚠️ 程式碼中未見呼叫 |
| `ic_result_download_diabled.svg` | 結果下載(停用) | ⚠️ 程式碼中未見呼叫 |
| `ic_result_download_hover.svg` | 結果下載 Hover | ⚠️ 程式碼中未見呼叫 |
| `ic_result_download_normal.svg` | 結果下載 Normal | ⚠️ 程式碼中未見呼叫 |
| `ic_result_download_pressed.svg` | 結果下載 Pressed | ⚠️ 程式碼中未見呼叫 |
| `ic_result_folder_hover.svg` | 結果資料夾 Hover | ⚠️ 程式碼中未見呼叫(除被借用為 ComboBox 下拉箭頭) |
| `ic_result_folder_normal.svg` | 結果資料夾 Normal | 被用作 LoginScreen QComboBox 下拉箭頭 |
| `ic_result_folder_pressed.svg` | 結果資料夾 Pressed | ⚠️ 程式碼中未見呼叫 |
#### 通用圖示(`ic_*`
| 檔名 | 推斷用途 | 使用位置 |
|------|---------|---------|
| `ic_customization_upload_folder.svg` | 自定義模型上傳資料夾圖示 | ⚠️ 程式碼中未見呼叫 |
| `ic_dialog_customization.svg` | 自定義對話框圖示 | ⚠️ 程式碼中未見呼叫 |
| `ic_dialog_device.svg` | 裝置對話框圖示 | SelectionScreen Utilities 卡片圖示 |
| `ic_dialog_missing_camera.svg` | 找不到攝影機圖示 | ⚠️ 程式碼中未見呼叫(應用於相機找不到的提示) |
| `ic_dongle_520.svg` | KL520 Dongle 圖示SVG 版) | ⚠️ 程式碼中未見呼叫PNG 版本被使用) |
| `ic_recording_camera.svg` | 攝影機/錄影圖示 | SelectionScreen Demo App 卡片圖示Media Panel 第五個按鈕(錄影) |
| `ic_recording_voice.svg` | 錄音圖示 | Media Panel 第四個按鈕(錄音) |
| `ic_window_customization.svg` | 自定義視窗圖示 | ⚠️ 程式碼中未見呼叫 |
| `ic_window_device.svg` | 裝置視窗圖示 | Device Popup 標題列Device Panel 標題列 |
| `ic_window_toolbox.svg` | 工具箱視窗圖示 | Custom Model Block 標題列 |
---
### 2.3 動態圖(`Assets_gif/`
| 檔名 | 用途 | 使用位置 |
|------|------|---------|
| `no_device_temp.gif` | 無裝置狀態動態提示圖 | `MainWindow.show_no_device_gif()`(無裝置時在主視窗顯示) |
---
### 2.4 根目錄 SVG 資源
| 檔名 | 推斷用途 | 使用位置 |
|------|---------|---------|
| `canvas_background.svg` | 攝影機畫布背景 SVG | ⚠️ 程式碼中未見呼叫 |
| `usb_dongle.svg` | USB Dongle 圖示 | ⚠️ 程式碼中未見呼叫 |
---
## ③ 頁面元件清單
### 3.1 SelectionScreen首頁選擇畫面
**背景色**`#F8F9FA`(淺灰白)
**佈局**QVBoxLayoutMargin 40px 全周Spacing 20px
| 元件 | Class/類型 | 尺寸/規格 | 樣式摘要 |
|------|-----------|---------|---------|
| 頁面背景 | QWidget | 1200×900px | `background: #F8F9FA` |
| Header Frame | QFrame | 全寬 × 100px 固定高 | `background: #34495E; border-radius: 10px` |
| Kneron Logo | QLabel + QPixmap | 縮放至 150×60px | 水平居中 |
| 內容容器 | QFrame | 彈性高(填充剩餘空間)| `background: white; border-radius: 10px; border: 1px solid #E0E0E0`Padding 30px |
| APP 名稱標題 | QLabel | — | `font-size: 28px; font-weight: bold; color: #34495E`,水平居中 |
| 副標題 | QLabel | — | `font-size: 16px; color: #7F8C8D`,水平居中,文字:「請選擇您要使用的功能」 |
| 按鈕容器 | QWidget + QHBoxLayout | — | Margin 20px 左右Spacing 40px |
| **Utilities 卡片** | QFrame | min 300×250px | `background: white; border-radius: 15px; border: 1px solid #E0E0E0`Hover`background: #F5F9FF; border: #5DADE2`cursor: PointingHand |
| Utilities 卡片圖示 | QLabel + QPixmap | 64×64px | `ic_dialog_device.svg`,水平居中 |
| Utilities 卡片標題 | QLabel | — | `font-size: 22px; font-weight: bold; color: #34495E`,居中 |
| Utilities 卡片說明 | QLabel | — | `font-size: 14px; color: #7F8C8D`,居中,換行 |
| **Demo App 卡片** | QFrame | min 300×250px | `background: white; border-radius: 15px; border: 1px solid #E0E0E0`Hover`background: #F5FFF7; border: #7DCEA0`cursor: PointingHand |
| Demo App 卡片圖示 | QLabel + QPixmap | 64×64px | `ic_recording_camera.svg`,水平居中 |
| Demo App 卡片標題 | QLabel | — | `font-size: 22px; font-weight: bold; color: #34495E`,居中 |
| Demo App 卡片說明 | QLabel | — | `font-size: 14px; color: #7F8C8D`,居中,換行 |
| Footer | QLabel | — | `font-size: 12px; color: #95A5A6`,居中,文字:「© 2025 Innovedus Inc. All rights reserved.」 |
---
### 3.2 LoginScreen登入畫面
**背景色**`#F5F7FA`(淺灰)
**佈局**QVBoxLayoutMargin 40px 全周Spacing 20px
| 元件 | Class/類型 | 尺寸/規格 | 樣式摘要 |
|------|-----------|---------|---------|
| 頁面背景 | QWidget | 1200×900px | `background: #F5F7FA` |
| Header Frame | QFrame | 全寬 × 100px 固定高 | `background: #2C3E50; border-radius: 10px` |
| Kneron Logo | QLabel + QPixmap | 縮放至 150×60px | 水平居中 |
| 表單容器 | QFrame | 彈性高(填充剩餘空間)| `background: white; border-radius: 10px; border: 1px solid #E0E0E0`Padding 30px |
| 登入標題 | QLabel | — | `font-size: 28px; font-weight: bold; color: #2C3E50; margin-bottom: 10px`,居中,文字:"Login" |
| Server 驗證標籤 | QLabel | — | `font-size: 14px; font-weight: bold; color: #2C3E50`,文字:"Server Authentication Type" |
| **驗證類型 ComboBox** | QComboBox | min-height 40px | `border: 1px solid #E0E0E0; border-radius: 5px; padding: 5px 10px; font-size: 14px; color: #2C3E50`;下拉箭頭使用 `ic_result_folder_normal.svg` 12×12px |
| 帳號標籤 | QLabel | — | `font-size: 14px; font-weight: bold; color: #2C3E50`,文字:"Username" |
| **帳號輸入框** | QLineEdit | min-height 40px | `border: 1px solid #E0E0E0; border-radius: 5px; padding: 5px 10px; font-size: 14px; color: #2C3E50`Focus`border: 1px solid #3498DB`Placeholder"Enter your username" |
| 密碼標籤 | QLabel | — | `font-size: 14px; font-weight: bold; color: #2C3E50`,文字:"Password" |
| **密碼輸入框** | QLineEdit | min-height 40px | 同帳號輸入框EchoMode: PasswordPlaceholder"Enter your password" |
| 錯誤訊息 | QLabel | — | `color: #E74C3C; font-size: 14px`;預設隱藏(`hide()`),驗證失敗時顯示 |
| Back 按鈕 | QPushButton | min-height 45px | `background: #95A5A6; color: white; border-radius: 5px; font-size: 14px; font-weight: bold`Hover`#7F8C8D`Pressed`#616A6B` |
| Login 按鈕 | QPushButton | min-height 45px | `background: #3498DB; color: white; border-radius: 5px; font-size: 14px; font-weight: bold`Hover`#2980B9`Pressed`#1F618D` |
| Footer | QLabel | — | `font-size: 12px; color: #95A5A6`,居中 |
---
### 3.3 UtilitiesScreen裝置管理工具
**背景色**`#F5F7FA`(淺灰)
**佈局**QVBoxLayoutMargin 20px 全周Spacing 20px
#### 3.3.1 Header
| 元件 | 類型 | 尺寸/規格 | 樣式摘要 |
|------|------|---------|---------|
| Header Frame | QFrame | 全寬 × 60px 固定高 | `background: #2C3E50; border-radius: 0px` |
| Back 按鈕 | QPushButton | 40×40px | 圖示按鈕(`Assets_png/back_arrow.png`⚠️ 檔案缺失Hover`rgba(255,255,255,0.1) border-radius 20px` |
| 頁面標題 | QLabel | — | `color: white; font-size: 24px; font-weight: bold`,文字:"Utilities" |
| **Utilities 導航按鈕**(選中) | QPushButton | padding 5px 10px | `background: #3498DB; color: white; border: none; border-radius: 5px; font-weight: bold` |
| **Purchased Items 導航按鈕**(未選) | QPushButton | padding 5px 10px | `background: transparent; color: #BDC3C7; border: none; border-radius: 5px; font-weight: bold`Hover`color: white` |
| Kneron Logo | QLabel + QPixmap | 縮放至 104×40px | 靠右對齊 |
#### 3.3.2 Utilities 子頁面 — 裝置管理區塊
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| 區塊容器 | QFrame | — | `background: white; border-radius: 8px; border: 1px solid #E0E0E0`Padding 15px |
| 區塊標題 | QLabel | — | `font-size: 18px; font-weight: bold; color: #2C3E50`,文字:"Device Connection" |
| 區塊說明 | QLabel | — | `font-size: 14px; color: #7F8C8D`,文字:"Connect and manage your Kneron devices" |
| **裝置資料表** | QTableWidget | min-height 可捲動 | 6 欄Device Type / Port ID / Firmware Version / KN Number / Link Speed / StatusHeader `background: #3498DB; color: white; font-weight: bold; padding: 8px`Item padding 8pxSelected `background: #3498DB; color: white`gridline `#E0E0E0` |
| Refresh Devices 按鈕 | QPushButton | min-height 40px | `background: #3498DB; border: 2px solid #2980B9; border-radius: 5px; font-size: 14px; font-weight: bold`;狀態色如前 |
| Register Device 按鈕 | QPushButton | min-height 40px | `background: #2ECC71; border: 2px solid #27AE60`;狀態色如前 |
| Update Firmware 按鈕 | QPushButton | min-height 40px | `background: #F39C12; border: 2px solid #D35400`;狀態色如前 |
| Install Driver 按鈕 | QPushButton | min-height 40px | `background: #9B59B6; border: 2px solid #8E44AD`;狀態色如前 |
#### 3.3.3 Utilities 子頁面 — 狀態區塊
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| 區塊容器 | QFrame | — | `background: white; border-radius: 8px; border: 1px solid #E0E0E0`Padding 15px |
| 區塊標題 | QLabel | — | `font-size: 18px; font-weight: bold; color: #2C3E50`,文字:"Device Status" |
| 狀態訊息 | QLabel | — | `font-size: 14px; color: #7F8C8D`,預設文字:"No devices found" |
| 進度區塊(隱藏) | QFrame | — | `background: #F8F9FA; border-radius: 5px; border: 1px solid #E0E0E0; padding: 10px`;預設 `setVisible(False)` |
| 進度標題 | QLabel | — | `font-size: 14px; font-weight: bold; color: #2C3E50` |
| **進度條** | QProgressBar | height 20px | `border: 1px solid #E0E0E0; border-radius: 5px; background: white; text-align: center`Chunk`background: #3498DB; border-radius: 5px` |
#### 3.3.4 Purchased Items 子頁面
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| 區塊容器 | QFrame | — | `background: white; border-radius: 8px; border: 1px solid #E0E0E0`Padding 15px |
| 區塊標題 | QLabel | — | `font-size: 18px; font-weight: bold; color: #2C3E50`,文字:"Your Purchased Items" |
| 區塊說明 | QLabel | — | `font-size: 14px; color: #7F8C8D`,文字:"Select items to download to your device" |
| **已購項目表格** | QTableWidget | min-height 300px | 5 欄SelectCheckbox/ Product / Model / Current Version / Compatible Dongles樣式同裝置表格藍色 Header選中藍色 |
| Refresh Items 按鈕 | QPushButton | min-height 40px | `background: #3498DB; border: none; border-radius: 5px; font-size: 14px; font-weight: bold`Hover`#2980B9`Pressed`#1F618D` |
| Download Selected 按鈕 | QPushButton | min-height 40px | `background: #2ECC71; border: none; border-radius: 5px; font-size: 14px; font-weight: bold`Hover`#27AE60`Pressed`#1E8449` |
---
### 3.4 MainWindowAI Demo 推論主視窗)
**背景色**`#143058`(深海軍藍)
**佈局**QVBoxLayout 外層啟動時先顯示歡迎畫面Logo500ms 後切換至主頁面
#### 3.4.1 歡迎畫面500ms 轉場)
| 元件 | 類型 | 規格 | 說明 |
|------|------|------|------|
| 歡迎 Logo | QLabel + QPixmap | 原始尺寸 | `kneron_logo.png`,水平居中 |
#### 3.4.2 主頁面佈局QHBoxLayout
左右兩欄佈局:
- **左欄**:固定寬度 260pxQVBoxLayoutMargin 10pxSpacing 10px
- **右欄**彈性寬度stretch factor 2QGridLayoutMargin 0px
#### 3.4.3 左欄元件
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| Kneron Logo | QLabel + QPixmap | 縮放至 104×40px | — |
| **Device Panel** | QFrame | 240×200px 固定 | `background: #005ED7; border-radius: 15px; border: none` |
| — Device 圖示 | QSvgWidget | 20×20px | `ic_window_device.svg` |
| — Device 標題 | QLabel | — | `color: white; font-size: 20px; font-weight: bold` |
| — 裝置列表 | QListWidget | — | 透明背景Itempadding 5pxSelected`rgba(255,255,255,0.2)`;無 Scrollbar |
| — Details 按鈕 | QPushButton | 72×30px | 全域 `BUTTON_STYLE`(透明背景、白色邊框) |
| **Custom Model Block** | QFrame | 240×270px 固定 | `background: #005ED7; border-radius: 15px; border: none` |
| — 工具箱圖示 | QSvgWidget | 28×28px | `ic_window_toolbox.svg` |
| — Custom Model 標題 | QLabel | — | `color: white; font-size: 16px; font-weight: bold` |
| — 分隔線 | QFrame HLine | 1px 高 | `background: rgba(255,255,255,0.2)` |
| — Model 上傳列(.nef | FileUploadRow | — | 標籤 50px 寬;檔案框 22px 高;上傳按鈕 28×22px |
| — SCPU 上傳列(.bin | FileUploadRow | — | 同上 |
| — NCPU 上傳列(.bin | FileUploadRow | — | 同上 |
| — Labels 上傳列(.txt | FileUploadRow | — | 同上 |
| — 狀態標籤 | QLabel | — | `color: #4CAF50; font-size: 11px`;成功綠色,錯誤時 `#ff6b6b` |
| — Stop 按鈕 | QPushButton | 高 32px | 透明背景,紅色邊框文字(`#ff6b6b` |
| — Run Inference 按鈕 | QPushButton | 高 32px | 半透明綠色(`rgba(76,175,80,0.3)`),綠色邊框 |
#### 3.4.4 右欄元件
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| **Canvas Area** | QFrame | 900×750px 固定 | `border: 1px solid gray; background: black; border-radius: 20px`Padding 10px |
| Canvas Label顯示區| QLabel | 880×730px 最小 | 黑色背景,透明邊框;顯示攝影機 QImage 或靜態圖片 |
| **Media Panel**(浮動右下)| QFrame | 90×290px 固定 | `background: #005ED7; border-radius: 20px`;使用 `Qt.AlignBottom | Qt.AlignRight` 疊加在 Canvas 右下 |
| — 截圖按鈕 | QPushButton | 50×50px | 圖示 `bt_function_screencapture_normal.svg`40×40px |
| — 上傳按鈕 | QPushButton | 50×50px | 圖示 `bt_function_upload_normal.svg`40×40px |
| — 暫停/恢復按鈕 | QPushButton | 50×50px | 暫停時:`btn_result_image_delete_hover.svg`;恢復時:`bt_function_video_hover.svg` |
| — 錄音按鈕 | QPushButton | 50×50px | 圖示 `ic_recording_voice.svg`40×40px |
| — 錄影按鈕 | QPushButton | 50×50px | 圖示 `ic_recording_camera.svg`40×40px |
#### 3.4.5 Device Popup蒙版彈出視窗
| 元件 | 類型 | 規格 | 樣式摘要 |
|------|------|------|---------|
| 蒙版 Overlay | QWidget | 1200×900px全視窗 | `background: rgba(0, 0, 0, 0.7)` |
| **Device Popup** | QWidget | 804×603px視窗 × 0.67 | `background: #005ED7; border-radius: 20px; padding: 20px` |
| — 裝置圖示 | QSvgWidget | 35×35px | `ic_window_device.svg` |
| — 彈窗標題 | QLabel | — | `color: white; font-size: 32px; font-weight: bold`,文字:"Device Connection" |
| — 裝置列表Popup | QListWidget | min-height 250px | `background: rgba(255,255,255,0.1); border-radius: 10px; color: white`Item Selected`rgba(52,152,219,0.5)` |
| — 裝置列表項 | 自訂 QWidget | 60px 固定高 | 圖示容器 30×30px`#182D4B` 背景border-radius 5px+ 裝置名16px bold+ KN 號碼14px+ 狀態12px 右對齊) |
| — Refresh 按鈕 | QPushButton | 150×45px | 全域 `BUTTON_STYLE` |
| — Done 按鈕 | QPushButton | 150×45px | 全域 `BUTTON_STYLE` |
---
## ④ 互動規格
### 4.1 頁面切換方式Signal/Slot
```
SelectionScreen.open_utilities → 主程式切換到 LoginScreen
SelectionScreen.open_demo_app → 主程式切換到 MainWindow
LoginScreen.login_success → 主程式切換到 UtilitiesScreen
LoginScreen.back_to_selection → 主程式切換到 SelectionScreen
UtilitiesScreen.back_to_selection → 主程式切換到 SelectionScreen
```
> **⚠️ 待確認**:頁面切換的具體實作(是否使用 QStackedWidget還是直接 hide/show需查看 `main.py` 或應用程式進入點。
### 4.2 MainWindow 啟動流程
```
1. 顯示歡迎畫面Kneron Logo
2. 500ms 後 → 清除歡迎畫面,建立主頁面
3. 100ms 後 → 呼叫 device_controller.refresh_devices()
4. 顯示 Device Popup蒙版覆蓋主頁面
5. 500ms 後 → 自動啟動攝影機auto_start_camera
```
### 4.3 按鈕 Enabled/Disabled 條件
| 按鈕 | 停用條件 | 啟用條件 |
|------|---------|---------|
| Media Panel 圖示按鈕(相機) | 相機功能不可用時(`bt_function_camera_disabled.svg` 存在暗示) | ⚠️ 未在程式碼中找到明確的 setEnabled 邏輯 |
| Custom Model — Run Inference | 無(任何時候都可點擊,但點擊後會驗證檔案) | 有效檔案選擇後 |
| Custom Model — Stop | 無推論進行時重置檔案選擇 | — |
| UtilitiesScreen 操作按鈕 | ⚠️ 未找到明確的停用條件(選取裝置後才應啟用的邏輯有待確認) | — |
### 4.4 對話框觸發條件
| 對話框 | 觸發條件 | 類型 |
|--------|---------|------|
| Device Popup | MainWindow 啟動時自動顯示 | 自訂 QWidget 蒙版 |
| Inference Result | 推論回傳非 Bounding Box 結果(分類等)時 | QMessageBox |
| Device 相容性警告 | 選擇不相容裝置的 AI 工具時 | QMessageBox推斷 |
| 無裝置 GIF | 推斷:無裝置連接時 | QLabel + QMovie |
| Download Complete | 所有選中項目下載完成 | QMessageBox.information |
| No Selection 警告 | 點擊 Download 但未勾選任何項目 | QMessageBox.warning |
| 相機失敗提示 | 相機開啟失敗 | ⚠️ 程式碼中有 `ic_dialog_missing_camera.svg` 資源,但未見對應邏輯 |
### 4.5 UtilitiesScreen 頁面切換
| 觸發 | 行為 |
|------|------|
| 點擊「Utilities」導航按鈕 | `utilities_page.show()``purchased_items_page.hide()`Utilities 按鈕樣式變為選中(`#3498DB`Purchased 按鈕變為未選中(透明) |
| 點擊「Purchased Items」導航按鈕 | `purchased_items_page.show()``utilities_page.hide()` |
> **⚠️ 待確認**:目前程式碼中未見導航按鈕選中/未選中狀態的動態切換邏輯(按鈕樣式可能沒有在切換時更新)。
### 4.6 Custom Model 檔案上傳互動
| 步驟 | 互動 | 視覺變化 |
|------|------|---------|
| 點擊「...」按鈕 | 開啟系統檔案選擇器 | — |
| 選擇檔案成功 | 顯示檔案名稱(超過 15 字截斷加「...」) | 標籤顏色從 `#aaa` 變為 `#4CAF50`;邊框 `#4CAF50`;背景 `rgba(76,175,80,0.1)` |
| 點擊 Run Inference缺少必要檔案 | — | 狀態標籤文字更新為錯誤提示,顏色變為 `#ff6b6b` |
| 點擊 Run Inference所有必要檔案已選 | 啟動推論 | 狀態標籤顯示 "Running: {model_name}",顏色 `#4CAF50` |
| 點擊 Stop有推論進行中 | 停止推論 | 狀態標籤顯示 "Stopped & Disconnected",顏色 `#ff6b6b` |
| 點擊 Stop無推論 | 重置所有檔案選擇 | 所有 FileUploadRow 回復到「未選擇」狀態 |
---
## ⑤ 缺失的設計規格
### 5.1 互動細節(程式碼中看不出)
| 項目 | 說明 |
|------|------|
| Bounding Box 繪製樣式 | 推論結果繪製在 Canvas 上的 Bounding Box 顏色、線條粗細、Label 字型樣式均無 Design Spec從程式碼僅知座標格式 |
| 相機找不到時的 UI | 有 `ic_dialog_missing_camera.svg` 資源,但找不到對應的顯示邏輯 |
| 推論進行中的 Loading 狀態 | Canvas 載入文字為 "Starting camera...",但推論進行中是否有 Loading indicator 不清楚 |
| UtilitiesScreen 導航按鈕切換動態 | 頁面切換時按鈕選中狀態的樣式切換邏輯不完整 |
| 相機暫停後的 Canvas 顯示 | 暫停時 Canvas 顯示最後一幀還是空白?未知 |
| MSE 幀差異偵測的視覺提示 | 技術機制存在,但使用者是否看到任何提示?未知 |
| 推論 Queue 滿時的提示 | Queue 最大 5 幀,滿時的行為是靜默丟棄,無視覺提示 |
### 5.2 未完整實作的 UI 功能
| 項目 | 說明 |
|------|------|
| Driver 安裝 UI | `Install Driver` 按鈕存在,但 `install_drivers()` 方法的實際 UI 流程未見完整實作 |
| Register Device UI | `Register Device` 按鈕存在,`register_device()` 方法實作未確認 |
| Dongle 授權管理 UI | PRD 中有完整描述,但 UtilitiesScreen 中未見對應 UI 元素 |
| Toolbox原始工具箱| `create_ai_toolbox()` 元件存在但在 MainWindow 中已被 `CustomModelBlock` 取代Toolbox 功能未整合 |
| 購買模型下載(真實 API| 目前為 Mock 資料,`populate_mock_purchased_items()` 方法,無真實下載邏輯 |
| 自定義對話框 UI | `btn_dialog_customization_*``ic_dialog_customization.svg` 資源存在,但無對應 UI 元件 |
| 裝置斷線按鈕 UI | `btn_dialog_device_disconnect_*` 資源存在,但無對應 UI 元件 |
| 結果下載/資料夾 UI | `ic_result_download_*``ic_result_folder_*``btn_result_edit_*` 資源存在,但程式碼中大多未呼叫 |
| 設定頁面 | `btn_setting.svg` 存在,但無對應的設定頁面 |
### 5.3 設計一致性問題(觀察到的設計債)
| 問題 | 說明 | 嚴重程度 |
|------|------|---------|
| 兩種截然不同的設計語言 | MainWindow 使用深海軍藍(`#143058`為主的深色設計SelectionScreen、LoginScreen、UtilitiesScreen 使用淺色(`#F5F7FA`)設計。同一個應用存在兩套視覺語言,缺乏統一感 | High |
| Back 按鈕圖示資源缺失 | UtilitiesScreen Header 使用的 `Assets_png/back_arrow.png` 不存在於 `uxui/Assets_png/` 目錄,按鈕為空白 | High |
| 暫停按鈕圖示錯誤 | Media Panel 暫停按鈕使用 `btn_result_image_delete_hover.svg`(刪除圖示)作為暫停狀態,語意不正確 | Medium |
| 未定義系統字型 | 無明確的 `font-family` 定義,依賴 Windows 系統預設字型,跨平台(若未來支援)可能不一致 | Low |
| Dark Mode 未規劃 | 完全無 Dark Mode 設計 | Low |
| Disabled 按鈕視覺未完整定義 | UtilitiesScreen 按鈕幾乎未定義 Disabled 狀態樣式 | Medium |
| Focus 狀態不完整 | 大多數按鈕無 Focus ring 設計(僅 LoginScreen QLineEdit 有 Focus 邊框) | Medium |
---
*本文件由 Design Agent 從既有程式碼反向整理版本日期2026-04-04*

View File

@ -0,0 +1,970 @@
# 技術設計文件TDD— KNEO Academy v2.0
**作者**Architect Agent
**狀態**Draft從既有程式碼反向整理
**日期**2026-04-04
**對應 Design Doc**`04-architecture/design-doc.md`
---
## 目錄
1. [模組 API 規格](#1-模組-api-規格)
- 1.1 [config.py — 全域設定](#11-configpy--全域設定)
- 1.2 [AppControllermain.py](#12-appcontrollermainpy)
- 1.3 [DeviceController](#13-devicecontroller)
- 1.4 [InferenceController](#14-inferencecontroller)
- 1.5 [MediaController](#15-mediacontroller)
- 1.6 [InferenceWorkerThread](#16-inferenceworkerthread)
- 1.7 [CustomInferenceWorkerThread](#17-custominferenceworkerthread)
- 1.8 [VideoThread](#18-videothread)
- 1.9 [DeviceService](#19-deviceservice)
- 1.10 [FileService](#110-fileservice)
- 1.11 [ConfigUtils](#111-configutils)
- 1.12 [image_utils](#112-image_utils)
2. [InferenceWorkerThread 與 script.py 介面規範](#2-inferenceworkerthread-與-scriptpy-介面規範)
3. [裝置連接 API 流程kp SDK 呼叫順序)](#3-裝置連接-api-流程kp-sdk-呼叫順序)
4. [推論 Queue 設計](#4-推論-queue-設計)
5. [Signal/Slot 對應表](#5-signalslot-對應表)
6. [config.json 完整 Schema 定義](#6-configjson-完整-schema-定義)
7. [已知限制與邊界條件](#7-已知限制與邊界條件)
---
## 1. 模組 API 規格
### 1.1 config.py — 全域設定
**路徑**`src/config.py`
#### 常數
| 常數名稱 | 型別 | 說明 |
|---------|------|------|
| `APPDATA_PATH` | str | `%LOCALAPPDATA%` 環境變數值 |
| `PROJECT_ROOT` | str | 專案根目錄絕對路徑 |
| `UXUI_ASSETS` | str | `{PROJECT_ROOT}/uxui/` |
| `UTILS_DIR` | str | `%LOCALAPPDATA%/Kneron_Academy/utils` |
| `SCRIPT_CONFIG` | str | `{UTILS_DIR}/config.json` |
| `UPLOAD_DIR` | str | `%LOCALAPPDATA%/Kneron_Academy/uploads` |
| `FW_DIR` | str | `%LOCALAPPDATA%/Kneron_Academy/firmware` |
| `APP_NAME` | str | `"Innovedus AI Playground"` |
| `WINDOW_SIZE` | tuple[int, int] | `(1200, 900)` |
| `BACKGROUND_COLOR` | str | `"#143058"` |
| `MODEL_TIMEOUT` | int | `5000`(毫秒) |
| `FIRMWARE_PATHS` | dict | `{"scpu": "../../res/firmware/fw_scpu.bin", "ncpu": "..."}` ⚠️ 見備注 |
> **⚠️ 備注**`FIRMWARE_PATHS` 使用相對路徑,但在打包後此相對路徑可能無效,實際運作中 `DeviceController.connect_device()` 改用 `os.path.join(FW_DIR, dongle, "fw_scpu.bin")` 計算路徑。`FIRMWARE_PATHS` 常數目前**未被使用**。
#### DeviceType Enum
```python
class DeviceType(Enum):
KL520 = 256 # product_id hex: 0x100
KL720 = 1824 # product_id hex: 0x720
KL720_L = 512 # product_id hex: 0x200 無 DongleModelMap 對應)
KL530 = 530 # (⚠️ 無 DongleModelMap 對應)
KL832 = 832 # (⚠️ 無 DongleModelMap 對應)
KL730 = 732 # (⚠️ 無 DongleModelMap 對應)
KL630 = 630 # (⚠️ 無 DongleModelMap 對應)
KL540 = 540 # (⚠️ 無 DongleModelMap 對應)
```
#### DongleModelMapproduct_id → model name
```python
DongleModelMap = {
"0x100": "KL520",
"0x720": "KL720",
}
# 注意key 為 lowercase hex string含 0x 前綴),如 "0x100"
```
#### DongleIconMapproduct_id → icon 檔名)
```python
DongleIconMap = {
"0x100": "ic_dongle_520.png",
"0x720": "ic_dongle_720.png",
}
```
---
### 1.2 AppControllermain.py
**路徑**`main.py`
#### Class: AppController
```python
class AppController:
app: QApplication
stack: QStackedWidget
selection_screen: SelectionScreen
login_screen: LoginScreen
utilities_screen: UtilitiesScreen
main_window: MainWindow
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `__init__()` | — | — | 初始化 QApplication、QStackedWidget、所有頁面並連接 Signal |
| `init_screens()` | — | None | 建立四個頁面物件並加入 stack |
| `connect_signals()` | — | None | 連接頁面間的 Signal/Slot |
| `show_selection_screen()` | — | None | 切換到 SelectionScreen |
| `show_login_screen()` | — | None | 切換到 LoginScreen |
| `show_utilities_screen()` | — | None | 切換到 UtilitiesScreen |
| `show_demo_app()` | — | None | 切換到 MainWindow |
| `run()` | — | int | 啟動 Qt event loop回傳 exit code |
**⚠️ 已知問題**:所有頁面在 `init_screens()` 時一次性建立,包含 `MainWindow`(內部會啟動相機等重資源操作)。若希望延遲初始化,需要重構。
---
### 1.3 DeviceController
**路徑**`src/controllers/device_controller.py`
#### Class: DeviceController
```python
class DeviceController:
main_window: QWidget # 持有 UI 參考(用於更新 device_list_widget
selected_device: dict | None # 當前選中的裝置dict 格式)
connected_devices: list[dict] # 已解析的裝置列表
device_group: kp.DeviceGroup | None # kp SDK 連接的裝置群組
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `__init__(main_window)` | main_window: QWidget | — | 初始化 |
| `refresh_devices()` | — | bool | 掃描裝置並更新 UITrue = 找到裝置 |
| `parse_and_store_devices(devices)` | devices: list | None | 解析 descriptor 存入 `connected_devices` |
| `display_devices(devices)` | devices: list | None | 更新 `main_window.device_list_widget` |
| `get_devices()` | — | list | 呼叫掃描並回傳 descriptor list |
| `get_selected_device()` | — | dict \| None | 回傳目前選中的裝置 |
| `select_device(device, list_item, list_widget)` | device, item, widget | None | 選取裝置,更新 UI 高亮 |
| `connect_device()` | — | bool | 連接 selected_device 並上傳 firmware |
| `disconnect_device()` | — | bool | 中斷連接,釋放 device_group |
| `get_device_group()` | — | kp.DeviceGroup \| None | 回傳目前的 device_group |
#### connected_devices 元素格式
```python
{
"usb_port_id": int, # USB port 編號(唯一識別)
"product_id": int, # 數字形式的 product_id
"kn_number": int | str, # KN number裝置序號
"dongle": str # 裝置型號名稱,如 "KL520"
}
```
---
### 1.4 InferenceController
**路徑**`src/controllers/inference_controller.py`
#### Class: InferenceController
```python
class InferenceController:
main_window: QWidget
device_controller: DeviceController
inference_worker: InferenceWorkerThread | CustomInferenceWorkerThread | None
inference_queue: queue.Queue # maxsize=5
current_tool_config: dict | None
previous_tool_config: dict | None
_camera_was_active: bool
original_frame_width: int # 預設 640
original_frame_height: int # 預設 480
model_descriptor: kp.ModelDescriptor | None
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `__init__(main_window, device_controller)` | — | — | 初始化,建立 inference_queuemaxsize=5 |
| `select_tool(tool_config)` | tool_config: dict | bool | 選擇標準工具,建立 InferenceWorkerThread |
| `select_custom_tool(tool_config)` | tool_config: dict | bool | 選擇自定義模型,建立 CustomInferenceWorkerThread |
| `_clear_inference_queue()` | — | None | 清空 inference_queue 中所有幀 |
| `add_frame_to_queue(frame)` | frame: np.ndarray | None | 若 queue 未滿,加入幀;同時更新 original_frame_width/height |
| `stop_inference()` | — | None | 停止 inference_worker |
| `process_uploaded_image(file_path)` | file_path: str | None | 清空 queue 後加入指定圖片,供 Image 模式使用 |
#### select_tool() 的 tool_config 期望格式
```python
{
"display_name": str, # 顯示名稱
"mode": str, # 推論模式目錄名稱(對應 utils/{mode}/
"model_name": str, # 模型目錄名稱(對應 utils/{mode}/{model_name}/
"input_info": {
"type": "video" | "image" | "voice"
},
"input_parameters": dict, # 傳入 script.py 的 params
"compatible_devices": list[str], # 如 ["KL520", "KL720"]
"model_file": str, # .nef 檔名(可選,若有則自動上傳模型)
}
```
---
### 1.5 MediaController
**路徑**`src/controllers/media_controller.py`
#### Class: MediaController
```python
class MediaController:
main_window: QWidget
inference_controller: InferenceController
video_thread: VideoThread | None
recording: bool
recording_audio: bool
recorded_frames: list[np.ndarray]
_signal_was_connected: bool
_inference_paused: bool
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `start_camera()` | — | None | 建立 VideoThread 並連接 Signal若已運行則跳過 |
| `stop_camera()` | — | None | 斷開 Signal停止並銷毀 VideoThread |
| `update_image(qt_image)` | qt_image: QImage | None | 接收相機幀,繪製 Bounding Box更新 canvas加入 inference_queue |
| `reconnect_camera_signal()` | — | None | 重新連接已斷開的 change_pixmap_signal |
| `record_video(button=None)` | button: QPushButton \| None | None | 切換錄影狀態;停止時彈出儲存對話框 |
| `record_audio(button=None)` | button: QPushButton \| None | None | 切換錄音狀態 |
| `take_screenshot()` | — | None | 儲存 canvas 當前幀為圖片 |
| `toggle_inference_pause()` | — | bool | 切換推論暫停狀態,回傳新狀態 |
| `is_inference_paused()` | — | bool | 回傳目前是否暫停推論 |
---
### 1.6 InferenceWorkerThread
**路徑**`src/models/inference_worker.py`
#### 模組函數
```python
def load_inference_module(mode: str, model_name: str) -> module:
"""
動態載入 {UTILS_DIR}/{mode}/{model_name}/script.py 並回傳 module 物件。
若檔案不存在importlib 會拋出 FileNotFoundError。
"""
```
#### Class: InferenceWorkerThreadQThread
```python
class InferenceWorkerThread(QThread):
inference_result_signal = pyqtSignal(object) # 回傳 dict | None
frame_queue: queue.Queue
mode: str
model_name: str
min_interval: float # 最小推論間隔(秒)
mse_threshold: float # MSE 閾值,低於此值視為相似幀
_running: bool
once_mode: bool # True = 處理一幀後停止Image 模式)
last_inference_time: float
last_frame: np.ndarray | None
cached_result: object | None
input_params: dict
inference_module: module # 動態載入的 script.py module
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `__init__(frame_queue, mode, model_name, min_interval=0.5, mse_threshold=500, once_mode=False)` | — | — | 初始化並立即載入 inference_module |
| `run()` | — | None | 主迴圈:取幀 → MSE 判斷 → 推論 → emit 結果 |
| `stop()` | — | None | 設 `_running=False``wait()` |
**⚠️ 注意**`InferenceController.select_tool()` 建立 Worker 時,`min_interval` 設定為 **2 秒**(非預設的 0.5 秒):
```python
self.inference_worker = InferenceWorkerThread(
...,
min_interval=2,
mse_threshold=500,
once_mode=once_mode
)
```
#### run() 主迴圈邏輯
```
while _running:
1. frame_queue.get(timeout=0.1) — 若 0.1 秒內無幀則 continue
2. 時間間隔檢查current_time - last_inference_time < min_interval continue
3. MSE 比較(若 last_frame 不為 None 且尺寸相同):
- mse < mse_threshold AND cached_result 不為 None emit cached_result continue
4. inference_module.inference(frame, params=input_params) → result
5. 更新 last_inference_time、last_frame、cached_result
6. result 不為 None → inference_result_signal.emit(result)
7. once_mode = True → _running = Falsebreak
quit()
```
---
### 1.7 CustomInferenceWorkerThread
**路徑**`src/models/custom_inference_worker.py`
#### 輔助 Classes
```python
class ExampleBoundingBox:
x1: int; y1: int; x2: int; y2: int
score: float
class_num: int
def get_member_variable_dict() -> dict
class ExampleYoloResult:
class_count: int
box_count: int
box_list: list[ExampleBoundingBox]
def get_member_variable_dict() -> dict
```
#### 模組函數
```python
def preprocess_frame(frame: np.ndarray, target_size: int = 640) -> tuple[np.ndarray, int, int]:
"""
Args:
frame: BGR 格式 numpy array
target_size: 縮放目標尺寸(預設 640
Returns:
(frame_bgr565, original_width, original_height)
- frame_bgr565: cv2 BGR565 格式
"""
def postprocess(output_list, hw_preproc_info, original_width, original_height,
target_size=640, thresh=0.2) -> ExampleYoloResult:
"""
YOLO V5 後處理主入口
- 呼叫 post_process_yolo_v5()
- 將 bounding box 座標縮放回原始影像尺寸
"""
def post_process_yolo_v5(inference_float_node_output_list, hardware_preproc_info,
thresh_value, with_sigmoid=True) -> ExampleYoloResult:
"""
完整 YOLO V5 後處理:
- sigmoid 激活
- anchor 解碼(使用內建 YOLO_V5_ANCHERS
- padding/縮放補償
- 分數閾值過濾
- NMSper-classIoU threshold=0.5
回傳 ExampleYoloResult
"""
```
#### Class: CustomInferenceWorkerThreadQThread
```python
class CustomInferenceWorkerThread(QThread):
inference_result_signal = pyqtSignal(object) # 回傳 dict | None
frame_queue: queue.Queue
min_interval: float
mse_threshold: float
_running: bool
last_inference_time: float
last_frame: np.ndarray | None
cached_result: object | None
input_params: dict
device_group: kp.DeviceGroup | None # Worker 內部管理的裝置連接
model_descriptor: kp.ModelDescriptor | None
is_initialized: bool
custom_labels: list[str] | None
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `__init__(frame_queue, min_interval=0.5, mse_threshold=500)` | — | — | 初始化 |
| `initialize_device()` | — | bool | 連接裝置、上傳 firmware 和模型(從 input_params 讀取路徑) |
| `run_single_inference(frame)` | frame: np.ndarray | dict \| None | 執行單次 YOLOv5 推論,回傳結果 dict |
| `run()` | — | None | 主迴圈(類似 InferenceWorkerThread但無 once_mode |
| `cleanup()` | — | None | 斷開 device_group |
| `stop()` | — | None | 設 `_running=False``wait()`,然後 `cleanup()` |
#### initialize_device() 的 input_params 期望 Keys
```python
input_params = {
"custom_model_path": str, # .nef 完整路徑
"custom_scpu_path": str, # fw_scpu.bin 完整路徑
"custom_ncpu_path": str, # fw_ncpu.bin 完整路徑
"usb_port_id": int, # USB port 編號
"custom_labels": list[str] | None # 自定義類別名稱(可選)
}
```
#### run_single_inference() 回傳格式
```python
{
"num_boxes": int,
"bounding boxes": [[x1, y1, x2, y2], ...], # 注意key 有空格
"results": ["label1", "label2", ...]
}
```
**⚠️ 注意**key 為 `"bounding boxes"`(有空格),`MainWindow.handle_inference_result()` 中對應的處理 key 也是 `"bounding boxes"`(有空格),需保持一致。
#### YOLOv5 後處理常數
```python
YOLO_V3_CELL_BOX_NUM = 3
NMS_THRESH_YOLOV5 = 0.5
YOLO_MAX_DETECTION_PER_CLASS = 100
YOLO_V5_ANCHERS = np.array([
[[10, 13], [16, 30], [33, 23]],
[[30, 61], [62, 45], [59, 119]],
[[116, 90], [156, 198], [373, 326]]
])
```
---
### 1.8 VideoThread
**路徑**`src/models/video_thread.py`
#### Class: VideoThreadQThread
```python
class VideoThread(QThread):
change_pixmap_signal = pyqtSignal(QImage) # 每幀觸發
camera_error_signal = pyqtSignal(str) # 目前未被任何 Slot 連接
_run_flag: bool
_camera_open_attempts: int
_max_attempts: int # = 3
_camera_timeout: int # = 5
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `_open_camera_with_timeout(camera_index, backend=None)` | camera_index: int, backend | cv2.VideoCapture \| None | 在 daemon thread 中開啟相機5 秒 timeout |
| `run()` | — | None | 嘗試最多 3 次開啟相機;成功後進入幀擷取迴圈 |
| `stop()` | — | None | 設 `_run_flag=False``wait()` |
#### 相機設定
```python
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 最小緩衝以降低延遲
```
開啟順序:
1. 優先嘗試 DirectShow backend`cv2.CAP_DSHOW`
2. 失敗則嘗試預設 backend
3. 兩者都失敗則等 1 秒後重試(最多 3 次)
幀格式轉換:
```
BGROpenCV 預設) → RGBcv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
→ QImage.Format_RGB888
→ change_pixmap_signal.emit(qt_image.copy())
```
---
### 1.9 DeviceService
**路徑**`src/services/device_service.py`
#### Class: EmptyDescriptor
```python
class EmptyDescriptor:
device_descriptor_number: int = 0
device_descriptor_list: list = []
```
#### 函數
```python
def check_available_device(timeout: float = 5.0) -> kp.DeviceDescriptors | EmptyDescriptor:
"""
在 daemon thread 中執行 kp.core.scan_devices(),設 timeout=5.0 秒。
Returns:
- 成功kp.DeviceDescriptors含 device_descriptor_number 和 device_descriptor_list
- 失敗/Timeout/無裝置EmptyDescriptordevice_descriptor_number = 0
注意事項:
- 使用 threading.Thread非 QThread
- thread 設為 daemon=True即使 timeout 後 thread 仍可能繼續執行
"""
```
---
### 1.10 FileService
**路徑**`src/services/file_service.py`
#### Class: FileService
```python
class FileService:
main_window: QWidget
upload_dir: str # = UPLOAD_DIR%LOCALAPPDATA%/Kneron_Academy/uploads
destination: str | None # 最近上傳的檔案完整路徑
_camera_was_active: bool
```
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `upload_file()` | — | str \| None | 開啟檔案選擇器,複製檔案,觸發推論;回傳目的路徑 |
| `show_message(icon, title, message)` | — | None | 顯示自訂樣式的 QMessageBox |
#### upload_file() 完整流程
```
1. 若相機正在運行:
a. 暫停推論toggle_inference_pause()
b. 斷開 change_pixmap_signal保留 VideoThread不停止
2. QFileDialog.getOpenFileName()
3. 若使用者取消 → return None
4. 確認/建立 upload_dir
5. 檢查來源檔案存在
6. 測試目的地寫入權限
7. shutil.copy2(source, destination)
8. 更新 main_window.destination
9. 若 inference_controller.current_tool_config 存在:
a. 讀取並顯示圖片於 canvas_label
b. inference_controller.process_uploaded_image(destination)
10. finally若相機之前活動重新連接 Signal 並恢復推論
```
---
### 1.11 ConfigUtils
**路徑**`src/utils/config_utils.py`
#### Class: ConfigUtils
| 方法 | 參數 | 回傳 | 說明 |
|------|------|------|------|
| `generate_global_config()` | — | dict | 掃描 UTILS_DIR產生 config.json |
| `create_model_config_template(model_path)` | model_path: str | bool | 為指定模型目錄建立 config.json 範本 |
#### generate_global_config() 輸出範例
```json
{
"plugins": [
{
"mode": "object_detection",
"display_name": "Object Detection",
"models": [
{
"name": "yolov5_coco",
"display_name": "Yolov5 Coco",
"description": "YOLOv5 COCO object detection",
"compatible_devices": ["KL520", "KL720"]
}
]
}
]
}
```
---
### 1.12 image_utils
**路徑**`src/utils/image_utils.py`
```python
def qimage_to_numpy(qimage: QImage) -> np.ndarray:
"""
將 QImage 轉換為 numpy array。
先強制轉換為 QImage.Format_RGB888確保格式一致
Returns:
np.ndarray形狀 (H, W, 3)dtype=uint8RGB 格式
注意:使用 qimage.bits() 直接存取記憶體,需確保 qimage 在 array 使用期間有效。
"""
```
---
## 2. InferenceWorkerThread 與 script.py 介面規範
### 2.1 script.py 必須實作的函數
每個 Plugin 的 `script.py` **必須** 實作以下函數:
```python
def inference(frame: np.ndarray, params: dict) -> dict | None:
...
```
`InferenceWorkerThread` 呼叫方式:
```python
result = self.inference_module.inference(frame, params=self.input_params)
```
### 2.2 `frame` 參數規格
| 屬性 | 值 |
|------|-----|
| 型別 | `np.ndarray` |
| 形狀 | `(H, W, 3)` |
| dtype | `uint8` |
| 色彩空間 | RGBVideoThread 輸出)|
| 典型尺寸 | 640 x 480 |
### 2.3 `params` 字典完整 Keys
以下 keys 由 `InferenceController.select_tool()` 注入,加上 model `config.json``input_parameters`
| Key | 型別 | 必填 | 說明 |
|-----|------|------|------|
| `device_group` | kp.DeviceGroup \| None | 是 | kp SDK 連接的裝置群組 |
| `usb_port_id` | int | 是 | USB port 編號 |
| `scpu_path` | str | 否 | SCPU firmware 完整路徑 |
| `ncpu_path` | str | 否 | NCPU firmware 完整路徑 |
| `model` | str | 否 | .nef 模型完整路徑(若 config.json 有 model_file |
| `model_descriptor` | kp.ModelDescriptor \| None | 否 | 已上傳的模型描述(若有 model_file |
| `file_path` | str | 否 | image/voice 模式的上傳檔案路徑 |
| (自訂)| any | 否 | 來自 model config.json 的 `input_parameters` 欄位 |
### 2.4 回傳值格式
#### 格式 A無 Bounding Box分類/其他結果)
觸發 `QMessageBox` 顯示:
```python
return {
"result": "class_name",
# 任意 key-value 對
}
```
#### 格式 B單一 Bounding Box舊格式仍相容
直接繪製於畫面:
```python
return {
"bounding box": [x1, y1, x2, y2], # 整數,像素座標
"result": "class_label" # 可選
}
```
#### 格式 C多個 Bounding Box推薦格式
直接繪製於畫面:
```python
return {
"bounding boxes": [ # 注意 key 有空格
[x1, y1, x2, y2], # 整數,像素座標
[x1, y1, x2, y2],
...
],
"results": [ # 可選,與 bounding boxes 對應
"label1",
"label2",
...
]
}
```
#### 回傳 None
表示此幀跳過,不更新 Bounding Box不顯示 QMessageBox。
---
## 3. 裝置連接 API 流程kp SDK 呼叫順序)
### 3.1 DeviceController.connect_device() 流程
```python
# 1. 掃描(在 refresh_devices() 時已完成)
descriptors = kp.core.scan_devices()
# descriptors.device_descriptor_number: int
# descriptors.device_descriptor_list: list
# 2. 連接
device_group = kp.core.connect_devices(usb_port_ids=[usb_port_id])
# 3. 上傳 firmware
kp.core.load_firmware_from_file(
device_group=device_group,
scpu_fw_path=scpu_path, # 完整路徑
ncpu_fw_path=ncpu_path # 完整路徑
)
# 4. 上傳模型(在 InferenceController.select_tool() 中,非 DeviceController
model_descriptor = kp.core.load_model_from_file(
device_group=device_group,
file_path=model_file_path
)
```
### 3.2 CustomInferenceWorkerThread.initialize_device() 流程
```python
# 完整的連接 + firmware + model 上傳
device_group = kp.core.connect_devices(usb_port_ids=[port_id])
kp.core.set_timeout(device_group=device_group, milliseconds=5000)
kp.core.load_firmware_from_file(device_group, scpu_path, ncpu_path)
model_descriptor = kp.core.load_model_from_file(device_group, file_path=model_path)
```
### 3.3 推論 API 呼叫順序CustomInferenceWorkerThread
```python
# 建立推論描述符
descriptor = kp.GenericImageInferenceDescriptor(
model_id=model_descriptor.models[0].id,
inference_number=0,
input_node_image_list=[
kp.GenericInputNodeImage(
image=img_bgr565,
image_format=kp.ImageFormat.KP_IMAGE_FORMAT_RGB565,
resize_mode=kp.ResizeMode.KP_RESIZE_ENABLE,
padding_mode=kp.PaddingMode.KP_PADDING_CORNER,
normalize_mode=kp.NormalizeMode.KP_NORMALIZE_KNERON
)
]
)
# 非同步發送推論請求
kp.inference.generic_image_inference_send(device_group, descriptor)
# 接收推論結果(阻塞式)
result = kp.inference.generic_image_inference_receive(device_group)
# result.header.num_output_node: int
# result.header.hw_pre_proc_info_list: list
# 取得每個輸出節點的 float 值
for node_idx in range(result.header.num_output_node):
node_output = kp.inference.generic_inference_retrieve_float_node(
node_idx=node_idx,
generic_raw_result=result,
channels_ordering=kp.ChannelOrdering.KP_CHANNEL_ORDERING_CHW
)
# node_output.shape: (batch, channels, height, width)
# node_output.ndarray: numpy array
```
### 3.4 中斷連接
```python
kp.core.disconnect_devices(device_group=device_group)
```
---
## 4. 推論 Queue 設計
### 4.1 基本規格
| 屬性 | 值 |
|------|-----|
| 類型 | `queue.Queue` |
| maxsize | **5** |
| 生產者 | `MediaController.update_image()`VideoThread 幀)、`InferenceController.process_uploaded_image()`(圖片)、`InferenceController.select_tool()`(初始圖片) |
| 消費者 | `InferenceWorkerThread.run()``CustomInferenceWorkerThread.run()` |
### 4.2 滿了怎麼辦drop policy
當 queue 滿時,**新幀被靜默丟棄**
```python
# add_frame_to_queue() 的邏輯
if not self.inference_queue.full():
self.inference_queue.put(frame)
else:
print("Warning: inference queue is full") # 只有 printUI 不更新
```
**⚠️ 問題**:這是 `put_nowait` 語意,但實際上是 `if not full` 檢查後再 `put`,在多執行緒環境中存在微小的 race windowcheck-then-act
### 4.3 清空時機
以下情況會清空 queue
| 觸發 | 方法 | 說明 |
|------|------|------|
| 切換工具 | `InferenceController.select_tool()` 開頭 | 避免舊工具的幀被新 Worker 處理 |
| 切換自定義工具 | `InferenceController.select_custom_tool()` 開頭 | 同上 |
| 上傳圖片 | `InferenceController.process_uploaded_image()` 開頭 | 確保只處理最新上傳的圖片 |
### 4.4 Worker 從 Queue 取幀的行為
```python
frame = self.frame_queue.get(timeout=0.1)
# - 若 0.1 秒無幀,拋出 queue.Empty → continue
# - 不 blocking僅等 0.1 秒)
# - 不呼叫 task_done()queue 未使用 join 功能)
```
---
## 5. Signal/Slot 對應表
### 5.1 頁面導航 SignalAppController 層)
| Signal | 發出 Class | Slot | 說明 |
|--------|-----------|------|------|
| `open_utilities` | SelectionScreen | AppController.show_login_screen | 點擊 Utilities 卡片 |
| `open_demo_app` | SelectionScreen | AppController.show_demo_app | 點擊 Demo App 卡片 |
| `login_success` | LoginScreen | AppController.show_utilities_screen | 登入成功 |
| `back_to_selection` | LoginScreen | AppController.show_selection_screen | 點擊 Back |
| `back_to_selection` | UtilitiesScreen | AppController.show_selection_screen | 點擊 Back |
### 5.2 媒體與推論 SignalMainWindow 層)
| Signal | 型別 | 發出 Class | Slot | 說明 |
|--------|------|-----------|------|------|
| `change_pixmap_signal` | QImage | VideoThread | MediaController.update_image | 相機每幀 |
| `camera_error_signal` | str | VideoThread | **未連接** | ⚠️ 無 Slot 接收 |
| `inference_result_signal` | object | InferenceWorkerThread | MainWindow.handle_inference_result | 推論完成 |
| `inference_result_signal` | object | CustomInferenceWorkerThread | MainWindow.handle_inference_result | 推論完成 |
**⚠️ 注意**`VideoThread.camera_error_signal` 目前沒有任何 Slot 連接,相機錯誤不會通知 UI。
### 5.3 UtilitiesScreen 內部 Signal
| Signal | 發出 Class | 說明 |
|--------|-----------|------|
| `back_to_selection` | UtilitiesScreen | 點擊 Back 按鈕 |
---
## 6. config.json 完整 Schema 定義
### 6.1 全域 config.jsonSCRIPT_CONFIG
路徑:`%LOCALAPPDATA%/Kneron_Academy/utils/config.json`
`ConfigUtils.generate_global_config()` 自動產生,不應手動修改。
```json
{
"plugins": [
{
"mode": "string", // 模式目錄名稱(如 "object_detection"
"display_name": "string", // UI 顯示名稱(自動從 mode 名稱產生)
"models": [
{
"name": "string", // 模型目錄名稱(如 "yolov5_coco"
"display_name": "string", // UI 顯示名稱(來自 model config.json
"description": "string", // 描述(來自 model config.json
"compatible_devices": ["string"] // 如 ["KL520", "KL720"]
}
]
}
]
}
```
### 6.2 模型層級 config.json
路徑:`%LOCALAPPDATA%/Kneron_Academy/utils/{mode}/{model_name}/config.json`
由 Plugin 開發者提供。
```json
{
"display_name": "string", // 必填UI 顯示名稱
"description": "string", // 選填:模型描述
"model_file": "string", // 選填:.nef 檔名(若有則 InferenceController 自動上傳)
"input_info": {
"type": "video" | "image" | "voice", // 必填:輸入類型
"supported_formats": ["string"] // 選填:支援的格式(目前主要用於文件)
},
"input_parameters": {
// 選填:任意 key-value會被合併進 input_params 傳給 script.py
"threshold": 0.5,
// 可包含任何 script.py 需要的參數
},
"compatible_devices": ["string"] // 必填:如 ["KL520", "KL720"]
}
```
### 6.3 ConfigUtils.create_model_config_template() 產生的範本
```json
{
"display_name": "Model Name",
"description": "AI model for model_name",
"model_file": "model_name.nef",
"input_info": {
"type": "video",
"supported_formats": ["mp4", "avi"]
},
"input_parameters": {
"threshold": 0.5
},
"compatible_devices": ["KL520", "KL720"]
}
```
---
## 7. 已知限制與邊界條件
### 7.1 裝置相關限制
| 限制 | 描述 |
|------|------|
| 僅支援 KL520 / KL720 | `DongleModelMap` 只有這兩個映射;其他型號被標注為 "unknown" |
| 單一裝置 | `connect_devices(usb_port_ids=[single_id])`,不支援多裝置並行推論 |
| Firmware 必須預先放置 | `connect_device()` 不自動下載 firmware`%LOCALAPPDATA%/Kneron_Academy/firmware/{dongle}/` 下不存在,回傳 False |
| kp SDK timeout | `CustomInferenceWorkerThread.initialize_device()` 設定 timeout=5000ms`DeviceController.connect_device()` 未設定 timeout |
### 7.2 相機相關限制
| 限制 | 描述 |
|------|------|
| 僅支援 camera index 0 | `cv2.VideoCapture(0, cv2.CAP_DSHOW)` — 硬體寫死 |
| 最多嘗試 3 次 | `_max_attempts=3`;若 3 次後仍無法開啟Worker 靜默退出,無 UI 通知 |
| 解析度固定 640x480 | `cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)` 固定設定 |
| camera_error_signal 未連接 | 相機錯誤只有 print使用者不知道相機失敗 |
### 7.3 推論相關限制
| 限制 | 描述 |
|------|------|
| Custom Model 僅支援 YOLOv5 | `CustomInferenceWorkerThread` 的後處理 hardcode 了 YOLOv5 的 anchor 和 NMS 邏輯 |
| COCO 80 類 | 預設使用 COCO 80 類別;需自定義類別時必須傳入 `custom_labels` |
| Voice 模式未完整實作 | `input_info.type = "voice"` 的路徑只傳 `file_path`,無音訊處理邏輯在主程式 |
| Image 模式一次只能處理一張 | `once_mode=True` 後 Worker 自動停止,需重新建立才能推論下一張 |
| MSE 比較跨 frame 尺寸變化 | 若相機解析度切換MSE 比較因形狀不一致而 reset功能正確但可能不符預期 |
| Script module 快取問題 | `load_inference_module()` 每次切換工具都重新 `exec_module()`若同一模組切換多次Python module cache 可能有舊版本 |
### 7.4 Plugin 開發注意事項
| 事項 | 說明 |
|------|------|
| script.py 執行在 Worker Thread 中 | 不可在 script.py 中直接呼叫 PyQt UI 方法 |
| script.py 不應長時間阻塞 | 若推論超過 `min_interval`,下一幀會等待 queue 中的幀累積 |
| Bounding Box 座標系 | 座標為相對於 `frame`QImage 轉出的 numpy array的像素座標若輸入解析度是 640x480座標範圍為 0-639 / 0-479 |
| script.py 例外處理 | `InferenceWorkerThread``try/except` 會捕捉 `script.inference()` 的例外,設 result=None 並繼續;不會中斷 Worker |
### 7.5 打包相關邊界條件
| 條件 | 問題 |
|------|------|
| PyInstaller + kp SDK | kp 為 C Extension需在 `.spec` 中手動設定 `hiddenimports` |
| UXUI_ASSETS 路徑 | 開發模式下透過 `__file__` 計算;打包後需改用 `sys._MEIPASS` |
| `FIRMWARE_PATHS` 常數 | 使用相對路徑,打包後無效;實際使用的是 `FW_DIR` + dongle 名稱(正確) |
| script.py 動態載入 | Plugin 的 `script.py` 必須放在 `%LOCALAPPDATA%`(外部目錄),不能打包進 exe |

View File

@ -0,0 +1,569 @@
# Design Doc: KNEO AcademyInnovedus AI Playgroundv2.0
**作者**Architect Agent
**狀態**Draft從既有程式碼反向整理
**日期**2026-04-04
**產品版本**v2.0
---
## 1. 背景與目標
### 1.1 產品概述
KNEO Academy對外名稱Innovedus AI Playground是一套 Windows 桌面應用程式,讓擁有 Kneron NPU USB Dongle 的使用者能夠在本機端執行 Edge AI 推論,無需雲端服務、無需撰寫程式碼。
**核心使用情境**
- 業務展示:即插即用,開箱展示 AI 推論能力
- 研發驗證:快速測試 Kneron NPU 在特定任務的推論效果
- 客戶自訂:上傳自訂 `.nef` 模型進行推論測試
### 1.2 設計目標
- **即插即用**:連接 Kneron dongle 後,數秒內可開始推論
- **Plugin 化架構**:透過目錄結構 + `config.json` + `script.py` 新增模型,不需修改主程式
- **Thread 隔離**UI 執行緒與推論執行緒完全分離,確保畫面不卡頓
- **PyInstaller 相容**:可打包為單一可執行檔,方便分發
### 1.3 非目標Out of Scope
- 雲端推論或 API 服務
- 多裝置同時推論
- 行動平台支援
---
## 2. 整體架構概覽
### 2.1 設計模式
本應用程式採用 **MVCModel-View-Controller** 架構,搭配 Qt 的 **Signal/Slot** 機制實現跨執行緒通訊。
```
┌─────────────────────────────────────────────────────────────────┐
│ Views呈現層
│ SelectionScreen LoginScreen MainWindow UtilitiesScreen │
│ QWidget QWidget QWidget QWidget │
└────────────────────────────┬────────────────────────────────────┘
│ Signal/Slot
┌────────────────────────────▼────────────────────────────────────┐
│ AppControllermain.py
│ QStackedWidget — 頁面路由中樞 │
└──────┬──────────────────┬───────────────────┬───────────────────┘
│ │ │
┌──────▼──────┐ ┌────────▼────────┐ ┌──────▼──────────┐
│ Device │ │ Inference │ │ Media │
│ Controller │ │ Controller │ │ Controller │
│ (裝置管理) │ │ (推論管理) │ │ (相機/媒體) │
└──────┬──────┘ └───────┬─────────┘ └──────┬──────────┘
│ │ │
┌──────▼──────┐ ┌───────▼─────────┐ ┌──────▼──────────┐
│ device_ │ │ InferenceWorker │ │ VideoThread │
│ service.py │ │ Thread │ │ QThread
kp SDK │ │ QThread │ │ OpenCV 擷取) │
└─────────────┘ └────────┬────────┘ └─────────────────┘
│ 動態載入
┌────────▼────────┐
│ script.py │
Plugin 推論) │
└─────────────────┘
```
### 2.2 頁面導航架構
AppController 使用 `QStackedWidget` 作為根容器,所有頁面在啟動時一次性初始化,透過 `setCurrentWidget()` 切換顯示,不需重新建立物件。
```
AppController
└── QStackedWidgetstack
├── [index 0] SelectionScreen ← 預設顯示
├── [index 1] LoginScreen
├── [index 2] UtilitiesScreen
└── [index 3] MainWindow
```
**Signal 連接關係(頁面切換)**
| 發出者 | Signal | 接收者Slot | 效果 |
|--------|--------|--------------|------|
| SelectionScreen | `open_utilities` | `AppController.show_login_screen` | 跳至登入頁 |
| SelectionScreen | `open_demo_app` | `AppController.show_demo_app` | 跳至主視窗 |
| LoginScreen | `login_success` | `AppController.show_utilities_screen` | 登入成功,進入工具頁 |
| LoginScreen | `back_to_selection` | `AppController.show_selection_screen` | 返回首頁 |
| UtilitiesScreen | `back_to_selection` | `AppController.show_selection_screen` | 返回首頁 |
### 2.3 MainWindow 內部架構
MainWindow 是 AI Demo 推論的核心容器,內部持有三個 Controller 組成協作關係:
```
MainWindowQWidget
├── DeviceController — 管理 kp SDK 裝置連接
├── InferenceController — 管理推論 Worker Thread 與 Queue
│ └── inference_queuequeue.Queue, maxsize=5
└── MediaController — 管理相機擷取與畫面更新
└── VideoThreadQThread
```
---
## 3. 模組依賴圖
```
main.pyAppController
├── views/selection_screen.pySelectionScreen
│ └── config.py
├── views/login_screen.pyLoginScreen
│ └── config.py
├── views/utilities_screen.pyUtilitiesScreen
│ ├── config.py
│ ├── controllers/device_controller.pyDeviceController
│ └── services/device_service.pycheck_available_device
└── views/mainWindows.pyMainWindow
├── config.py
├── controllers/device_controller.pyDeviceController
│ ├── config.py
│ └── services/device_service.py
├── controllers/inference_controller.pyInferenceController
│ ├── config.py
│ ├── models/inference_worker.pyInferenceWorkerThread
│ │ ├── config.py
│ │ └── [動態載入] utils/{mode}/{model}/script.py
│ └── models/custom_inference_worker.pyCustomInferenceWorkerThread
├── controllers/media_controller.pyMediaController
│ ├── models/video_thread.pyVideoThread
│ └── utils/image_utils.py
├── services/file_service.pyFileService
└── utils/config_utils.pyConfigUtils
```
**注意**`UtilitiesScreen` 建立了**自己的** `DeviceController` 實例(與 `MainWindow` 的是不同物件),兩者不共享裝置狀態。
---
## 4. 資料流程圖
### 4.1 Video 即時推論流程
```
相機硬體
│ 每幀(~30fps
VideoThread.run()
│ QImage
│ change_pixmap_signal.emit(qt_image)
MediaController.update_image(qt_image)
├── 1. 繪製 Bounding Box → canvas_label.setPixmap(pixmap)
└── 2. qimage_to_numpy(qt_image) → frame_np
InferenceController.add_frame_to_queue(frame_np)
│ 若 queue 未滿maxsize=5
inference_queue.put(frame_np)
InferenceWorkerThread.run()
├── 1. MSE 比較(與前一幀)→ 差異不大時emit 快取結果
├── 2. 時間間隔檢查min_interval=2秒
└── 3. script.inference(frame, params) → result
│ inference_result_signal.emit(result)
MainWindow.handle_inference_result(result)
├── 若有 "bounding box"/"bounding boxes"
│ → 更新 current_bounding_boxes下一幀繪製
└── 若無 bounding box → QMessageBox 彈出顯示
```
### 4.2 Image 推論流程
```
使用者點擊「Upload」
FileService.upload_file()
├── 1. 暫停相機 Signaldisconnect change_pixmap_signal
├── 2. QFileDialog 選檔
├── 3. shutil.copy2() → %LOCALAPPDATA%/Kneron_Academy/uploads/
├── 4. 顯示圖片於 canvas_label
└── 5. InferenceController.process_uploaded_image(file_path)
_clear_inference_queue()
inference_queue.put(img)
InferenceWorkerThreadonce_mode=True
│ 只處理一幀後停止
script.inference(frame, params) → result
│ inference_result_signal.emit(result)
MainWindow.handle_inference_result(result)
```
### 4.3 Custom Model 推論流程
```
使用者提供:
- custom_model_path.nef 檔)
- custom_scpu_pathfw_scpu.bin
- custom_ncpu_pathfw_ncpu.bin
- custom_labels可選
InferenceController.select_custom_tool(tool_config)
CustomInferenceWorkerThreadQThread
├── initialize_device()(首次執行時)
│ ├── kp.core.connect_devices([port_id])
│ ├── kp.core.load_firmware_from_file(scpu, ncpu)
│ └── kp.core.load_model_from_file(model_path)
└── run_single_inference(frame)
├── preprocess_frame()resize to 640, BGR → BGR565
├── kp.GenericImageInferenceDescriptor
├── kp.inference.generic_image_inference_send()
├── kp.inference.generic_image_inference_receive()
├── kp.inference.generic_inference_retrieve_float_node()
└── post_process_yolo_v5() → ExampleYoloResult
│ inference_result_signal.emit(result_dict)
MainWindow.handle_inference_result()
```
**注意**`CustomInferenceWorkerThread` 在 Worker Thread 內部**自行連接/重置裝置**,與 `DeviceController` 管理的 `device_group` 是**不同的連接**。這是一個雙重連接問題(見第 8 節技術問題)。
---
## 5. Thread 架構
### 5.1 執行緒關係圖
```
Qt Main ThreadUI Thread
├── AppControllerQStackedWidget 管理)
├── MainWindowUI 事件處理)
│ ├── handle_inference_result() ← 由 Signal 呼叫(執行在主執行緒)
│ └── update_image() via MediaController ← 由 Signal 呼叫(執行在主執行緒)
├── VideoThreadQThread #1
│ └── 職責相機擷取、QImage 轉換、emit change_pixmap_signal
│ └── 內部threading.Thread用於相機開啟 timeout 機制)
├── InferenceWorkerThreadQThread #2
│ └── 職責:從 queue 取幀、MSE 比較、呼叫 script.inference()、emit 結果
└── CustomInferenceWorkerThreadQThread #3,替代 InferenceWorkerThread
└── 職責device init、kp 推論、YOLOv5 後處理、emit 結果
```
### 5.2 裝置掃描的執行緒
`check_available_device()` 使用 `threading.Thread`(非 QThread執行 `kp.core.scan_devices()`,並設 5 秒 timeout
```
check_available_device() in Main Thread
└── threading.Threaddaemon=True
└── kp.core.scan_devices()(阻塞式 SDK 呼叫)
thread.join(timeout=5.0)
```
同樣地,`VideoThread._open_camera_with_timeout()` 也使用 `threading.Thread` 開啟相機timeout 為 5 秒。
### 5.3 跨執行緒通訊
所有跨執行緒通訊均透過 Qt Signal/Slot 機制Qt 保證跨執行緒的 Signal 會在接收執行緒的 Event Loop 中排隊執行:
| Signal | 發出執行緒 | 接收執行緒Slot |
|--------|-----------|----------------|
| `VideoThread.change_pixmap_signal` | VideoThread | Main Thread`MediaController.update_image` |
| `InferenceWorkerThread.inference_result_signal` | InferenceWorkerThread | Main Thread`MainWindow.handle_inference_result` |
| `CustomInferenceWorkerThread.inference_result_signal` | CustomInferenceWorkerThread | Main Thread`MainWindow.handle_inference_result` |
---
## 6. Plugin 系統設計
### 6.1 架構概念
Plugin 系統讓 Kneron 或第三方可以透過放置目錄和設定檔來新增 AI 工具,完全不需修改主程式碼。
### 6.2 目錄結構
```
%LOCALAPPDATA%/Kneron_Academy/utils/
├── config.json ← 全域 Plugin 索引(自動產生)
├── {mode_name}/ ← 推論模式目錄(如 object_detection
│ └── {model_name}/ ← 模型目錄(如 yolov5_person
│ ├── config.json ← 模型設定
│ ├── script.py ← 推論腳本Plugin 核心)
│ └── {model_name}.nef ← Kneron 模型檔
```
### 6.3 Plugin 載入流程
```
應用程式啟動
ConfigUtils.generate_global_config()
├── 掃描 utils/ 下所有 mode 目錄(跳過 _ 開頭的目錄)
├── 掃描每個 mode 下所有 model 目錄
├── 讀取每個 model/config.json
└── 輸出 utils/config.jsonPlugin 索引)
使用者選擇工具
InferenceController.select_tool(tool_config)
InferenceWorkerThread.__init__()
└── load_inference_module(mode, model_name)
└── importlib.util.spec_from_file_location()
→ 動態 import utils/{mode}/{model}/script.py
```
### 6.4 `script.py` 介面規範
每個 Plugin 的 `script.py` 必須實作以下介面:
```python
def inference(frame: np.ndarray, params: dict) -> dict | None:
"""
Args:
frame: 影像幀numpy array形狀 (H, W, 3)RGB 格式
params: 推論參數字典(詳見 config.json schema
Returns:
dict 或 NoneNone 表示跳過此幀)
支援的回傳格式(在 handle_inference_result 中處理):
格式 A單一 Bounding Box
{
"bounding box": [x1, y1, x2, y2],
"result": "class_label"
}
格式 B多個 Bounding Box推薦
{
"bounding boxes": [[x1, y1, x2, y2], ...],
"results": ["label1", "label2", ...]
}
格式 C任意分類結果彈出 QMessageBox 顯示)
{
"key": "value",
...
}
"""
```
### 6.5 `params` 字典的內容
`InferenceController.select_tool()` 在建立 `InferenceWorkerThread` 前,會將以下資訊注入 `input_params`
| Key | 型別 | 來源 |
|-----|------|------|
| `device_group` | kp.DeviceGroup | DeviceController |
| `usb_port_id` | int | 已連接裝置 |
| `scpu_path` | str | firmware 路徑 |
| `ncpu_path` | str | firmware 路徑 |
| `model` | str | model .nef 完整路徑 |
| `model_descriptor` | kp.ModelDescriptor | 已上傳的模型描述 |
| `file_path` | str | 圖片/聲音模式的上傳檔案路徑 |
| (其他) | any | 來自 model config.json 的 `input_parameters` |
---
## 7. 資料存放設計
### 7.1 執行期資料目錄
全部存放於 Windows 的 `%LOCALAPPDATA%\Kneron_Academy\`
```
%LOCALAPPDATA%\Kneron_Academy\
├── utils\
│ ├── config.json ← Plugin 全域索引(啟動時自動產生)
│ ├── {mode}\
│ │ └── {model}\
│ │ ├── config.json ← 模型設定
│ │ ├── script.py ← 推論腳本
│ │ └── *.nef ← 模型檔
│ └── ...
├── uploads\ ← 使用者上傳的圖片(不自動清理)
│ └── *.jpg / *.png / *.wav / ...
└── firmware\
├── KL520\
│ ├── fw_scpu.bin
│ └── fw_ncpu.bin
└── KL720\
├── fw_scpu.bin
└── fw_ncpu.bin
```
### 7.2 靜態 UI 資源
打包在應用程式內(`uxui/` 目錄),路徑透過 `PROJECT_ROOT` 常數計算:
```python
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
UXUI_ASSETS = os.path.join(PROJECT_ROOT, "uxui", "")
```
**PyInstaller 注意**:打包後 `__file__` 的位置會改變,需確認 `UXUI_ASSETS` 路徑在打包後仍正確(詳見第 8.3 節)。
---
## 8. 打包架構PyInstaller
### 8.1 打包工具鏈
- **PyInstaller 6.12.0**:將 Python 應用打包為 Windows .exe
- **Inno Setup**`dist/test.iss`):製作 Windows 安裝包 (.exe installer)
- **PyArmor**(計畫中):混淆/加密 Python 原始碼
### 8.2 打包注意事項
| 項目 | 問題 | 解決方向 |
|------|------|---------|
| `kp` SDK | kp 是 C Extension需確認是否能被 PyInstaller 正確打包 | 需設定 `hiddenimports``binaries` |
| 動態 import | `importlib.util.spec_from_file_location()` 在打包後需從外部路徑載入 | `script.py` 必須放在 `%LOCALAPPDATA%`,不能打包進 exe |
| `UXUI_ASSETS` 路徑 | 打包後 `__file__` 指向臨時目錄 | 需在 `.spec` 中設定 `datas`,並使用 `sys._MEIPASS` 處理路徑 |
| OpenCV | OpenCV 需包含 DLL | 通常 PyInstaller 能自動偵測 |
### 8.3 目錄結構(打包後)
```
安裝目錄/
├── Innovedus AI Playground.exe ← 主執行檔
├── uxui/ ← 靜態資源(需隨 exe 一起安裝)
└── ...
%LOCALAPPDATA%\Kneron_Academy\ ← 使用者資料(安裝時建立)
├── utils/
├── uploads/
└── firmware/
```
---
## 9. 已知技術問題 / 技術債
### 9.1 雙重裝置連接(⚠️ 嚴重)
**問題**`CustomInferenceWorkerThread` 在 Worker Thread 內部調用 `kp.core.connect_devices()`,但 `DeviceController` 可能已經對同一個 `usb_port_id` 建立了連接(在 MainWindow 流程中)。
**影響**:可能導致 kp SDK 報告「裝置已被連接」的錯誤,或產生未定義行為。
**建議**`CustomInferenceWorkerThread` 應改為接受外部傳入的 `device_group`,而非自行連接。
### 9.2 UtilitiesScreen 的 DeviceController 孤立問題(⚠️ 中度)
**問題**`UtilitiesScreen` 建立了自己的 `DeviceController(self)` 實例,與 `MainWindow``DeviceController` 完全獨立,兩者各自管理自己的 `device_group`
**影響**:使用者在 UtilitiesScreen 連接裝置後,切換到 MainWindow 並不知道裝置已連接;反之亦然。
**建議**:將 `DeviceController` 提升到 `AppController` 層級,作為共享的單例。
### 9.3 推論 Queue 丟幀而不通知(⚠️ 中度)
**問題**`inference_queue``maxsize=5`,當 queue 滿時,`add_frame_to_queue()` 靜默丟棄幀(只印 print不通知 UI
**影響**:在高推論延遲時,使用者不知道有幀被丟棄,可能誤以為推論仍在即時進行。
**建議**:新增 UI 指示推論 queue 壓力如幀率顯示、lag 指示)。
### 9.4 LoginScreen 的驗證邏輯未實作(⚠️ 中度)
**問題**`LoginScreen.attempt_login()` 的實際 Server 驗證邏輯未實作,目前只要輸入任何非空帳密就會成功登入。
```python
# 目前實作(不安全)
if not username or not password:
self.show_error("Please enter both username and password")
return
self.login_success.emit() # 永遠成功
```
**建議**:需補齊 Server 端驗證 API 呼叫。
### 9.5 debug print 語句散落各處(低優先)
**問題**:各 Controller 和 Thread 中有大量 `print()` 呼叫作為 debug 輸出,打包後仍會執行(輸出被丟棄,但有效能成本)。
**建議**:改用 Python 的 `logging` 模組,並設定適當的 log level。
### 9.6 `custom_inference_worker.py` 中的 `kp` 全域引用問題(⚠️ 中度)
**問題**`_boxes_scale()``post_process_yolo_v5()` 函數的型別標注直接引用 `kp.HwPreProcInfo``kp.InferenceFloatNodeOutput`(如 `def _boxes_scale(boxes, hardware_preproc_info: kp.HwPreProcInfo)`),但 `kp` 在模組頂層未被 import。實際 kp import 是在函數內部的 `run_single_inference()` 中延遲進行的。
**影響**:型別標注在模組載入時會被解析(在 Python 3.10+ 以下),可能導致 `NameError`
**建議**:在頂層加入 `if TYPE_CHECKING: import kp`,或改用字串型別標注 `"kp.HwPreProcInfo"`
### 9.7 VideoThread 的 `threading.Thread` 記憶體洩漏風險(低優先)
**問題**`_open_camera_with_timeout()` 啟動了 `daemon=True``threading.Thread` 並等待最多 5 秒,但如果 thread 仍存活timeout其仍會繼續嘗試開啟相機可能導致相機資源被不正確佔用。
**建議**:使用 cv2 的 nonblocking 方式或設定相機 timeout 參數,避免 daemon thread 的不確定行為。
### 9.8 MSE 計算的效能問題(低優先)
**問題**`InferenceWorkerThread``CustomInferenceWorkerThread` 的 MSE 計算會把整個 frame 轉成 float32 進行運算:
```python
mse = np.mean((frame.astype(np.float32) - self.last_frame.astype(np.float32)) ** 2)
```
對於 640x480 的 3 通道影像,每次計算需要處理 ~921,600 個浮點數。
**建議**:可改為縮小解析度後再計算 MSE或使用 histogram 比較等更快速的方式。
---
## 10. 容量與效能估算
### 10.1 系統需求(桌面應用)
| 資源 | 需求 | 備註 |
|------|------|------|
| CPU | 雙核心以上 | 主要用於影像轉換和後處理 |
| RAM | 2GB 以上 | kp SDK + OpenCV + PyQt5 |
| USB | USB 3.0 | KL720 需要 USB 3.0 |
| GPU | 不需要 | 推論在 NPU 執行 |
| 磁碟 | 500MB 以上 | 安裝包 + 模型檔 |
### 10.2 推論速度特性
- **Queue maxsize**5 幀
- **VideoThread 輸出**~30fps640x480
- **InferenceWorkerThread min_interval**2 秒(標準模式)/ 0.5 秒Custom 模式)
- **MSE threshold**500低於此值視為相似幀使用快取結果
- **相機開啟 timeout**5 秒 × 最多 3 次嘗試
---
## 11. 安全性設計
### 11.1 目前狀態
| 項目 | 狀態 | 說明 |
|------|------|------|
| Server 登入驗證 | ❌ 未實作 | `attempt_login()` 永遠成功 |
| 程式碼保護 | ⚠️ 計畫中 | PyArmor 列在計畫中 |
| 自定義模型驗證 | ❌ 無 | 任何 .nef 檔都能上傳 |
| 網路通訊加密 | ❌ 未知 | Server 驗證端點未見 TLS 設定 |
### 11.2 Plugin 安全風險
`load_inference_module()` 使用 `importlib` 動態執行 `script.py`,等同於執行任意 Python 程式碼。若 `%LOCALAPPDATA%` 中的 `script.py` 被惡意替換,攻擊者可以完整控制推論行為。
**建議**:考慮對 `script.py` 進行簽章驗證,或限制其沙盒執行環境。

39
.autoflow/progress.md Normal file
View File

@ -0,0 +1,39 @@
# 專案進度 — KNEO Academy
## 目的:既有專案接入 → 補齊文件
## 當前階段:文件補齊完成,待使用者確認下一步
## 當前狀態:待審核
## 最後更新2026-04-05
## 進度表
| 階段 | 狀態 | 完成時間 | 備註 |
|------|------|----------|------|
| 既有專案接入 | ✅ 已完成 | 2026-04-04 | 本地路徑 |
| 專案健檢 | ✅ 已完成 | 2026-04-04 | 見 00-onboarding/health-check.md |
| 確認目的與策略 | ✅ 已完成 | 2026-04-04 | 客戶產品,補齊 PRD + 架構文件 + 設計規格 |
| PRDPM Agent 反推) | ✅ 已完成 | 2026-04-04 | 見 02-prd/PRD.md 項目保留待確認 |
| 架構文件 + TDDArchitect Agent | ✅ 已完成 | 2026-04-04 | 見 04-architecture/design-doc.md + TDD.md |
| 設計規格Design Agent | ✅ 已完成 | 2026-04-05 | 見 03-design/design-spec.md |
| 使用者確認各文件 | ✅ 已完成 | 2026-04-05 | PRD + 架構文件已確認;設計規格待確認 |
## 當前待辦
- [ ] 使用者審核設計規格03-design/design-spec.md
- [ ] 決定下一步(新增功能 / 修 bug / 其他)
## 未解決問題
- PRD 中 ⚠️ 待確認項目(保留待日後確認)
- 技術問題Architect Agent 標注):
- 🔴 CustomInferenceWorkerThread 雙重裝置連接風險
- 🔴 LoginScreen 驗證未實作
- 🟡 UtilitiesScreen DeviceController 孤立
- 設計問題Design Agent 標注):
- 兩套設計語言(深色 vs 淺色)共存,視覺不統一
- back_arrow.png 缺失,返回按鈕空白
- 暫停按鈕使用語意錯誤的刪除圖示
## 重要決策紀錄
- 程式碼來源:本地路徑 C:\Users\sungs\Documents\abin\KNEO-Academy
- 專案類型客戶產品Python + PyQt5 桌面 AI 推論應用)
- 設計稿:無 Figma僅 uxui/ 資源
- 測試計畫:暫不需要
- PRD 待確認項目:保留現狀,不強迫確認

View File

@ -67,6 +67,10 @@ class AppController:
self.main_window = MainWindow() self.main_window = MainWindow()
self.stack.addWidget(self.main_window) self.stack.addWidget(self.main_window)
# Share MainWindow's DeviceController with UtilitiesScreen so both
# screens reflect the same device connection state.
self.utilities_screen.set_device_controller(self.main_window.device_controller)
def connect_signals(self): def connect_signals(self):
""" """
Connect signals between screens for navigation. Connect signals between screens for navigation.

View File

@ -11,7 +11,6 @@ import cv2
import json import json
from PyQt5.QtWidgets import QMessageBox, QApplication from PyQt5.QtWidgets import QMessageBox, QApplication
from PyQt5.QtCore import QTimer, Qt from PyQt5.QtCore import QTimer, Qt
import kp
from src.models.inference_worker import InferenceWorkerThread from src.models.inference_worker import InferenceWorkerThread
from src.models.custom_inference_worker import CustomInferenceWorkerThread from src.models.custom_inference_worker import CustomInferenceWorkerThread
@ -187,6 +186,7 @@ class InferenceController:
# Upload model to device # Upload model to device
if device_group: if device_group:
try: try:
import kp
print('[Uploading model]') print('[Uploading model]')
self.model_descriptor = kp.core.load_model_from_file( self.model_descriptor = kp.core.load_model_from_file(
device_group=device_group, device_group=device_group,
@ -364,6 +364,9 @@ class InferenceController:
input_params["custom_ncpu_path"] = tool_config.get("custom_ncpu_path") input_params["custom_ncpu_path"] = tool_config.get("custom_ncpu_path")
input_params["custom_labels"] = tool_config.get("custom_labels") input_params["custom_labels"] = tool_config.get("custom_labels")
# Pass existing device_group to avoid double connection in the worker
input_params["device_group"] = self.device_controller.get_device_group()
# Get device-related settings # Get device-related settings
selected_device = self.device_controller.get_selected_device() selected_device = self.device_controller.get_selected_device()
if selected_device: if selected_device:

View File

@ -66,6 +66,7 @@ class MediaController:
print("Camera signal connected successfully") print("Camera signal connected successfully")
except Exception as e: except Exception as e:
print(f"Error connecting camera signal: {e}") print(f"Error connecting camera signal: {e}")
self.video_thread.camera_error_signal.connect(self.handle_camera_error)
# Start camera thread # Start camera thread
self.video_thread.start() self.video_thread.start()
@ -163,6 +164,20 @@ class MediaController:
import traceback import traceback
print(traceback.format_exc()) print(traceback.format_exc())
def handle_camera_error(self, error_msg):
"""
Handle camera error signal from VideoThread.
Args:
error_msg (str): Error message describing what went wrong.
"""
print(f"Camera error: {error_msg}")
if hasattr(self.main_window, 'canvas_label'):
self.main_window.canvas_label.setText(error_msg)
self.main_window.canvas_label.setAlignment(Qt.AlignCenter)
self.video_thread = None
self._signal_was_connected = False
def reconnect_camera_signal(self): def reconnect_camera_signal(self):
"""Reconnect the camera signal if it was previously disconnected""" """Reconnect the camera signal if it was previously disconnected"""
if self.video_thread is not None and self._signal_was_connected: if self.video_thread is not None and self._signal_was_connected:

View File

@ -4,16 +4,17 @@ custom_inference_worker.py - Custom Inference Worker
This module provides a worker thread for running inference using user-uploaded This module provides a worker thread for running inference using user-uploaded
custom models. It uses YOLO V5 pre/post-processing logic for object detection. custom models. It uses YOLO V5 pre/post-processing logic for object detection.
""" """
from __future__ import annotations
import os import os
import time import time
import queue import queue
import cv2 import cv2
import numpy as np import numpy as np
from typing import List from typing import List, TYPE_CHECKING
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtCore import QThread, pyqtSignal
import kp if TYPE_CHECKING:
from kp.KPBaseClass.ValueBase import ValueRepresentBase import kp
# COCO dataset class names (80 classes) # COCO dataset class names (80 classes)
@ -31,7 +32,7 @@ COCO_CLASSES = [
] ]
class ExampleBoundingBox(ValueRepresentBase): class ExampleBoundingBox:
"""Bounding box descriptor.""" """Bounding box descriptor."""
def __init__(self, def __init__(self,
@ -59,7 +60,7 @@ class ExampleBoundingBox(ValueRepresentBase):
} }
class ExampleYoloResult(ValueRepresentBase): class ExampleYoloResult:
"""YOLO output result descriptor.""" """YOLO output result descriptor."""
def __init__(self, def __init__(self,
@ -354,6 +355,10 @@ class CustomInferenceWorkerThread(QThread):
""" """
Initialize device, upload firmware and model. Initialize device, upload firmware and model.
If a device_group is already provided in input_params (connected by
DeviceController), reuse it and skip connect_devices to avoid double
connection conflicts with the Kneron SDK.
Returns: Returns:
bool: True if initialization successful, False otherwise. bool: True if initialization successful, False otherwise.
""" """
@ -374,12 +379,20 @@ class CustomInferenceWorkerThread(QThread):
print("Missing required file paths") print("Missing required file paths")
return False return False
# Connect to device import kp
# Reuse existing device_group if provided to avoid double connection
existing_device_group = self.input_params.get("device_group")
if existing_device_group is not None:
print('[Reusing existing device connection]')
self.device_group = existing_device_group
else:
print('[Connecting device]') print('[Connecting device]')
self.device_group = kp.core.connect_devices(usb_port_ids=[port_id]) self.device_group = kp.core.connect_devices(usb_port_ids=[port_id])
kp.core.set_timeout(device_group=self.device_group, milliseconds=5000)
print(' - Connection successful') print(' - Connection successful')
kp.core.set_timeout(device_group=self.device_group, milliseconds=5000)
# Upload firmware # Upload firmware
print('[Uploading firmware]') print('[Uploading firmware]')
kp.core.load_firmware_from_file(self.device_group, scpu_path, ncpu_path) kp.core.load_firmware_from_file(self.device_group, scpu_path, ncpu_path)
@ -421,6 +434,7 @@ class CustomInferenceWorkerThread(QThread):
img_processed, original_width, original_height = preprocess_frame(frame) img_processed, original_width, original_height = preprocess_frame(frame)
# 建立推論描述符 # 建立推論描述符
import kp
descriptor = kp.GenericImageInferenceDescriptor( descriptor = kp.GenericImageInferenceDescriptor(
model_id=self.model_descriptor.models[0].id, model_id=self.model_descriptor.models[0].id,
inference_number=0, inference_number=0,
@ -532,11 +546,20 @@ class CustomInferenceWorkerThread(QThread):
self.quit() self.quit()
def cleanup(self): def cleanup(self):
"""Clean up resources and disconnect device.""" """Clean up resources.
Only disconnects device if this worker created the connection itself
(i.e. no device_group was provided via input_params).
"""
try: try:
if self.device_group is not None: if self.device_group is not None:
owned_by_worker = self.input_params.get("device_group") is None
if owned_by_worker:
import kp
kp.core.disconnect_devices(self.device_group) kp.core.disconnect_devices(self.device_group)
print('[Device disconnected]') print('[Device disconnected]')
else:
print('[Device connection owned by DeviceController, skipping disconnect]')
self.device_group = None self.device_group = None
except Exception as e: except Exception as e:
print(f"Error cleaning up resources: {e}") print(f"Error cleaning up resources: {e}")

View File

@ -125,9 +125,10 @@ class VideoThread(QThread):
height, width, channel = frame.shape height, width, channel = frame.shape
bytes_per_line = channel * width bytes_per_line = channel * width
qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888) qt_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888)
self.change_pixmap_signal.emit(qt_image) self.change_pixmap_signal.emit(qt_image.copy())
else: else:
print("Unable to read camera frame, camera may be disconnected") print("Unable to read camera frame, camera may be disconnected")
self.camera_error_signal.emit("相機連線中斷,嘗試重新連線...")
break break
# Release camera resources # Release camera resources
@ -141,6 +142,7 @@ class VideoThread(QThread):
if self._camera_open_attempts >= self._max_attempts: if self._camera_open_attempts >= self._max_attempts:
print("Maximum attempts reached, unable to open camera") print("Maximum attempts reached, unable to open camera")
self.camera_error_signal.emit("無法開啟相機,請確認相機是否連接")
def stop(self): def stop(self):
"""Stop the video capture thread.""" """Stop the video capture thread."""

View File

@ -25,7 +25,7 @@ def create_media_panel(parent, media_controller, file_service):
media_controller.take_screenshot), media_controller.take_screenshot),
('upload file', os.path.join(assets_path, "Assets_svg/bt_function_upload_normal.svg").replace('\\', '/'), ('upload file', os.path.join(assets_path, "Assets_svg/bt_function_upload_normal.svg").replace('\\', '/'),
file_service.upload_file), file_service.upload_file),
('pause/resume', os.path.join(assets_path, "Assets_svg/btn_result_image_delete_hover.svg").replace('\\', '/'), ('pause/resume', os.path.join(assets_path, "Assets_svg/bt_function_video_normal.svg").replace('\\', '/'),
lambda: toggle_pause_button(parent, media_controller)), lambda: toggle_pause_button(parent, media_controller)),
('voice', os.path.join(assets_path, "Assets_svg/ic_recording_voice.svg").replace('\\', '/'), ('voice', os.path.join(assets_path, "Assets_svg/ic_recording_voice.svg").replace('\\', '/'),
lambda: media_controller.record_audio(None)), lambda: media_controller.record_audio(None)),

View File

@ -14,7 +14,6 @@ from PyQt5.QtWidgets import (
from PyQt5.QtCore import Qt, pyqtSignal, QTimer from PyQt5.QtCore import Qt, pyqtSignal, QTimer
from PyQt5.QtGui import QPixmap, QFont, QIcon, QColor from PyQt5.QtGui import QPixmap, QFont, QIcon, QColor
import os import os
import kp
from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, DongleModelMap from src.config import UXUI_ASSETS, WINDOW_SIZE, BACKGROUND_COLOR, DongleModelMap
from src.controllers.device_controller import DeviceController from src.controllers.device_controller import DeviceController
from src.services.device_service import check_available_device from src.services.device_service import check_available_device
@ -56,6 +55,18 @@ class UtilitiesScreen(QWidget):
self.current_page = "utilities" # Track current page: "utilities" or "purchased_items" self.current_page = "utilities" # Track current page: "utilities" or "purchased_items"
self.init_ui() self.init_ui()
def set_device_controller(self, device_controller):
"""
Replace the local DeviceController with a shared instance.
Call this from AppController after all screens are created so that
UtilitiesScreen and MainWindow share the same device connection state.
Args:
device_controller: Shared DeviceController instance from MainWindow.
"""
self.device_controller = device_controller
def init_ui(self): def init_ui(self):
""" """
Initialize the user interface. Initialize the user interface.
@ -135,14 +146,14 @@ class UtilitiesScreen(QWidget):
header_layout.setContentsMargins(20, 0, 20, 0) header_layout.setContentsMargins(20, 0, 20, 0)
# Back button # Back button
back_button = QPushButton("", self) back_button = QPushButton("", self)
back_button.setIcon(QIcon(os.path.join(UXUI_ASSETS, "Assets_png/back_arrow.png")))
back_button.setIconSize(QPixmap(os.path.join(UXUI_ASSETS, "Assets_png/back_arrow.png")).size())
back_button.setFixedSize(40, 40) back_button.setFixedSize(40, 40)
back_button.setStyleSheet(""" back_button.setStyleSheet("""
QPushButton { QPushButton {
background-color: transparent; background-color: transparent;
border: none; border: none;
color: white;
font-size: 20px;
} }
QPushButton:hover { QPushButton:hover {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
@ -851,6 +862,7 @@ class UtilitiesScreen(QWidget):
firmware_version = "-" firmware_version = "-"
try: try:
if device.is_connectable: if device.is_connectable:
import kp
# Connect to device and get system info # Connect to device and get system info
device_group = kp.core.connect_devices(usb_port_ids=[port_id]) device_group = kp.core.connect_devices(usb_port_ids=[port_id])
system_info = kp.core.get_system_info( system_info = kp.core.get_system_info(
@ -980,6 +992,7 @@ class UtilitiesScreen(QWidget):
self.show_progress(f"Updating firmware for {device_model}...", 0) self.show_progress(f"Updating firmware for {device_model}...", 0)
# Connect to device # Connect to device
import kp
device_group = kp.core.connect_devices(usb_port_ids=[int(port_id)]) device_group = kp.core.connect_devices(usb_port_ids=[int(port_id)])
# Build firmware file paths # Build firmware file paths
@ -1026,6 +1039,7 @@ class UtilitiesScreen(QWidget):
self.show_progress("Installing Kneron Device Drivers...", 0) self.show_progress("Installing Kneron Device Drivers...", 0)
# List all product IDs # List all product IDs
import kp
product_ids = [ product_ids = [
kp.ProductId.KP_DEVICE_KL520, kp.ProductId.KP_DEVICE_KL520,
kp.ProductId.KP_DEVICE_KL720_LEGACY, kp.ProductId.KP_DEVICE_KL720_LEGACY,