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

15 KiB
Raw Blame History

Dependency Bundling — visionA-local

所有外部依賴的內嵌策略Python runtime、KneronPLUS、ffmpeg、yt-dlp、預置模型。 核心目標:完全離線、零外部依賴、一鍵安裝。


1. Python Runtime雙策略

1.1 總策略

主策略 A預設內嵌 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 x64cpython-3.12.x-x86_64-apple-darwin-install_only.tar.gz~30MB 壓縮 / ~90MB 解壓)
  • Windows x64cpython-3.12.x-x86_64-pc-windows-msvc-install_only.tar.gz~30MB / ~90MB
  • Linux x64cpython-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 sidecarrequirements.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_64Apple 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 codesigncodesign --force --deep --sign - <path>),否則 Gatekeeper 擋
  • 這步在 installer/app.go 已有實作,沿用
  • 首次啟動時可能跳 Gatekeeper 警告(因為沒有正式簽章),需要使用者右鍵 → 開啟(文件寫清楚)

Linux

  • 需要 libusb-1.0.so.0。Ubuntu 22.04/24.04 預設不安裝,但是:
    • 方法 Aapt-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 不 GPLGPL 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.zipffmpeg.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
    }
    // 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
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 Bplan-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/*.nefdata/nef/kl720/*.nefdata/models.json 一致
  • Windowsdrivers/kneron_winusb.inf + amd64/*.dll 存在
  • 安裝檔大小 < 500MB