From 1e42293896701d637845880713a7bb88dc51c135 Mon Sep 17 00:00:00 2001 From: abin Date: Tue, 7 Apr 2026 14:37:04 +0800 Subject: [PATCH] Add autoflow --- .autoflow/00-onboarding/health-check.md | 173 +++++ .autoflow/02-prd/PRD.md | 625 +++++++++++++++ .autoflow/03-design/design-spec.md | 729 ++++++++++++++++++ .autoflow/04-architecture/TDD.md | 970 ++++++++++++++++++++++++ .autoflow/04-architecture/design-doc.md | 569 ++++++++++++++ .autoflow/progress.md | 39 + 6 files changed, 3105 insertions(+) create mode 100644 .autoflow/00-onboarding/health-check.md create mode 100644 .autoflow/02-prd/PRD.md create mode 100644 .autoflow/03-design/design-spec.md create mode 100644 .autoflow/04-architecture/TDD.md create mode 100644 .autoflow/04-architecture/design-doc.md create mode 100644 .autoflow/progress.md diff --git a/.autoflow/00-onboarding/health-check.md b/.autoflow/00-onboarding/health-check.md new file mode 100644 index 0000000..f0824aa --- /dev/null +++ b/.autoflow/00-onboarding/health-check.md @@ -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) diff --git a/.autoflow/02-prd/PRD.md b/.autoflow/02-prd/PRD.md new file mode 100644 index 0000000..1173e8f --- /dev/null +++ b/.autoflow/02-prd/PRD.md @@ -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": , # 已連接的裝置群組物件 + "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": , # 已上傳的模型描述符 + # 以下為 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)* +*狀態:待產品負責人確認標注「⚠️ 待確認」的項目* diff --git a/.autoflow/03-design/design-spec.md b/.autoflow/03-design/design-spec.md new file mode 100644 index 0000000..6fcd63f --- /dev/null +++ b/.autoflow/03-design/design-spec.md @@ -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* diff --git a/.autoflow/04-architecture/TDD.md b/.autoflow/04-architecture/TDD.md new file mode 100644 index 0000000..6bcc0ba --- /dev/null +++ b/.autoflow/04-architecture/TDD.md @@ -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 | diff --git a/.autoflow/04-architecture/design-doc.md b/.autoflow/04-architecture/design-doc.md new file mode 100644 index 0000000..c7399ab --- /dev/null +++ b/.autoflow/04-architecture/design-doc.md @@ -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` 進行簽章驗證,或限制其沙盒執行環境。 diff --git a/.autoflow/progress.md b/.autoflow/progress.md new file mode 100644 index 0000000..3fb932a --- /dev/null +++ b/.autoflow/progress.md @@ -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 待確認項目:保留現狀,不強迫確認