feat: move asr to windows and disable local model
This commit is contained in:
@@ -562,3 +562,11 @@
|
|||||||
- `huobao-drama` on `:5678`
|
- `huobao-drama` on `:5678`
|
||||||
- Extended `deploy_fnos_storyforge_lan_stack.sh` so the NAS LAN stack can recreate model gateway, n8n, huobao, live recorder, collector and web from repo-managed assets.
|
- Extended `deploy_fnos_storyforge_lan_stack.sh` so the NAS LAN stack can recreate model gateway, n8n, huobao, live recorder, collector and web from repo-managed assets.
|
||||||
- Switched collector fnOS defaults away from the Mac host for `LOCAL_OPENAI_BASE_URL`, `N8N_BASE_URL`, and `HUOBAO_BASE_URL`, so the NAS stack no longer depends on local disk-hosted services for those routes.
|
- Switched collector fnOS defaults away from the Mac host for `LOCAL_OPENAI_BASE_URL`, `N8N_BASE_URL`, and `HUOBAO_BASE_URL`, so the NAS stack no longer depends on local disk-hosted services for those routes.
|
||||||
|
# 2026-04-06
|
||||||
|
|
||||||
|
## 公网模型 / Windows ASR 收口
|
||||||
|
|
||||||
|
- 默认不再为 fnOS collector 注入 `LOCAL_OPENAI_BASE_URL`,避免运行链继续误依赖本机 `8317`
|
||||||
|
- 公网 collector 示例配置改为显式禁用 `local_model`,并把 `ASR` 桥接端口切到 `127.0.0.1:28088`
|
||||||
|
- 新增 Windows `ASR HTTP` 服务资产,兼容 StoryForge 当前 `/transcribe` 协议,便于把 ASR 迁到 Windows 主机 `192.168.31.18`
|
||||||
|
- Windows 端新增 `ASR` 启动脚本、云端桥接脚本与计划任务注册脚本,并放通 `8088` 入站,保证局域网和公网都可直连该 `ASR` 服务
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
- 云服务器 `127.0.0.1:8081` -> 云服务器本地 `collector-service`
|
- 云服务器 `127.0.0.1:8081` -> 云服务器本地 `collector-service`
|
||||||
- 云服务器 `127.0.0.1:19191` -> 云服务器本地 `StoryForge Web V4` 静态服务
|
- 云服务器 `127.0.0.1:19191` -> 云服务器本地 `StoryForge Web V4` 静态服务
|
||||||
- 云服务器 `127.0.0.1:15670` -> 本机 `n8n :5670`
|
- 云服务器 `127.0.0.1:15670` -> 本机 `n8n :5670`
|
||||||
- 云服务器 `127.0.0.1:18317` -> 本机模型网关 `:8317`
|
- 云服务器不再默认依赖本机模型网关
|
||||||
- 云服务器 `127.0.0.1:18088` -> 本机 `ASR :8088`
|
- 云服务器 `127.0.0.1:28088` -> Windows `ASR :8088`
|
||||||
- 云服务器 `127.0.0.1:15678` -> 本机 `huobao :5678`
|
- 云服务器 `127.0.0.1:15678` -> 本机 `huobao :5678`
|
||||||
- 云服务器 `127.0.0.1:17860` -> 局域网 Windows `cutvideo :7860`
|
- 云服务器 `127.0.0.1:17860` -> 局域网 Windows `cutvideo :7860`
|
||||||
- 云服务器 `127.0.0.1:19106` -> 局域网 NAS `live-recorder :19106`
|
- 云服务器 `127.0.0.1:19106` -> 局域网 NAS `live-recorder :19106`
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ Environment=JOBS_DIR=/home/ubuntu/storyforge/data/collector/jobs
|
|||||||
Environment=DOWNLOADS_DIR=/home/ubuntu/storyforge/data/collector/downloads
|
Environment=DOWNLOADS_DIR=/home/ubuntu/storyforge/data/collector/downloads
|
||||||
Environment=MODELS_DIR=/home/ubuntu/storyforge/data/collector/models
|
Environment=MODELS_DIR=/home/ubuntu/storyforge/data/collector/models
|
||||||
Environment=DEFAULT_EXTERNAL_BASE_URL=https://storyforge.hyzq.net
|
Environment=DEFAULT_EXTERNAL_BASE_URL=https://storyforge.hyzq.net
|
||||||
Environment=LOCAL_OPENAI_BASE_URL=http://127.0.0.1:18317/v1
|
Environment=LOCAL_OPENAI_BASE_URL=
|
||||||
Environment=ASR_HTTP_BASE_URL=http://127.0.0.1:18088
|
Environment=ASR_HTTP_BASE_URL=http://127.0.0.1:28088
|
||||||
Environment=N8N_BASE_URL=http://127.0.0.1:15670
|
Environment=N8N_BASE_URL=http://127.0.0.1:15670
|
||||||
Environment=ORCHESTRATOR_SHARED_SECRET=__set_a_strong_shared_secret__
|
Environment=ORCHESTRATOR_SHARED_SECRET=__set_a_strong_shared_secret__
|
||||||
Environment=BOOTSTRAP_SUPERADMIN_USERNAME=storyforge-admin
|
Environment=BOOTSTRAP_SUPERADMIN_USERNAME=storyforge-admin
|
||||||
|
|||||||
90
deploy/storyforge-windows-asr-http/app.py
Normal file
90
deploy/storyforge-windows-asr-http/app.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import FastAPI, File, UploadFile
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
MODEL_NAME = os.getenv("WHISPER_MODEL", "base")
|
||||||
|
LANGUAGE = os.getenv("WHISPER_LANGUAGE", "zh")
|
||||||
|
DEVICE = os.getenv("WHISPER_DEVICE", "cpu")
|
||||||
|
COMPUTE_TYPE = os.getenv("WHISPER_COMPUTE_TYPE", "int8")
|
||||||
|
BEAM_SIZE = int(os.getenv("WHISPER_BEAM_SIZE", "5"))
|
||||||
|
VAD_FILTER = os.getenv("WHISPER_VAD_FILTER", "1").strip().lower() not in {"0", "false", "no"}
|
||||||
|
DOWNLOAD_ROOT = Path(os.getenv("WHISPER_DOWNLOAD_ROOT", str(Path(__file__).resolve().parent / "models-cache")))
|
||||||
|
|
||||||
|
app = FastAPI(title="storyforge-windows-asr", version="1.0.0")
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_model():
|
||||||
|
from faster_whisper import WhisperModel
|
||||||
|
|
||||||
|
DOWNLOAD_ROOT.mkdir(parents=True, exist_ok=True)
|
||||||
|
return WhisperModel(
|
||||||
|
MODEL_NAME,
|
||||||
|
device=DEVICE,
|
||||||
|
compute_type=COMPUTE_TYPE,
|
||||||
|
download_root=str(DOWNLOAD_ROOT),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
def health() -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"service": "storyforge-windows-asr",
|
||||||
|
"model_name": MODEL_NAME,
|
||||||
|
"language": LANGUAGE,
|
||||||
|
"device": DEVICE,
|
||||||
|
"compute_type": COMPUTE_TYPE,
|
||||||
|
"download_root": str(DOWNLOAD_ROOT),
|
||||||
|
"model_loaded": get_model.cache_info().currsize > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root() -> dict[str, str]:
|
||||||
|
return {"service": "storyforge-windows-asr", "docs": "/docs"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/transcribe", response_model=None)
|
||||||
|
async def transcribe(wav: UploadFile = File(...)):
|
||||||
|
started = time.perf_counter()
|
||||||
|
suffix = Path(wav.filename or "segment.wav").suffix or ".wav"
|
||||||
|
with tempfile.NamedTemporaryFile(prefix="storyforge-asr-", suffix=suffix, delete=False) as handle:
|
||||||
|
temp_path = Path(handle.name)
|
||||||
|
handle.write(await wav.read())
|
||||||
|
|
||||||
|
try:
|
||||||
|
model = get_model()
|
||||||
|
segments, _info = model.transcribe(
|
||||||
|
str(temp_path),
|
||||||
|
language=LANGUAGE or None,
|
||||||
|
beam_size=max(1, BEAM_SIZE),
|
||||||
|
vad_filter=VAD_FILTER,
|
||||||
|
)
|
||||||
|
text = "".join(segment.text for segment in segments).strip()
|
||||||
|
duration_ms = int((time.perf_counter() - started) * 1000)
|
||||||
|
return {
|
||||||
|
"text": text,
|
||||||
|
"success": bool(text),
|
||||||
|
"duration_ms": duration_ms,
|
||||||
|
"error_message": None if text else "empty transcription",
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=500,
|
||||||
|
content={
|
||||||
|
"text": "",
|
||||||
|
"success": False,
|
||||||
|
"duration_ms": int((time.perf_counter() - started) * 1000),
|
||||||
|
"error_message": str(exc),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
temp_path.unlink(missing_ok=True)
|
||||||
19
deploy/storyforge-windows-asr-http/bridge-cloud.ps1
Normal file
19
deploy/storyforge-windows-asr-http/bridge-cloud.ps1
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$serverHost = if ($env:STORYFORGE_CLOUD_HOST) { $env:STORYFORGE_CLOUD_HOST } else { "111.231.132.51" }
|
||||||
|
$serverUser = if ($env:STORYFORGE_CLOUD_USER) { $env:STORYFORGE_CLOUD_USER } else { "ubuntu" }
|
||||||
|
$localPort = if ($env:STORYFORGE_ASR_LOCAL_PORT) { $env:STORYFORGE_ASR_LOCAL_PORT } else { "8088" }
|
||||||
|
$remotePort = if ($env:STORYFORGE_ASR_REMOTE_PORT) { $env:STORYFORGE_ASR_REMOTE_PORT } else { "28088" }
|
||||||
|
$identity = if ($env:STORYFORGE_CLOUD_IDENTITY) { $env:STORYFORGE_CLOUD_IDENTITY } else { (Join-Path $env:USERPROFILE ".ssh\storyforge_cloud_bridge_ed25519") }
|
||||||
|
|
||||||
|
$sshArgs = @(
|
||||||
|
"-N",
|
||||||
|
"-i", $identity,
|
||||||
|
"-o", "StrictHostKeyChecking=no",
|
||||||
|
"-o", "ServerAliveInterval=30",
|
||||||
|
"-o", "ServerAliveCountMax=3",
|
||||||
|
"-R", "127.0.0.1:$remotePort`:127.0.0.1:$localPort",
|
||||||
|
"$serverUser@$serverHost"
|
||||||
|
)
|
||||||
|
|
||||||
|
& ssh.exe @sshArgs
|
||||||
14
deploy/storyforge-windows-asr-http/launch-asr.ps1
Normal file
14
deploy/storyforge-windows-asr-http/launch-asr.ps1
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$runScript = Join-Path $scriptDir "run.ps1"
|
||||||
|
|
||||||
|
$existing = Get-NetTCPConnection -State Listen -LocalPort 8088 -ErrorAction SilentlyContinue
|
||||||
|
if ($existing) {
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Process -FilePath "powershell.exe" `
|
||||||
|
-ArgumentList @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $runScript) `
|
||||||
|
-WorkingDirectory $scriptDir `
|
||||||
|
-WindowStyle Hidden
|
||||||
22
deploy/storyforge-windows-asr-http/register-tasks.ps1
Normal file
22
deploy/storyforge-windows-asr-http/register-tasks.ps1
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$runScript = Join-Path $scriptDir "run.ps1"
|
||||||
|
$launchAsrScript = Join-Path $scriptDir "launch-asr.ps1"
|
||||||
|
$bridgeScript = Join-Path $scriptDir "bridge-cloud.ps1"
|
||||||
|
|
||||||
|
$tasks = @(
|
||||||
|
@{
|
||||||
|
Name = "StoryForgeWindowsAsr"
|
||||||
|
Script = $launchAsrScript
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Name = "StoryForgeWindowsAsrCloudBridge"
|
||||||
|
Script = $bridgeScript
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($task in $tasks) {
|
||||||
|
schtasks /Create /F /SC ONLOGON /RL HIGHEST /TN $task.Name /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File `"$($task.Script)`""
|
||||||
|
schtasks /Run /TN $task.Name
|
||||||
|
}
|
||||||
4
deploy/storyforge-windows-asr-http/requirements.txt
Normal file
4
deploy/storyforge-windows-asr-http/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
fastapi==0.116.1
|
||||||
|
uvicorn[standard]==0.35.0
|
||||||
|
python-multipart==0.0.20
|
||||||
|
faster-whisper>=1.1,<2
|
||||||
28
deploy/storyforge-windows-asr-http/run.ps1
Normal file
28
deploy/storyforge-windows-asr-http/run.ps1
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
$venvDir = Join-Path $scriptDir ".venv"
|
||||||
|
$python = "py -3.11"
|
||||||
|
|
||||||
|
if (-not (Test-Path $venvDir)) {
|
||||||
|
Invoke-Expression "$python -m venv `"$venvDir`""
|
||||||
|
}
|
||||||
|
|
||||||
|
$venvPython = Join-Path $venvDir "Scripts\python.exe"
|
||||||
|
& $venvPython -m pip install --upgrade pip
|
||||||
|
& $venvPython -m pip install -r (Join-Path $scriptDir "requirements.txt")
|
||||||
|
|
||||||
|
$env:WHISPER_MODEL = if ($env:WHISPER_MODEL) { $env:WHISPER_MODEL } else { "base" }
|
||||||
|
$env:WHISPER_LANGUAGE = if ($env:WHISPER_LANGUAGE) { $env:WHISPER_LANGUAGE } else { "zh" }
|
||||||
|
$env:WHISPER_DEVICE = if ($env:WHISPER_DEVICE) { $env:WHISPER_DEVICE } else { "cpu" }
|
||||||
|
$env:WHISPER_COMPUTE_TYPE = if ($env:WHISPER_COMPUTE_TYPE) { $env:WHISPER_COMPUTE_TYPE } else { "int8" }
|
||||||
|
$env:WHISPER_BEAM_SIZE = if ($env:WHISPER_BEAM_SIZE) { $env:WHISPER_BEAM_SIZE } else { "5" }
|
||||||
|
$env:WHISPER_VAD_FILTER = if ($env:WHISPER_VAD_FILTER) { $env:WHISPER_VAD_FILTER } else { "1" }
|
||||||
|
$env:WHISPER_DOWNLOAD_ROOT = if ($env:WHISPER_DOWNLOAD_ROOT) { $env:WHISPER_DOWNLOAD_ROOT } else { (Join-Path $scriptDir "models-cache") }
|
||||||
|
|
||||||
|
Push-Location $scriptDir
|
||||||
|
try {
|
||||||
|
& $venvPython -m uvicorn app:app --host 0.0.0.0 --port 8088
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
@@ -48,12 +48,12 @@ COLLECTOR_LOCAL_BASE_IMAGE="${STORYFORGE_COLLECTOR_LOCAL_BASE_IMAGE:-python:3.11
|
|||||||
COLLECTOR_BASE_IMAGE="${STORYFORGE_COLLECTOR_BASE_IMAGE:-docker.m.daocloud.io/library/python:3.11-slim}"
|
COLLECTOR_BASE_IMAGE="${STORYFORGE_COLLECTOR_BASE_IMAGE:-docker.m.daocloud.io/library/python:3.11-slim}"
|
||||||
WEB_AUTOLOGIN_ACCOUNT_USERNAME="${WEB_AUTOLOGIN_ACCOUNT_USERNAME:-kris}"
|
WEB_AUTOLOGIN_ACCOUNT_USERNAME="${WEB_AUTOLOGIN_ACCOUNT_USERNAME:-kris}"
|
||||||
ORCHESTRATOR_SHARED_SECRET="${ORCHESTRATOR_SHARED_SECRET:-storyforge-local-secret}"
|
ORCHESTRATOR_SHARED_SECRET="${ORCHESTRATOR_SHARED_SECRET:-storyforge-local-secret}"
|
||||||
LOCAL_OPENAI_BASE_URL="${LOCAL_OPENAI_BASE_URL:-${HOST_LAN_IP:+http://$HOST_LAN_IP:8317/v1}}"
|
LOCAL_OPENAI_BASE_URL="${LOCAL_OPENAI_BASE_URL:-}"
|
||||||
LOCAL_OPENAI_MODEL="${LOCAL_OPENAI_MODEL:-GLM-5}"
|
LOCAL_OPENAI_MODEL="${LOCAL_OPENAI_MODEL:-GLM-5}"
|
||||||
LOCAL_OPENAI_API_KEY="${LOCAL_OPENAI_API_KEY:-}"
|
LOCAL_OPENAI_API_KEY="${LOCAL_OPENAI_API_KEY:-}"
|
||||||
N8N_BASE_URL="${N8N_BASE_URL:-${HOST_LAN_IP:+http://$HOST_LAN_IP:5670}}"
|
N8N_BASE_URL="${N8N_BASE_URL:-http://$FNOS_HOST:5670}"
|
||||||
ASR_HTTP_BASE_URL="${ASR_HTTP_BASE_URL:-${HOST_LAN_IP:+http://$HOST_LAN_IP:8088}}"
|
ASR_HTTP_BASE_URL="${ASR_HTTP_BASE_URL:-http://192.168.31.18:8088}"
|
||||||
HUOBAO_BASE_URL="${HUOBAO_BASE_URL:-${HOST_LAN_IP:+http://$HOST_LAN_IP:5678}}"
|
HUOBAO_BASE_URL="${HUOBAO_BASE_URL:-http://$FNOS_HOST:5678}"
|
||||||
CUTVIDEO_BASE_URL="${CUTVIDEO_BASE_URL:-http://$FNOS_HOST:19186}"
|
CUTVIDEO_BASE_URL="${CUTVIDEO_BASE_URL:-http://$FNOS_HOST:19186}"
|
||||||
LIVE_RECORDER_BASE_URL="${LIVE_RECORDER_BASE_URL:-http://192.168.31.188:19106}"
|
LIVE_RECORDER_BASE_URL="${LIVE_RECORDER_BASE_URL:-http://192.168.31.188:19106}"
|
||||||
CLOUD_DB_PATH="${CLOUD_DB_PATH:-/home/ubuntu/storyforge/data/collector/storyforge.db}"
|
CLOUD_DB_PATH="${CLOUD_DB_PATH:-/home/ubuntu/storyforge/data/collector/storyforge.db}"
|
||||||
|
|||||||
Reference in New Issue
Block a user