#!/bin/bash # # Meeting Assistant Client - 打包腳本 # 將 Electron 應用與 Python Sidecar 打包成免安裝 exe # # 依賴: # - Node.js 18+ # - Python 3.10+ # - PyInstaller (會自動安裝) # set -e # 顏色定義 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # 專案路徑 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" CLIENT_DIR="$PROJECT_DIR/client" SIDECAR_DIR="$PROJECT_DIR/sidecar" BUILD_DIR="$PROJECT_DIR/build" # 預設配置 TARGET_PLATFORM="win" # win, mac, linux SKIP_SIDECAR=false CLEAN_BUILD=false # 函數:印出訊息 log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[OK]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } log_step() { echo -e "${CYAN}[STEP]${NC} $1" } # 函數:顯示標題 show_banner() { echo "" echo -e "${CYAN}==========================================" echo " Meeting Assistant Client Builder" echo " 打包 Electron + Sidecar 為免安裝執行檔" echo -e "==========================================${NC}" echo "" } # 函數:檢查環境 check_environment() { log_step "檢查建置環境..." local all_ok=true # 檢查 Node.js if command -v node &> /dev/null; then local node_version=$(node --version) log_success "Node.js: $node_version" else log_error "Node.js 未安裝" all_ok=false fi # 檢查 npm if command -v npm &> /dev/null; then local npm_version=$(npm --version) log_success "npm: $npm_version" else log_error "npm 未安裝" all_ok=false fi # 檢查 Python local python_cmd="" for cmd in python3 python; do if command -v $cmd &> /dev/null; then local version=$($cmd -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') local major=$($cmd -c 'import sys; print(sys.version_info.major)') local minor=$($cmd -c 'import sys; print(sys.version_info.minor)') if [ "$major" -ge 3 ] && [ "$minor" -ge 10 ]; then python_cmd=$cmd log_success "Python: $version ($cmd)" break fi fi done if [ -z "$python_cmd" ]; then log_error "需要 Python 3.10+" all_ok=false fi if [ "$all_ok" = false ]; then log_error "環境檢查失敗,請安裝缺少的依賴" exit 1 fi echo "$python_cmd" } # 函數:清理建置目錄 clean_build() { log_step "清理建置目錄..." rm -rf "$BUILD_DIR" rm -rf "$CLIENT_DIR/dist" rm -rf "$SIDECAR_DIR/dist" rm -rf "$SIDECAR_DIR/build" rm -rf "$SIDECAR_DIR/*.spec" log_success "建置目錄已清理" } # 函數:設置 Sidecar 虛擬環境 setup_sidecar_venv() { local python_cmd=$1 log_step "設置 Sidecar 建置環境..." cd "$SIDECAR_DIR" # 創建或使用現有虛擬環境 if [ ! -d "venv" ]; then log_info "創建虛擬環境..." $python_cmd -m venv venv fi # 啟動虛擬環境 if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]] || [[ "$OSTYPE" == "cygwin" ]]; then source venv/Scripts/activate else source venv/bin/activate fi # 安裝依賴 log_info "安裝 Sidecar 依賴..." pip install --upgrade pip -q pip install -r requirements.txt -q # 安裝 PyInstaller log_info "安裝 PyInstaller..." pip install pyinstaller -q log_success "Sidecar 建置環境就緒" } # 函數:打包 Sidecar build_sidecar() { log_step "打包 Sidecar (Python → 獨立執行檔)..." cd "$SIDECAR_DIR" # 啟動虛擬環境 if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]] || [[ "$OSTYPE" == "cygwin" ]]; then source venv/Scripts/activate else source venv/bin/activate fi # 確保 dist 目錄存在 mkdir -p dist # PyInstaller 打包參數 local pyinstaller_args=( --onedir # 打包成目錄 (比 onefile 啟動快) --name transcriber # 輸出名稱 --distpath dist # 輸出目錄 --workpath build # 工作目錄 --specpath . # spec 檔案位置 --noconfirm # 覆蓋現有檔案 --clean # 清理暫存檔 --log-level WARN # 日誌等級 ) # 根據平台添加特定參數 if [[ "$TARGET_PLATFORM" == "win" ]]; then pyinstaller_args+=(--console) # Windows 保留控制台 (用於 stdio 通訊) fi # 收集 faster-whisper 的隱藏導入 pyinstaller_args+=( --hidden-import=faster_whisper --hidden-import=ctranslate2 --hidden-import=huggingface_hub --hidden-import=tokenizers --hidden-import=onnxruntime --hidden-import=opencc --hidden-import=pydub --hidden-import=numpy --hidden-import=av ) # 收集 onnxruntime 資料 pyinstaller_args+=( --collect-data=onnxruntime --collect-data=faster_whisper ) log_info "執行 PyInstaller..." log_info "這可能需要幾分鐘..." pyinstaller "${pyinstaller_args[@]}" transcriber.py if [ -d "dist/transcriber" ]; then log_success "Sidecar 打包完成: $SIDECAR_DIR/dist/transcriber" else log_error "Sidecar 打包失敗" exit 1 fi } # 函數:安裝前端依賴 setup_client() { log_step "設置前端建置環境..." cd "$CLIENT_DIR" if [ ! -d "node_modules" ]; then log_info "安裝前端依賴..." npm install else log_info "前端依賴已安裝" fi # 確保 .env 存在 if [ ! -f ".env" ]; then if [ -f ".env.example" ]; then cp .env.example .env log_warn "已創建 .env 檔案,請確認設定" fi fi log_success "前端建置環境就緒" } # 函數:打包 Electron build_electron() { log_step "打包 Electron 應用..." cd "$CLIENT_DIR" # 根據目標平台設置建置參數 local build_args=() case $TARGET_PLATFORM in win) build_args+=(--win) log_info "目標平台: Windows (Portable)" ;; mac) build_args+=(--mac) log_info "目標平台: macOS (DMG)" ;; linux) build_args+=(--linux) log_info "目標平台: Linux (AppImage)" ;; *) log_error "不支援的平台: $TARGET_PLATFORM" exit 1 ;; esac log_info "執行 electron-builder..." npm run build -- "${build_args[@]}" if [ -d "dist" ]; then log_success "Electron 打包完成" log_info "輸出目錄: $CLIENT_DIR/dist" else log_error "Electron 打包失敗" exit 1 fi } # 函數:整合輸出 finalize_build() { log_step "整合建置輸出..." mkdir -p "$BUILD_DIR" # 複製 Electron 輸出 if [ -d "$CLIENT_DIR/dist" ]; then cp -r "$CLIENT_DIR/dist/"* "$BUILD_DIR/" 2>/dev/null || true fi # 顯示結果 echo "" echo -e "${CYAN}==========================================" echo " 建置完成" echo -e "==========================================${NC}" echo "" echo " 輸出目錄: $BUILD_DIR" echo "" # 列出輸出檔案 if command -v tree &> /dev/null; then tree -L 2 "$BUILD_DIR" 2>/dev/null || ls -la "$BUILD_DIR" else ls -la "$BUILD_DIR" fi echo "" log_success "打包完成!" echo "" # 平台特定說明 case $TARGET_PLATFORM in win) echo " Windows 使用說明:" echo " 1. 找到 dist/ 中的 .exe 檔案" echo " 2. 直接執行即可,無需安裝" echo "" ;; mac) echo " macOS 使用說明:" echo " 1. 打開 dist/ 中的 .dmg 檔案" echo " 2. 將應用拖到 Applications 資料夾" echo "" ;; linux) echo " Linux 使用說明:" echo " 1. 賦予 .AppImage 執行權限: chmod +x *.AppImage" echo " 2. 直接執行 AppImage 檔案" echo "" ;; esac } # 函數:顯示幫助 show_help() { echo "" echo "Meeting Assistant Client - 打包腳本" echo "" echo "用法: $0 [命令] [選項]" echo "" echo "命令:" echo " build 完整建置 (Sidecar + Electron)" echo " sidecar 僅打包 Sidecar" echo " electron 僅打包 Electron (需先打包 Sidecar)" echo " clean 清理建置目錄" echo " help 顯示此幫助訊息" echo "" echo "選項:" echo " --platform PLATFORM 目標平台: win, mac, linux (預設: win)" echo " --skip-sidecar 跳過 Sidecar 打包" echo " --clean 建置前先清理" echo "" echo "範例:" echo " $0 build # 完整建置 Windows 版本" echo " $0 build --platform linux # 建置 Linux 版本" echo " $0 sidecar # 僅打包 Sidecar" echo " $0 electron --skip-sidecar # 僅打包 Electron" echo "" echo "注意:" echo " - Windows 打包需在 Windows 環境執行" echo " - macOS 打包需在 macOS 環境執行" echo " - Linux 打包可在 Linux 或 WSL 環境執行" echo "" } # 主程式 main() { local command=${1:-"help"} # 解析參數 shift || true while [[ $# -gt 0 ]]; do case $1 in --platform) TARGET_PLATFORM="$2" shift 2 ;; --skip-sidecar) SKIP_SIDECAR=true shift ;; --clean) CLEAN_BUILD=true shift ;; *) log_error "未知參數: $1" show_help exit 1 ;; esac done case $command in build) show_banner local python_cmd=$(check_environment) if [ "$CLEAN_BUILD" = true ]; then clean_build fi if [ "$SKIP_SIDECAR" = false ]; then setup_sidecar_venv "$python_cmd" build_sidecar fi setup_client build_electron finalize_build ;; sidecar) show_banner local python_cmd=$(check_environment) if [ "$CLEAN_BUILD" = true ]; then rm -rf "$SIDECAR_DIR/dist" rm -rf "$SIDECAR_DIR/build" fi setup_sidecar_venv "$python_cmd" build_sidecar ;; electron) show_banner check_environment > /dev/null # 檢查 Sidecar 是否已打包 if [ ! -d "$SIDECAR_DIR/dist/transcriber" ] && [ "$SKIP_SIDECAR" = false ]; then log_warn "Sidecar 尚未打包" log_info "請先執行: $0 sidecar" log_info "或使用 --skip-sidecar 跳過" exit 1 fi if [ "$CLEAN_BUILD" = true ]; then rm -rf "$CLIENT_DIR/dist" fi setup_client build_electron finalize_build ;; clean) clean_build ;; help|--help|-h) show_help ;; *) log_error "未知命令: $command" show_help exit 1 ;; esac } # 執行主程式 main "$@"