Add autoflow
This commit is contained in:
parent
7e323cf3e1
commit
1e42293896
173
.autoflow/00-onboarding/health-check.md
Normal file
173
.autoflow/00-onboarding/health-check.md
Normal file
@ -0,0 +1,173 @@
|
||||
# 專案健檢報告 — KNEO Academy
|
||||
|
||||
## 基本資訊
|
||||
- **專案名稱**:KNEO Academy(Innovedus 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 point,AppController(QStackedWidget 管理頁面)
|
||||
├── 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 # 自定義模型推論 worker(YOLOv5 後處理)
|
||||
│ │ └── 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(裝置管理工具,需登入)
|
||||
└── → MainWindow(Demo AI App,直接進入)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 主要功能清單
|
||||
|
||||
| 功能 | 描述 | 狀態 |
|
||||
|------|------|------|
|
||||
| 頁面路由 | QStackedWidget 管理多頁面切換 | ✅ 完成 |
|
||||
| 裝置掃描 | 掃描連接的 Kneron dongle(KL520 / 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 / 需求文件(產品功能邊界不清晰)
|
||||
- 測試(完全沒有自動化測試)
|
||||
|
||||
**中優先:**
|
||||
- 架構文件 / TDD(plugin script.py 介面規格未文件化)
|
||||
- 完整流程圖(主 App 推論流程尚未有流程圖)
|
||||
|
||||
**低優先:**
|
||||
- 部署完整指南
|
||||
- 設計規格文件(Wireframe + Design Tokens)
|
||||
625
.autoflow/02-prd/PRD.md
Normal file
625
.autoflow/02-prd/PRD.md
Normal file
@ -0,0 +1,625 @@
|
||||
# KNEO Academy(Innovedus AI Playground)v2.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 ID(Hex) | 支援狀態 |
|
||||
|---------|-----------------|---------|
|
||||
| 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 按鈕] ──────────────────→ MainWindow(AI 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 Number(Dongle 唯一識別號)
|
||||
- 授權卡(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 MainWindow(AI 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 等視覺化結果
|
||||
|
||||
**效能優化機制**:
|
||||
- MSE(Mean 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 backend(Windows 優先)
|
||||
- 解析度: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 推論 Worker(YOLOv5 後處理) |
|
||||
| `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 Agent(Autoflow)*
|
||||
*狀態:待產品負責人確認標注「⚠️ 待確認」的項目*
|
||||
729
.autoflow/03-design/design-spec.md
Normal file
729
.autoflow/03-design/design-spec.md
Normal file
@ -0,0 +1,729 @@
|
||||
# KNEO Academy(Innovedus 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` | 亮藍(SecondaryColor,Popup / 元件背景) |
|
||||
| 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 | 頁面主要 Margin(Popup、MainWindow canvas) |
|
||||
| spacing.xxl | 30px | 表單容器 Padding(Login、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 Label(Popup 中)、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
|
||||
Padding:5px 10px
|
||||
|
||||
Hover:background-color: rgba(255, 255, 255, 50)(約 20% 白色透明)
|
||||
Pressed:background-color: rgba(255, 255, 255, 100)(約 39% 白色透明)
|
||||
```
|
||||
|
||||
#### 主要 CTA 按鈕(藍色,淺色背景場景)
|
||||
|
||||
```
|
||||
背景:#3498DB
|
||||
文字:白色
|
||||
邊框:none(或 2px solid #2980B9)
|
||||
圓角:5px
|
||||
MinHeight:40-45px
|
||||
Padding:10px 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
|
||||
|
||||
Hover:rgba(76, 175, 80, 0.5)
|
||||
Disabled:rgba(128, 128, 128, 0.2),文字 #666,邊框 #666
|
||||
```
|
||||
|
||||
#### Custom Model — Stop 按鈕(紅色透明)
|
||||
|
||||
```
|
||||
背景:透明
|
||||
文字:#ff6b6b
|
||||
邊框:1px solid #ff6b6b
|
||||
圓角:8px
|
||||
高度:32px
|
||||
|
||||
Hover:rgba(255, 107, 107, 0.2)
|
||||
```
|
||||
|
||||
#### Media Panel 圖示按鈕
|
||||
|
||||
```
|
||||
背景:透明
|
||||
邊框:1px transparent
|
||||
圓角:10px
|
||||
尺寸:50×50px
|
||||
|
||||
Hover:rgba(255, 255, 255, 50)
|
||||
Pressed:rgba(255, 255, 255, 100)
|
||||
```
|
||||
|
||||
#### Header Back 按鈕(圖示透明按鈕)
|
||||
|
||||
```
|
||||
背景:透明
|
||||
邊框:none
|
||||
尺寸:40×40px
|
||||
|
||||
Hover:rgba(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`(淺灰白)
|
||||
**佈局**:QVBoxLayout,Margin 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`(淺灰)
|
||||
**佈局**:QVBoxLayout,Margin 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: Password;Placeholder:"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`(淺灰)
|
||||
**佈局**:QVBoxLayout,Margin 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 / Status;Header `background: #3498DB; color: white; font-weight: bold; padding: 8px`;Item padding 8px;Selected `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 欄:Select(Checkbox)/ 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 MainWindow(AI Demo 推論主視窗)
|
||||
|
||||
**背景色**:`#143058`(深海軍藍)
|
||||
**佈局**:QVBoxLayout 外層,啟動時先顯示歡迎畫面(Logo),500ms 後切換至主頁面
|
||||
|
||||
#### 3.4.1 歡迎畫面(500ms 轉場)
|
||||
|
||||
| 元件 | 類型 | 規格 | 說明 |
|
||||
|------|------|------|------|
|
||||
| 歡迎 Logo | QLabel + QPixmap | 原始尺寸 | `kneron_logo.png`,水平居中 |
|
||||
|
||||
#### 3.4.2 主頁面佈局(QHBoxLayout)
|
||||
|
||||
左右兩欄佈局:
|
||||
- **左欄**:固定寬度 260px,QVBoxLayout,Margin 10px,Spacing 10px
|
||||
- **右欄**:彈性寬度(stretch factor 2),QGridLayout,Margin 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 | — | 透明背景;Item:padding 5px,Selected:`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*
|
||||
970
.autoflow/04-architecture/TDD.md
Normal file
970
.autoflow/04-architecture/TDD.md
Normal 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 [AppController(main.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 對應)
|
||||
```
|
||||
|
||||
#### DongleModelMap(product_id → model name)
|
||||
|
||||
```python
|
||||
DongleModelMap = {
|
||||
"0x100": "KL520",
|
||||
"0x720": "KL720",
|
||||
}
|
||||
# 注意:key 為 lowercase hex string(含 0x 前綴),如 "0x100"
|
||||
```
|
||||
|
||||
#### DongleIconMap(product_id → icon 檔名)
|
||||
|
||||
```python
|
||||
DongleIconMap = {
|
||||
"0x100": "ic_dongle_520.png",
|
||||
"0x720": "ic_dongle_720.png",
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 AppController(main.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 | 掃描裝置並更新 UI;True = 找到裝置 |
|
||||
| `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_queue(maxsize=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: InferenceWorkerThread(QThread)
|
||||
|
||||
```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 = False,break
|
||||
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/縮放補償
|
||||
- 分數閾值過濾
|
||||
- NMS(per-class,IoU threshold=0.5)
|
||||
回傳 ExampleYoloResult
|
||||
"""
|
||||
```
|
||||
|
||||
#### Class: CustomInferenceWorkerThread(QThread)
|
||||
|
||||
```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: VideoThread(QThread)
|
||||
|
||||
```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 次)
|
||||
|
||||
幀格式轉換:
|
||||
```
|
||||
BGR(OpenCV 預設) → RGB(cv2.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/無裝置:EmptyDescriptor(device_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=uint8,RGB 格式
|
||||
|
||||
注意:使用 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` |
|
||||
| 色彩空間 | RGB(VideoThread 輸出)|
|
||||
| 典型尺寸 | 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") # 只有 print,UI 不更新
|
||||
```
|
||||
|
||||
**⚠️ 問題**:這是 `put_nowait` 語意,但實際上是 `if not full` 檢查後再 `put`,在多執行緒環境中存在微小的 race window(check-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 頁面導航 Signal(AppController 層)
|
||||
|
||||
| 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 媒體與推論 Signal(MainWindow 層)
|
||||
|
||||
| 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.json(SCRIPT_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 |
|
||||
569
.autoflow/04-architecture/design-doc.md
Normal file
569
.autoflow/04-architecture/design-doc.md
Normal file
@ -0,0 +1,569 @@
|
||||
# Design Doc: KNEO Academy(Innovedus AI Playground)v2.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 設計模式
|
||||
|
||||
本應用程式採用 **MVC(Model-View-Controller)** 架構,搭配 Qt 的 **Signal/Slot** 機制實現跨執行緒通訊。
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Views(呈現層) │
|
||||
│ SelectionScreen LoginScreen MainWindow UtilitiesScreen │
|
||||
│ QWidget QWidget QWidget QWidget │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ Signal/Slot
|
||||
┌────────────────────────────▼────────────────────────────────────┐
|
||||
│ AppController(main.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
|
||||
└── QStackedWidget(stack)
|
||||
├── [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 組成協作關係:
|
||||
|
||||
```
|
||||
MainWindow(QWidget)
|
||||
├── DeviceController — 管理 kp SDK 裝置連接
|
||||
├── InferenceController — 管理推論 Worker Thread 與 Queue
|
||||
│ └── inference_queue(queue.Queue, maxsize=5)
|
||||
└── MediaController — 管理相機擷取與畫面更新
|
||||
└── VideoThread(QThread)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 模組依賴圖
|
||||
|
||||
```
|
||||
main.py(AppController)
|
||||
├── views/selection_screen.py(SelectionScreen)
|
||||
│ └── config.py
|
||||
├── views/login_screen.py(LoginScreen)
|
||||
│ └── config.py
|
||||
├── views/utilities_screen.py(UtilitiesScreen)
|
||||
│ ├── config.py
|
||||
│ ├── controllers/device_controller.py(DeviceController)
|
||||
│ └── services/device_service.py(check_available_device)
|
||||
└── views/mainWindows.py(MainWindow)
|
||||
├── config.py
|
||||
├── controllers/device_controller.py(DeviceController)
|
||||
│ ├── config.py
|
||||
│ └── services/device_service.py
|
||||
├── controllers/inference_controller.py(InferenceController)
|
||||
│ ├── config.py
|
||||
│ ├── models/inference_worker.py(InferenceWorkerThread)
|
||||
│ │ ├── config.py
|
||||
│ │ └── [動態載入] utils/{mode}/{model}/script.py
|
||||
│ └── models/custom_inference_worker.py(CustomInferenceWorkerThread)
|
||||
├── controllers/media_controller.py(MediaController)
|
||||
│ ├── models/video_thread.py(VideoThread)
|
||||
│ └── utils/image_utils.py
|
||||
├── services/file_service.py(FileService)
|
||||
└── utils/config_utils.py(ConfigUtils)
|
||||
```
|
||||
|
||||
**注意**:`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. 暫停相機 Signal(disconnect 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)
|
||||
│
|
||||
▼
|
||||
InferenceWorkerThread(once_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_path(fw_scpu.bin)
|
||||
- custom_ncpu_path(fw_ncpu.bin)
|
||||
- custom_labels(可選)
|
||||
│
|
||||
▼
|
||||
InferenceController.select_custom_tool(tool_config)
|
||||
│
|
||||
▼
|
||||
CustomInferenceWorkerThread(QThread)
|
||||
│
|
||||
├── 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 Thread(UI Thread)
|
||||
├── AppController(QStackedWidget 管理)
|
||||
├── MainWindow(UI 事件處理)
|
||||
│ ├── handle_inference_result() ← 由 Signal 呼叫(執行在主執行緒)
|
||||
│ └── update_image() via MediaController ← 由 Signal 呼叫(執行在主執行緒)
|
||||
│
|
||||
├── VideoThread(QThread #1)
|
||||
│ └── 職責:相機擷取、QImage 轉換、emit change_pixmap_signal
|
||||
│ └── 內部:threading.Thread(用於相機開啟 timeout 機制)
|
||||
│
|
||||
├── InferenceWorkerThread(QThread #2)
|
||||
│ └── 職責:從 queue 取幀、MSE 比較、呼叫 script.inference()、emit 結果
|
||||
│
|
||||
└── CustomInferenceWorkerThread(QThread #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.Thread(daemon=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.json(Plugin 索引)
|
||||
|
||||
使用者選擇工具
|
||||
│
|
||||
▼
|
||||
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 或 None(None 表示跳過此幀)
|
||||
|
||||
支援的回傳格式(在 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 輸出**:~30fps(640x480)
|
||||
- **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
39
.autoflow/progress.md
Normal 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 + 架構文件 + 設計規格 |
|
||||
| PRD(PM Agent 反推) | ✅ 已完成 | 2026-04-04 | 見 02-prd/PRD.md,⚠️ 項目保留待確認 |
|
||||
| 架構文件 + TDD(Architect 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 待確認項目:保留現狀,不強迫確認
|
||||
Loading…
x
Reference in New Issue
Block a user