# 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//` 目錄,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 - `),否則 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 /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//python3 --version` 可執行 - [ ] `wheels//` 中所有 .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