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>
393 lines
15 KiB
Markdown
393 lines
15 KiB
Markdown
# 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)。**
|
||
**備策略 B(fallback):偵測系統 `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
|
||
}
|
||
// fallback:PATH
|
||
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 是 Unlicense(public domain),無授權問題
|
||
- 風險:yt-dlp 對不同網站的相容性會隨時間腐敗,但對內部工具可接受(使用者自己升級也可以)
|
||
- 不做 auto-update yt-dlp(Q6 = 不做 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 binary(Go + WebView) | ~15MB | ~12MB | ~14MB |
|
||
| visiona-local-server(含 embedded Next.js) | ~30MB | ~30MB | ~30MB |
|
||
| python-build-standalone | ~90MB | ~90MB | ~100MB |
|
||
| Python wheels(numpy + 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 .exe(Inno 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
|