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>
15 KiB
Dependency Bundling — visionA-local
所有外部依賴的內嵌策略:Python runtime、KneronPLUS、ffmpeg、yt-dlp、預置模型。 核心目標:完全離線、零外部依賴、一鍵安裝。
1. Python Runtime(雙策略)
1.1 總策略
主策略 A(預設):內嵌 python-build-standalone。
備策略 B(fallback):偵測系統 python3。
這是使用者明確決策(Q1 = A 且保留 B):正常使用者永遠走 A,但保留 B 作為:
- 開發模式(
--devflag,不用解壓內嵌 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:內嵌解壓流程
// 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
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 記憶體,不是安裝時間。
離線安裝指令:
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 內部
- 方法 A:
- 需要 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 步驟:
- pip 離線安裝 wheel(解壓 .dylib/.so/.dll 到 venv/site-packages)
- (macOS)對
.dylib做 ad-hoc sign - (Linux)寫 udev rule
- (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 LGPL static build | ffmpeg |
~35MB |
| Windows x64 | BtbN/FFmpeg-Builds ffmpeg-n6.1-latest-win64-lgpl-shared-6.1.zip 的 ffmpeg.exe(需連同幾個 DLL) |
ffmpeg.exe + DLLs |
~45MB |
| Linux x64 | johnvansickle.com 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", ...) 呼叫。新版改為:
// 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 |
| 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 策略
全部打包,不精簡。原因:
- 使用者明確決策 Q5 = 全打包
- 符合「完全離線、開箱即用」原則
- ~73MB 相對於總安裝檔 ~300MB 不算大
- 首次 demo 情境需要「看到最多示範」
授權待 Kneron 確認:第四輪決策 R4-1 維持「不主動詢問、發佈前 gate」。若屆時確認不允許 re-distribute → 觸發 Plan B(見
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。 原因:
- go:embed 73MB 進 Go binary 會讓 link 時間與記憶體暴漲
- 解壓後使用者可以手動替換、刪除、增加
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 + 平台 tagbin/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