Compare commits
4 Commits
92e203422b
...
b1633fdcff
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1633fdcff | ||
|
|
3dd667197f | ||
|
|
d3e3205692 | ||
|
|
7075078d9e |
136
DEPLOYMENT.md
136
DEPLOYMENT.md
@@ -27,6 +27,31 @@ Use the startup script to run all services locally:
|
|||||||
|
|
||||||
## Backend Deployment
|
## Backend Deployment
|
||||||
|
|
||||||
|
### Quick Start (One-Click Setup)
|
||||||
|
|
||||||
|
使用一鍵設置腳本自動安裝依賴並啟動服務:
|
||||||
|
|
||||||
|
**Linux/macOS/WSL:**
|
||||||
|
```bash
|
||||||
|
./scripts/setup-backend.sh start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```batch
|
||||||
|
.\scripts\setup-backend.bat start
|
||||||
|
```
|
||||||
|
|
||||||
|
**腳本命令:**
|
||||||
|
| 命令 | 說明 |
|
||||||
|
|------|------|
|
||||||
|
| `setup` | 僅設置環境(安裝依賴) |
|
||||||
|
| `start` | 設置並啟動後端服務(預設) |
|
||||||
|
| `stop` | 停止後端服務 |
|
||||||
|
| `--port PORT` | 指定服務端口(預設: 8000) |
|
||||||
|
| `--no-sidecar` | 不安裝 Sidecar 依賴 |
|
||||||
|
|
||||||
|
### Manual Setup
|
||||||
|
|
||||||
### 1. Setup Environment
|
### 1. Setup Environment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -104,47 +129,100 @@ sudo ./scripts/deploy-backend.sh install --port 8000
|
|||||||
|
|
||||||
## Electron Client Deployment
|
## Electron Client Deployment
|
||||||
|
|
||||||
### 1. Setup
|
### 1. Prerequisites
|
||||||
|
|
||||||
|
- **Windows**: Node.js 18+, Python 3.10+ (for building Sidecar)
|
||||||
|
- **Disk Space**: 5GB+ recommended (Whisper model + build artifacts)
|
||||||
|
|
||||||
|
### 2. Quick Build (Windows)
|
||||||
|
|
||||||
|
使用一鍵打包腳本在 Windows 上建置免安裝執行檔:
|
||||||
|
|
||||||
|
```batch
|
||||||
|
# 完整建置(使用預設 localhost)
|
||||||
|
.\scripts\build-client.bat build --clean
|
||||||
|
|
||||||
|
# 指定後端 API URL
|
||||||
|
.\scripts\build-client.bat build --api-url "http://192.168.1.100:8000/api" --clean
|
||||||
|
|
||||||
|
# 使用公司伺服器
|
||||||
|
.\scripts\build-client.bat build --api-url "https://api.company.com/api"
|
||||||
|
|
||||||
|
# 僅打包 Electron(已打包過 Sidecar)
|
||||||
|
.\scripts\build-client.bat build --skip-sidecar --api-url "http://your-server:8000/api"
|
||||||
|
```
|
||||||
|
|
||||||
|
或使用 PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\build-all.ps1 -ApiUrl "http://192.168.1.100:8000/api" -Clean
|
||||||
|
```
|
||||||
|
|
||||||
|
**打包腳本參數:**
|
||||||
|
|
||||||
|
| 參數 | 說明 |
|
||||||
|
|------|------|
|
||||||
|
| `--api-url URL` | 後端 API URL(會寫入 config.json) |
|
||||||
|
| `--skip-sidecar` | 跳過 Sidecar 打包(已打包過時使用) |
|
||||||
|
| `--clean` | 建置前清理所有暫存檔案 |
|
||||||
|
|
||||||
|
### 3. Runtime Configuration
|
||||||
|
|
||||||
|
打包後的 exe 會讀取 `config.json` 中的設定:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apiBaseUrl": "http://localhost:8000/api",
|
||||||
|
"uploadTimeout": 600000,
|
||||||
|
"appTitle": "Meeting Assistant",
|
||||||
|
"whisper": {
|
||||||
|
"model": "medium",
|
||||||
|
"device": "cpu",
|
||||||
|
"compute": "int8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式一**:打包時指定(推薦)
|
||||||
|
```batch
|
||||||
|
.\scripts\build-client.bat build --api-url "http://your-server:8000/api"
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式二**:打包前手動編輯 `client/config.json`
|
||||||
|
|
||||||
|
**方式三**:打包後修改(適合測試)
|
||||||
|
- 執行檔旁邊的 `resources/config.json` 可在打包後修改
|
||||||
|
|
||||||
|
### 4. Manual Setup (Development)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd client
|
cd client
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
npm install
|
npm install
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configure Environment
|
# Start in development mode
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy example and edit
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
**Environment Variables:**
|
|
||||||
|
|
||||||
| Variable | Description | Default |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `VITE_API_BASE_URL` | Backend API URL | http://localhost:8000/api |
|
|
||||||
| `VITE_UPLOAD_TIMEOUT` | Upload timeout (ms) | 600000 |
|
|
||||||
| `WHISPER_MODEL` | Whisper model size | medium |
|
|
||||||
| `WHISPER_DEVICE` | Execution device | cpu |
|
|
||||||
| `WHISPER_COMPUTE` | Compute precision | int8 |
|
|
||||||
|
|
||||||
### 3. Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Build for Distribution
|
### 5. Build Output
|
||||||
|
|
||||||
```bash
|
建置完成後,輸出檔案位於:
|
||||||
# Update VITE_API_BASE_URL to production server first
|
- `client/dist/` - Electron 打包輸出
|
||||||
# Then build portable executable
|
- `build/` - 最終整合輸出(含 exe)
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
The executable will be in `client/dist/`.
|
**輸出檔案:**
|
||||||
|
- `Meeting Assistant-1.0.0-portable.exe` - 免安裝執行檔
|
||||||
|
|
||||||
|
### 6. GitHub Actions (CI/CD)
|
||||||
|
|
||||||
|
也可以使用 GitHub Actions 自動建置:
|
||||||
|
|
||||||
|
1. 前往 GitHub Repository → Actions
|
||||||
|
2. 選擇 "Build Windows Client"
|
||||||
|
3. 點擊 "Run workflow"
|
||||||
|
4. 輸入 `api_url` 參數(例如 `http://192.168.1.100:8000/api`)
|
||||||
|
5. 等待建置完成後下載 artifact
|
||||||
|
|
||||||
## Transcription Sidecar
|
## Transcription Sidecar
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ let sidecarProcess;
|
|||||||
let sidecarReady = false;
|
let sidecarReady = false;
|
||||||
let streamingActive = false;
|
let streamingActive = false;
|
||||||
let appConfig = null;
|
let appConfig = null;
|
||||||
|
let activeWhisperConfig = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load configuration from external config.json
|
* Load configuration from external config.json
|
||||||
@@ -17,6 +18,11 @@ let appConfig = null;
|
|||||||
* - Packaged: <app>/resources/config.json
|
* - Packaged: <app>/resources/config.json
|
||||||
*/
|
*/
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
|
console.log("=== Config Loading Debug ===");
|
||||||
|
console.log("app.isPackaged:", app.isPackaged);
|
||||||
|
console.log("process.resourcesPath:", process.resourcesPath);
|
||||||
|
console.log("__dirname:", __dirname);
|
||||||
|
|
||||||
const configPaths = [
|
const configPaths = [
|
||||||
// Packaged app: resources folder
|
// Packaged app: resources folder
|
||||||
app.isPackaged ? path.join(process.resourcesPath, "config.json") : null,
|
app.isPackaged ? path.join(process.resourcesPath, "config.json") : null,
|
||||||
@@ -26,13 +32,18 @@ function loadConfig() {
|
|||||||
path.join(__dirname, "config.json"),
|
path.join(__dirname, "config.json"),
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
|
console.log("Config search paths:", configPaths);
|
||||||
|
|
||||||
for (const configPath of configPaths) {
|
for (const configPath of configPaths) {
|
||||||
|
const exists = fs.existsSync(configPath);
|
||||||
|
console.log(`Checking: ${configPath} - exists: ${exists}`);
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(configPath)) {
|
if (exists) {
|
||||||
const configData = fs.readFileSync(configPath, "utf-8");
|
const configData = fs.readFileSync(configPath, "utf-8");
|
||||||
appConfig = JSON.parse(configData);
|
appConfig = JSON.parse(configData);
|
||||||
console.log("Config loaded from:", configPath);
|
console.log("Config loaded from:", configPath);
|
||||||
console.log("Config:", appConfig);
|
console.log("Config content:", JSON.stringify(appConfig, null, 2));
|
||||||
|
console.log("Whisper config:", appConfig.whisper);
|
||||||
return appConfig;
|
return appConfig;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -51,7 +62,8 @@ function loadConfig() {
|
|||||||
compute: "int8"
|
compute: "int8"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
console.log("Using default config:", appConfig);
|
console.log("WARNING: No config.json found, using defaults");
|
||||||
|
console.log("Default config:", JSON.stringify(appConfig, null, 2));
|
||||||
return appConfig;
|
return appConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +137,15 @@ function startSidecar() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get Whisper configuration from config.json or environment variables
|
// Get Whisper configuration from config.json or environment variables
|
||||||
|
console.log("=== Whisper Config Resolution ===");
|
||||||
|
console.log("appConfig:", appConfig);
|
||||||
|
console.log("appConfig?.whisper:", appConfig?.whisper);
|
||||||
|
|
||||||
const whisperConfig = appConfig?.whisper || {};
|
const whisperConfig = appConfig?.whisper || {};
|
||||||
|
console.log("whisperConfig (resolved):", whisperConfig);
|
||||||
|
console.log("process.env.WHISPER_MODEL:", process.env.WHISPER_MODEL);
|
||||||
|
console.log("whisperConfig.model:", whisperConfig.model);
|
||||||
|
|
||||||
const whisperEnv = {
|
const whisperEnv = {
|
||||||
...process.env,
|
...process.env,
|
||||||
WHISPER_MODEL: process.env.WHISPER_MODEL || whisperConfig.model || "medium",
|
WHISPER_MODEL: process.env.WHISPER_MODEL || whisperConfig.model || "medium",
|
||||||
@@ -133,12 +153,20 @@ function startSidecar() {
|
|||||||
WHISPER_COMPUTE: process.env.WHISPER_COMPUTE || whisperConfig.compute || "int8",
|
WHISPER_COMPUTE: process.env.WHISPER_COMPUTE || whisperConfig.compute || "int8",
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Starting sidecar with:", sidecarExecutable, sidecarArgs.join(" "));
|
// Store the active whisper config for status reporting
|
||||||
console.log("Whisper config:", {
|
activeWhisperConfig = {
|
||||||
model: whisperEnv.WHISPER_MODEL,
|
model: whisperEnv.WHISPER_MODEL,
|
||||||
device: whisperEnv.WHISPER_DEVICE,
|
device: whisperEnv.WHISPER_DEVICE,
|
||||||
compute: whisperEnv.WHISPER_COMPUTE,
|
compute: whisperEnv.WHISPER_COMPUTE,
|
||||||
});
|
configSource: appConfig?.whisper ? "config.json" : "defaults"
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("=== Final Whisper Environment ===");
|
||||||
|
console.log("WHISPER_MODEL:", whisperEnv.WHISPER_MODEL);
|
||||||
|
console.log("WHISPER_DEVICE:", whisperEnv.WHISPER_DEVICE);
|
||||||
|
console.log("WHISPER_COMPUTE:", whisperEnv.WHISPER_COMPUTE);
|
||||||
|
console.log("Starting sidecar with:", sidecarExecutable, sidecarArgs.join(" "));
|
||||||
|
console.log("Active Whisper config:", activeWhisperConfig);
|
||||||
|
|
||||||
sidecarProcess = spawn(sidecarExecutable, sidecarArgs, {
|
sidecarProcess = spawn(sidecarExecutable, sidecarArgs, {
|
||||||
cwd: sidecarDir,
|
cwd: sidecarDir,
|
||||||
@@ -239,7 +267,11 @@ ipcMain.handle("navigate", (event, page) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-sidecar-status", () => {
|
ipcMain.handle("get-sidecar-status", () => {
|
||||||
return { ready: sidecarReady, streaming: streamingActive };
|
return {
|
||||||
|
ready: sidecarReady,
|
||||||
|
streaming: streamingActive,
|
||||||
|
whisper: activeWhisperConfig
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// === Streaming Mode IPC Handlers ===
|
// === Streaming Mode IPC Handlers ===
|
||||||
|
|||||||
@@ -170,6 +170,7 @@
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span>Transcript (逐字稿)</span>
|
<span>Transcript (逐字稿)</span>
|
||||||
|
<span id="whisper-status" style="font-size: 11px; color: #666; margin-left: 10px;" title="Whisper Model Info">Loading...</span>
|
||||||
<div class="recording-controls" style="padding: 0; display: flex; gap: 8px;">
|
<div class="recording-controls" style="padding: 0; display: flex; gap: 8px;">
|
||||||
<button class="btn btn-danger" id="record-btn">Start Recording</button>
|
<button class="btn btn-danger" id="record-btn">Start Recording</button>
|
||||||
<button class="btn btn-secondary" id="upload-audio-btn">Upload Audio</button>
|
<button class="btn btn-secondary" id="upload-audio-btn">Upload Audio</button>
|
||||||
@@ -281,6 +282,31 @@
|
|||||||
const uploadProgressEl = document.getElementById('upload-progress');
|
const uploadProgressEl = document.getElementById('upload-progress');
|
||||||
const uploadProgressText = document.getElementById('upload-progress-text');
|
const uploadProgressText = document.getElementById('upload-progress-text');
|
||||||
const uploadProgressFill = document.getElementById('upload-progress-fill');
|
const uploadProgressFill = document.getElementById('upload-progress-fill');
|
||||||
|
const whisperStatusEl = document.getElementById('whisper-status');
|
||||||
|
|
||||||
|
// Update Whisper status display
|
||||||
|
async function updateWhisperStatus() {
|
||||||
|
try {
|
||||||
|
const status = await window.electronAPI.getSidecarStatus();
|
||||||
|
if (status.whisper) {
|
||||||
|
const readyIcon = status.ready ? '✅' : '⏳';
|
||||||
|
whisperStatusEl.textContent = `${readyIcon} Model: ${status.whisper.model} | Device: ${status.whisper.device} | Compute: ${status.whisper.compute}`;
|
||||||
|
whisperStatusEl.title = `Config source: ${status.whisper.configSource || 'unknown'}`;
|
||||||
|
whisperStatusEl.style.color = status.ready ? '#28a745' : '#ffc107';
|
||||||
|
} else {
|
||||||
|
whisperStatusEl.textContent = status.ready ? '✅ Ready' : '⏳ Loading...';
|
||||||
|
whisperStatusEl.style.color = status.ready ? '#28a745' : '#ffc107';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
whisperStatusEl.textContent = '❌ Error';
|
||||||
|
whisperStatusEl.style.color = '#dc3545';
|
||||||
|
console.error('Failed to get sidecar status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial status check and periodic updates
|
||||||
|
updateWhisperStatus();
|
||||||
|
const whisperStatusInterval = setInterval(updateWhisperStatus, 5000);
|
||||||
|
|
||||||
// Load meeting data
|
// Load meeting data
|
||||||
async function loadMeeting() {
|
async function loadMeeting() {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ set "BUILD_DIR=%PROJECT_DIR%\build"
|
|||||||
REM 預設配置
|
REM 預設配置
|
||||||
set "SKIP_SIDECAR=false"
|
set "SKIP_SIDECAR=false"
|
||||||
set "CLEAN_BUILD=false"
|
set "CLEAN_BUILD=false"
|
||||||
|
set "API_URL="
|
||||||
|
|
||||||
REM 解析參數
|
REM 解析參數
|
||||||
set "COMMAND=help"
|
set "COMMAND=help"
|
||||||
@@ -35,6 +36,7 @@ if /i "%~1"=="clean" (set "COMMAND=clean" & shift & goto :parse_args)
|
|||||||
if /i "%~1"=="help" (set "COMMAND=help" & shift & goto :parse_args)
|
if /i "%~1"=="help" (set "COMMAND=help" & shift & goto :parse_args)
|
||||||
if /i "%~1"=="--skip-sidecar" (set "SKIP_SIDECAR=true" & shift & goto :parse_args)
|
if /i "%~1"=="--skip-sidecar" (set "SKIP_SIDECAR=true" & shift & goto :parse_args)
|
||||||
if /i "%~1"=="--clean" (set "CLEAN_BUILD=true" & shift & goto :parse_args)
|
if /i "%~1"=="--clean" (set "CLEAN_BUILD=true" & shift & goto :parse_args)
|
||||||
|
if /i "%~1"=="--api-url" (set "API_URL=%~2" & shift & shift & goto :parse_args)
|
||||||
echo %RED%[ERROR]%NC% 未知參數: %~1
|
echo %RED%[ERROR]%NC% 未知參數: %~1
|
||||||
goto :show_help
|
goto :show_help
|
||||||
|
|
||||||
@@ -93,6 +95,28 @@ if %errorlevel% equ 0 (
|
|||||||
echo %RED%[ERROR]%NC% 需要 Python 3.10 或更高版本
|
echo %RED%[ERROR]%NC% 需要 Python 3.10 或更高版本
|
||||||
exit /b 1
|
exit /b 1
|
||||||
|
|
||||||
|
:update_config
|
||||||
|
if "%API_URL%"=="" goto :eof
|
||||||
|
|
||||||
|
echo %BLUE%[STEP]%NC% 更新 API URL 設定...
|
||||||
|
|
||||||
|
set "CONFIG_FILE=%CLIENT_DIR%\config.json"
|
||||||
|
if not exist "%CONFIG_FILE%" (
|
||||||
|
echo %YELLOW%[WARN]%NC% 找不到 config.json,跳過 API URL 設定
|
||||||
|
goto :eof
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 使用 PowerShell 更新 JSON
|
||||||
|
powershell -Command "$config = Get-Content '%CONFIG_FILE%' -Raw | ConvertFrom-Json; $config.apiBaseUrl = '%API_URL%'; $config | ConvertTo-Json -Depth 10 | Set-Content '%CONFIG_FILE%' -Encoding UTF8"
|
||||||
|
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo %RED%[ERROR]%NC% 更新 config.json 失敗
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo %GREEN%[OK]%NC% API URL 已設定為: %API_URL%
|
||||||
|
goto :eof
|
||||||
|
|
||||||
:do_clean
|
:do_clean
|
||||||
echo %BLUE%[STEP]%NC% 清理建置目錄...
|
echo %BLUE%[STEP]%NC% 清理建置目錄...
|
||||||
|
|
||||||
@@ -276,6 +300,9 @@ if errorlevel 1 exit /b 1
|
|||||||
|
|
||||||
if "%CLEAN_BUILD%"=="true" call :do_clean
|
if "%CLEAN_BUILD%"=="true" call :do_clean
|
||||||
|
|
||||||
|
REM 更新 API URL(如果有指定)
|
||||||
|
call :update_config
|
||||||
|
|
||||||
if "%SKIP_SIDECAR%"=="false" (
|
if "%SKIP_SIDECAR%"=="false" (
|
||||||
call :setup_sidecar_venv
|
call :setup_sidecar_venv
|
||||||
call :build_sidecar
|
call :build_sidecar
|
||||||
@@ -337,13 +364,16 @@ echo clean 清理建置目錄
|
|||||||
echo help 顯示此幫助訊息
|
echo help 顯示此幫助訊息
|
||||||
echo.
|
echo.
|
||||||
echo 選項:
|
echo 選項:
|
||||||
|
echo --api-url URL 後端 API URL (預設: http://localhost:8000/api)
|
||||||
echo --skip-sidecar 跳過 Sidecar 打包
|
echo --skip-sidecar 跳過 Sidecar 打包
|
||||||
echo --clean 建置前先清理
|
echo --clean 建置前先清理
|
||||||
echo.
|
echo.
|
||||||
echo 範例:
|
echo 範例:
|
||||||
echo %~nx0 build 完整建置
|
echo %~nx0 build 完整建置 (使用預設 localhost)
|
||||||
echo %~nx0 sidecar 僅打包 Sidecar
|
echo %~nx0 build --api-url "http://192.168.1.100:8000/api" 指定後端 URL
|
||||||
echo %~nx0 electron --skip-sidecar 僅打包 Electron
|
echo %~nx0 build --api-url "https://api.company.com/api" 使用公司伺服器
|
||||||
|
echo %~nx0 sidecar 僅打包 Sidecar
|
||||||
|
echo %~nx0 electron --skip-sidecar 僅打包 Electron
|
||||||
echo.
|
echo.
|
||||||
echo 注意:
|
echo 注意:
|
||||||
echo - 首次打包 Sidecar 需下載 Whisper 模型,可能需要較長時間
|
echo - 首次打包 Sidecar 需下載 Whisper 模型,可能需要較長時間
|
||||||
|
|||||||
@@ -683,7 +683,7 @@ class Transcriber:
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
model_size = os.environ.get("WHISPER_MODEL", "small")
|
model_size = os.environ.get("WHISPER_MODEL", "medium")
|
||||||
device = os.environ.get("WHISPER_DEVICE", "cpu")
|
device = os.environ.get("WHISPER_DEVICE", "cpu")
|
||||||
compute_type = os.environ.get("WHISPER_COMPUTE", "int8")
|
compute_type = os.environ.get("WHISPER_COMPUTE", "int8")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user