visionA/local-tool/.autoflow/04-architecture/dependency-bundling.md
jim800121chen c54f16fca0 Initial commit: visionA monorepo with local-tool subproject
local-tool/: visionA-local desktop app
- M1: Wails shell + Go server + Next.js UI + Mock mode (macOS dmg ready)
- M2: i18n (zh-TW/en) + Settings 4-tab refactor
- M3: Embedded Python 3.12 runtime (python-build-standalone) + KneronPLUS wheels
- M4: Windows Inno Setup script (build on Windows runner)
- M5: Linux AppImage script + udev rule (build on Linux runner)
- M6: ffmpeg (GPL, pending legal review) + yt-dlp bundled
- Lifecycle: watchServer health check, fatal native dialog,
            Wails IPC raise endpoint, stale process cleanup

.autoflow/: full PRD / Design Spec / Architecture / Testing docs
            (4 rounds tri-party discussion + cross review)
.github/workflows/: macOS / Windows / Linux build CI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:10:38 +08:00

393 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Dependency Bundling — visionA-local
> 所有外部依賴的內嵌策略Python runtime、KneronPLUS、ffmpeg、yt-dlp、預置模型。
> 核心目標:**完全離線、零外部依賴、一鍵安裝。**
---
## 1. Python Runtime雙策略
### 1.1 總策略
**主策略 A預設內嵌 [python-build-standalone](https://github.com/astral-sh/python-build-standalone)。**
**備策略 Bfallback偵測系統 `python3`。**
這是使用者明確決策Q1 = A 且保留 B正常使用者永遠走 A但保留 B 作為:
- 開發模式(`--dev` flag不用解壓內嵌 runtime加快迭代
- Runtime 壞掉時的救援(例如內嵌 runtime 因為 anti-virus 誤殺被刪)
- 某些特殊環境下策略 A 不可用時的退路
### 1.2 python-build-standalone 版本選擇
| 項目 | 選擇 |
|------|------|
| Python 版本 | **3.12.x**最新穩定、KneronPLUS wheel 在 3.12 上有 ABI 支援) |
| 變體flavor | `install_only` 版本(不含 build tools檔案較小 |
| 平台 tarball | `cpython-3.12.x-{arch}-{os}-install_only.tar.gz` |
**各平台下載 URL 模式:**
- macOS x64`cpython-3.12.x-x86_64-apple-darwin-install_only.tar.gz`~30MB 壓縮 / ~90MB 解壓)
- Windows x64`cpython-3.12.x-x86_64-pc-windows-msvc-install_only.tar.gz`~30MB / ~90MB
- Linux x64`cpython-3.12.x-x86_64-unknown-linux-gnu-install_only.tar.gz`~30MB / ~100MB
下載後存放到 `visiona-local/payload/python/<os>/` 目錄build 時透過 `go:embed` 塞進 Wails binary。
### 1.3 策略 A內嵌解壓流程
```go
// visiona-local/python_runtime.go (新寫)
// 首次啟動時執行
func (inst *Installer) stepSetupPythonStandalone(config InstallConfig) error {
// 1. 解壓內嵌的 python tarball → ~/Library/Application Support/visiona-local/python/
pythonDir := filepath.Join(config.InstallDir, "python")
if err := inst.extractTarGz("payload/python/darwin/python-3.12.tar.gz", pythonDir); err != nil {
// 解壓失敗 → 嘗試策略 B
inst.logger.Warn("Bundled Python extract failed: %v, falling back to system python3", err)
return inst.stepSetupPythonSystem(config)
}
// 2. 驗證 python3 可執行
pythonBin := filepath.Join(pythonDir, "bin", "python3") // Linux/mac
if runtime.GOOS == "windows" {
pythonBin = filepath.Join(pythonDir, "python.exe")
}
if _, err := exec.Command(pythonBin, "--version").Output(); err != nil {
inst.logger.Warn("Bundled Python unusable: %v, falling back", err)
return inst.stepSetupPythonSystem(config)
}
// 3. 建立 venv使用內嵌 python
venvDir := filepath.Join(config.InstallDir, "venv")
if err := exec.Command(pythonBin, "-m", "venv", venvDir).Run(); err != nil {
return fmt.Errorf("create venv failed: %w", err)
}
// 4. 離線安裝 wheels
return inst.installWheelsOffline(venvDir, config)
}
```
### 1.4 策略 B系統 Python fallback
```go
func (inst *Installer) stepSetupPythonSystem(config InstallConfig) error {
// 沿用 edge-ai-platform 現有 findPython3() 邏輯
pythonBin, err := findPython3()
if err != nil {
return fmt.Errorf("no usable python3 found: %w (請安裝 Python 3.12+ 或重新安裝 visionA-local)", err)
}
// 建 venv + 離線安裝 wheels
venvDir := filepath.Join(config.InstallDir, "venv")
if err := exec.Command(pythonBin, "-m", "venv", venvDir).Run(); err != nil {
return err
}
return inst.installWheelsOffline(venvDir, config)
}
```
### 1.5 切換機制
**決策樹(首次安裝時執行一次):**
```
start
啟動旗標 --python-mode=auto|bundled|system (default: auto)
是 auto
├─ Yes → 嘗試 stepSetupPythonStandalone
│ 成功 → 記錄 python.mode=bundled 到 .installed
│ 失敗 → 嘗試 stepSetupPythonSystem
│ 成功 → 記錄 python.mode=system 到 .installed
│ 失敗 → 顯示錯誤,安裝中止
├─ bundled → 只嘗試 stepSetupPythonStandalone
└─ system → 只嘗試 stepSetupPythonSystem
```
**執行階段(每次啟動 server 時):**
Go server 啟動時透過 `--python-path` flag 告訴 `kneron_bridge.py` 該用哪個 python interpreter。Wails app 從 `.installed` 讀出 `python.mode`,對應到 `venv/bin/python3` 路徑傳給 server
```
Wails app → reads .installed → python.mode=bundled
→ spawns: visiona-local-server --python=/Users/.../venv/bin/python3
--scripts=/Users/.../scripts
```
### 1.6 需要的依賴清單
`scripts/requirements.txt`(沿用 edge-ai-platform 現有內容):
```
numpy>=1.26,<2.0
opencv-python-headless>=4.9
pyusb>=1.2.1
# KneronPLUS wheel 單獨透過 --find-links 安裝,不列在 requirements.txt
```
**Mock 模式提示**Mock 模式下 Go server **完全不 spawn** Python sidecar`requirements.txt` 裡的 wheel 也不會被使用,但仍必須在首次安裝時完整裝進 venv否則切到 Real 模式會失敗。Mock 模式省的是 runtime 記憶體,不是安裝時間。
**離線安裝指令:**
```bash
pip install --no-index --find-links /path/to/wheels/ \
-r requirements.txt \
KneronPLUS
```
### 1.7 Wheels 內嵌結構
```
visiona-local/payload/scripts/wheels/
├── common/ ← 跨平台 pure-python
│ └── (無opencv/numpy 都是 platform-specific)
├── macos-x64/
│ ├── numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl
│ ├── opencv_python_headless-4.9.0-cp312-cp312-macosx_10_13_x86_64.whl
│ ├── pyusb-1.2.1-py3-none-any.whl
│ └── KneronPLUS-2.0.0-cp312-cp312-macosx_11_0_x86_64.whl
├── windows-x64/
│ ├── numpy-1.26.4-cp312-cp312-win_amd64.whl
│ ├── opencv_python_headless-4.9.0-cp312-cp312-win_amd64.whl
│ ├── pyusb-1.2.1-py3-none-any.whl
│ └── KneronPLUS-3.1.2-cp312-cp312-win_amd64.whl
└── linux-x64/
├── numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
├── opencv_python_headless-4.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
├── pyusb-1.2.1-py3-none-any.whl
└── KneronPLUS-3.1.2-cp312-cp312-manylinux2014_x86_64.whl
```
build 時只把**對應目標平台**的 wheel 塞進該平台的 payload避免浪費空間。
---
## 2. KneronPLUS SDK
### 2.1 來源
原專案 `installer/wheels/` 已有三平台 wheel共 ~3.9MB
| 平台 | 檔案 | 原生函式庫 | 備註 |
|------|------|-----------|------|
| macOS | `KneronPLUS-2.0.0-cp39-abi3-macosx_11_0_x86_64.whl` | `.dylib` | **只有 x86_64**Apple Silicon 走 Rosetta |
| Linux | `KneronPLUS-3.1.2-cp39-abi3-manylinux_2_17_x86_64.whl` | `.so` | 需要 libusb-1.0 |
| Windows | `KneronPLUS-3.1.2-cp39-abi3-win_amd64.whl` | `.dll` | 需要 WinUSB driver |
**abi3 是好消息**wheel 對 CPython 版本有 forward compatibility不用每升 Python 就換 wheel。但我們仍建議 pin Python 3.12。
### 2.2 各平台特殊處理
#### macOS
- wheel 內的 `.dylib` 需要 **ad-hoc codesign**`codesign --force --deep --sign - <path>`),否則 Gatekeeper 擋
- 這步在 `installer/app.go` 已有實作,沿用
- 首次啟動時可能跳 Gatekeeper 警告(因為沒有正式簽章),需要使用者**右鍵 → 開啟**(文件寫清楚)
#### Linux
- 需要 `libusb-1.0.so.0`。Ubuntu 22.04/24.04 **預設不安裝**,但是:
- 方法 A`apt-get install libusb-1.0-0`(需 sudo
- 方法 B推薦**AppImage 內帶 libusb**,透過 `LD_LIBRARY_PATH` 指向 AppImage 內部
- 需要 udev rule 讓非 root 使用者存取 USB
```
# /etc/udev/rules.d/99-kneron.rules
SUBSYSTEM=="usb", ATTRS{idVendor}=="3231", MODE="0666"
```
首次啟動時透過 `pkexec cp` 寫入(會跳 GUI 提權對話框),使用者拒絕就提示「需要 sudo 手動安裝,或用 sudo 跑一次 visionA-local」
#### Windows
- 需要安裝 **WinUSB driver**(把 Kneron USB 裝置綁定到 WinUSB driver interface
- 原專案 `installer/drivers/kneron_winusb.inf` + `amd64/WdfCoInstaller01011.dll` + `winusbcoinstaller2.dll` 已有
- 透過 `pnputil /add-driver <inf> /install` 安裝,會彈 UAC
- 另外需要把 `libusb-1.0.dll` 放到 venv 的 `Scripts/` 或 `DLLs/` 目錄(沿用原專案做法)
### 2.3 安裝階段
在 `stepInstallKneronPlus` 步驟:
1. pip 離線安裝 wheel解壓 .dylib/.so/.dll 到 venv/site-packages
2. macOS對 `.dylib` 做 ad-hoc sign
3. Linux寫 udev rule
4. Windows呼叫 `pnputil` 安裝 WinUSB driver + 複製 libusb-1.0.dll
---
## 3. ffmpeg
### 3.1 策略
**內嵌 LGPL static build不依賴系統 ffmpeg / brew / winget。**
**為何 LGPL 不 GPL**GPL build 會感染整個產品授權。LGPL 允許商業使用,只要提供 LGPL 部分的 source 或 link。
### 3.2 各平台來源
| 平台 | 來源 | 檔名 | 大小 |
|------|------|------|------|
| macOS x64 | [evermeet.cx](https://evermeet.cx/ffmpeg/) LGPL static build | `ffmpeg` | ~35MB |
| Windows x64 | [BtbN/FFmpeg-Builds](https://github.com/BtbN/FFmpeg-Builds) `ffmpeg-n6.1-latest-win64-lgpl-shared-6.1.zip` 的 `ffmpeg.exe`(需連同幾個 DLL | `ffmpeg.exe` + DLLs | ~45MB |
| Linux x64 | [johnvansickle.com](https://johnvansickle.com/ffmpeg/) LGPL static build | `ffmpeg` | ~40MB |
### 3.3 內嵌結構
```
visiona-local/payload/bin/
├── ffmpeg ← macOS / Linux單檔 static
├── ffmpeg.exe ← Windows
└── (Windows 需要的 DLLs, e.g. avcodec-61.dll, avformat-61.dll)
```
### 3.4 Go server 使用方式
現有 `server/internal/camera/` 透過 `exec.Command("ffmpeg", ...)` 呼叫。新版改為:
```go
// server/internal/camera/ffmpeg.go
func ffmpegBinary() string {
// 先查環境變數dev 模式可 override
if p := os.Getenv("VISIONA_FFMPEG"); p != "" {
return p
}
// 生產模式:從 installDir/bin 找
exe, _ := os.Executable()
dir := filepath.Dir(exe) // 這個 exe 是 visiona-local-server
bundled := filepath.Join(dir, "ffmpeg")
if runtime.GOOS == "windows" {
bundled += ".exe"
}
if _, err := os.Stat(bundled); err == nil {
return bundled
}
// fallbackPATH
return "ffmpeg"
}
```
### 3.5 授權聲明
需要在 Settings → About 頁顯示:
```
此產品包含以 LGPLv2.1+ 授權的 FFmpeg。
- FFmpeg source: https://ffmpeg.org
- LGPL license: https://www.gnu.org/licenses/lgpl-2.1.html
- Build provided by BtbN/FFmpeg-Builds (Windows/Linux) / evermeet.cx (macOS)
```
---
## 4. yt-dlp使用者決策 Q10 = A保留
### 4.1 為何保留
`/api/media/url` endpoint 讓使用者可以丟 YouTube 或其他影片網址進來做離線推論,這對 demo 場景有價值(例如「拿 YouTube 影片跑物件偵測」),使用者明確要求保留。
### 4.2 內嵌方式
yt-dlp 有 **standalone executable**(自帶 Python runtime不需要系統 Python這是為離線部署設計的
| 平台 | 檔案 | 來源 |
|------|------|------|
| macOS | `yt-dlp_macos` | [yt-dlp release](https://github.com/yt-dlp/yt-dlp/releases) |
| Linux | `yt-dlp` | 同上 |
| Windows | `yt-dlp.exe` | 同上 |
大小約 8-15MB per platform。放到 `visiona-local/payload/bin/yt-dlp{.exe}` 內嵌。
**注意:** 使用 standalone 版本而不是 `pip install yt-dlp`,因為我們想與主 venv 解耦,避免 yt-dlp 的依賴與 KneronPLUS 衝突。
### 4.3 Go server 使用方式
沿用 `exec.Command` 模式path 查找與 ffmpeg 相同:先查 `VISIONA_YTDLP` 環境變數,再查同目錄 `bin/`。
### 4.4 授權與風險
- yt-dlp 是 Unlicensepublic domain無授權問題
- 風險yt-dlp 對不同網站的相容性會隨時間腐敗,但對內部工具可接受(使用者自己升級也可以)
- 不做 auto-update yt-dlpQ6 = 不做 auto-update
---
## 5. 預置模型 .nef使用者決策 Q5 = 全打包)
### 5.1 現狀
`edge-ai-platform/server/data/nef/` 總量 ~73MB分布
- `kl520/`5-6 個 .nef影像分類、物件偵測、臉辨
- `kl720/`5-6 個 .nef同上但 KL720 版本)
### 5.2 策略
**全部打包**,不精簡。原因:
1. 使用者明確決策 Q5 = 全打包
2. 符合「完全離線、開箱即用」原則
3. ~73MB 相對於總安裝檔 ~300MB 不算大
4. 首次 demo 情境需要「看到最多示範」
> **授權待 Kneron 確認**:第四輪決策 R4-1 維持「不主動詢問、發佈前 gate」。若屆時確認不允許 re-distribute → 觸發 Plan B見 [`plan-b-online-download.md`](./plan-b-online-download.md))。
### 5.3 打包路徑
```
visiona-local/payload/data/nef/
├── kl520/
│ ├── classification_mobilenetv2.nef
│ ├── detection_yolov5s.nef
│ └── ...
└── kl720/
├── classification_resnet50.nef
└── ...
```
首次執行時解壓到 `~/Library/Application Support/visiona-local/data/nef/`(或對應平台路徑)。使用者上傳的自訂模型放在 `data/custom-models/`,兩者分開管理。
### 5.4 go:embed vs payload
**選 payload解壓式不選 go:embed。** 原因:
1. go:embed 73MB 進 Go binary 會讓 link 時間與記憶體暴漲
2. 解壓後使用者可以手動替換、刪除、增加
3. `installer/embed.go` 已有 payload 模式可沿用
---
## 6. 內嵌大小預估
### 6.1 未壓縮Wails binary 解開後)
| 項目 | macOS x64 | Windows x64 | Linux x64 |
|------|----------|-------------|-----------|
| Wails binaryGo + WebView | ~15MB | ~12MB | ~14MB |
| visiona-local-server含 embedded Next.js | ~30MB | ~30MB | ~30MB |
| python-build-standalone | ~90MB | ~90MB | ~100MB |
| Python wheelsnumpy + opencv + pyusb + KneronPLUS | ~50MB | ~50MB | ~55MB |
| ffmpeg | ~35MB | ~45MB | ~40MB |
| yt-dlp | ~10MB | ~12MB | ~10MB |
| 預置 .nef 模型 | 73MB | 73MB | 73MB |
| WinUSB driver | - | ~1MB | - |
| **合計(未壓縮)** | **~303MB** | **~313MB** | **~322MB** |
### 6.2 壓縮後(.dmg / .exe / .AppImage
Wails payload 用 tar.gz 或 zstd 壓縮,預估壓縮率 60-65%
| 平台 | 最終安裝檔大小 |
|------|--------------|
| macOS .dmg | **~195MB** |
| Windows .exeInno Setup | **~205MB** |
| Linux .AppImage | **~210MB** |
符合 PRD 的「單平台 < 500MB」目標。
---
## 7. 依賴內嵌檢查清單(給 Build Pipeline
每次 release build 時必須驗證:
- [ ] `python/<os>/python3 --version` 可執行
- [ ] `wheels/<os>/` 中所有 .whl 存在且對應正確的 cp312 + 平台 tag
- [ ] `bin/ffmpeg` 存在,`ffmpeg -version` 可執行,且**版本字串中有 "LGPL"**(不可是 GPL
- [ ] `bin/yt-dlp` 存在,`yt-dlp --version` 可執行
- [ ] `data/nef/kl520/*.nef` 與 `data/nef/kl720/*.nef` 與 `data/models.json` 一致
- [ ] Windows`drivers/kneron_winusb.inf` + `amd64/*.dll` 存在
- [ ] 安裝檔大小 < 500MB