diff --git a/README.md b/README.md index b5f3040..c55f5c6 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,14 @@ corepack yarn start `deploy.sh` 会构建并启动 Docker 容器 `shuziren`,并等待健康检查通过。停止服务使用 `docker compose down`;查看日志使用 `docker compose logs -f`。 +如果 Nginx 直接从 `/data/nginx/www/shuziren/` 提供静态页面,而不是把整个站点反向代理到 9000 端口,请使用: + +```bash +STATIC_DIRECTORY=/data/nginx/www/shuziren sh deploy.sh +``` + +发布脚本会强制重建容器,并将容器中的最新 `dist` 同步到该 Nginx 静态目录,避免继续显示旧视频。 + 服务器需要安装 Docker Engine 和 Docker Compose v2,并放行 TCP `9000` 入站端口。服务端必须能够访问 `https://api.deepseek.com`。 ## 安全提示 diff --git a/deploy.sh b/deploy.sh index db79163..8d1237c 100644 --- a/deploy.sh +++ b/deploy.sh @@ -16,12 +16,26 @@ if ! docker compose version >/dev/null 2>&1; then exit 1 fi -docker compose up -d --build --remove-orphans +# Do not reuse an old frontend layer: media files must always come from the +# currently uploaded source package. +docker compose build --no-cache shuziren +docker compose up -d --force-recreate --remove-orphans shuziren + +sync_static_files() { + if [ -z "${STATIC_DIRECTORY:-}" ]; then + return + fi + + mkdir -p "$STATIC_DIRECTORY" + docker cp "shuziren:/app/dist/." "$STATIC_DIRECTORY/" + echo "Static frontend synchronized to: $STATIC_DIRECTORY" +} attempt=0 while [ "$attempt" -lt 20 ]; do status=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' shuziren 2>/dev/null || true) if [ "$status" = "healthy" ]; then + sync_static_files echo "Published successfully: http://SERVER_IP:9000" docker compose ps exit 0 diff --git a/index.html b/index.html index e3e307d..d3fc1c4 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,8 @@ - DeepSeek 数字人助手 + + 钱学森数字人 · 航天科学问答
diff --git a/server/productionServer.ts b/server/productionServer.ts index f5911fa..34df1e6 100644 --- a/server/productionServer.ts +++ b/server/productionServer.ts @@ -18,6 +18,7 @@ const contentTypes: Record = { '.js': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8', + '.mp4': 'video/mp4', '.png': 'image/png', '.svg': 'image/svg+xml', '.webp': 'image/webp', diff --git a/src/App.vue b/src/App.vue index 011ad70..4c07de3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -132,7 +132,7 @@ import {checkPlayUnMute} from './utils/audioUtils'; const ReadyState = {UNINSTANTIATED: -1, CONNECTING: 0, OPEN: 1, CLOSING: 2, CLOSED: 3}; const TOOLBAR_HIDE_DELAY_MS = 3_000; const POST_SPEECH_DISCONNECT_DELAY_MS = 10_000; -const HUMAN_SCENE_FADE_OUT_MS = 360; +const HUMAN_SCENE_FADE_OUT_MS = 920; const STANDBY_CROSSFADE_MS = 650; const dhIframe = new DHIframe('digital-human-iframe'); const realTimeVideoReady = ref(false); @@ -299,7 +299,7 @@ const releaseHuman = (preservePendingAnswer = false, smooth = false) => { }; const disconnectHuman = () => { soundUnlockTip.value = false; - releaseHuman(false); + releaseHuman(false, true); }; const sendSpeech = (text) => { if (postSpeechDisconnectTimer) { @@ -350,15 +350,6 @@ const handleAnswerReady = async (text) => { deepSeekAnswer.value = text; waitingForAnswer.value = false; pendingSpeechText.value = text; - if (soundEnabled.value && !audioUnlocked.value) { - const canPlayWithSound = await checkPlayUnMute(); - if (soundEnabled.value && !canPlayWithSound && !audioUnlocked.value) { - soundUnlockTip.value = true; - showToolbar(); - return; - } - if (canPlayWithSound) audioUnlocked.value = true; - } if (!showHuman.value) connectHuman(); else { cancelHumanRelease(); @@ -449,8 +440,8 @@ const onMessage = (event) => { } if (content.action === 'localVideoMuted') { videoIsMuted.value = parseMutedState(content.body); - if (soundEnabled.value && videoIsMuted.value && (pendingSpeechText.value || speechBusy.value)) { - audioUnlocked.value = false; + if (soundEnabled.value && videoIsMuted.value && !audioUnlocked.value + && (pendingSpeechText.value || speechBusy.value)) { soundUnlockTip.value = true; showToolbar(); } @@ -474,8 +465,7 @@ const onMessage = (event) => { if (replayAfterMutedPlayback) { pendingSpeechText.value = replayText; soundUnlockTip.value = true; - audioUnlocked.value = false; - releaseHuman(true); + releaseHuman(true, true); return; } postSpeechDisconnectTimer = window.setTimeout(() => { @@ -520,8 +510,8 @@ onUnmounted(() => { .standby-video.visible { z-index: 1; }.standby-video.incoming { z-index: 2; } .standby-scene.thinking .standby-video { filter: saturate(.92) brightness(.86); } .standby-shade { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(18, 8, 4, .03) 42%, rgba(17, 8, 4, .58) 100%); } -.human-frame { z-index: 1; opacity: 0; background: #5a2f1b; pointer-events: none; transition: opacity 320ms cubic-bezier(.22, 1, .36, 1); } -.human-frame.ready { opacity: 1; pointer-events: auto; } +.human-frame { z-index: 1; opacity: 0; overflow: hidden; background: #5a2f1b; pointer-events: none; transform: translateZ(0) scale(1.004); filter: blur(1.5px); backface-visibility: hidden; will-change: opacity, transform, filter; transition: opacity 900ms cubic-bezier(.4, 0, .2, 1), transform 1100ms cubic-bezier(.22, 1, .36, 1), filter 700ms ease-out; } +.human-frame.ready { opacity: 1; pointer-events: auto; transform: translateZ(0) scale(1); filter: blur(0); } .thinking-panel { position: absolute; z-index: 2; left: 50%; bottom: clamp(120px, 17vh, 190px); display: flex; align-items: center; gap: 13px; min-width: min(360px, calc(100% - 36px)); padding: 13px 17px; transform: translateX(-50%); border: 1px solid rgba(255, 238, 220, .35); border-radius: 18px; color: #fff7ed; background: rgba(37, 18, 10, .7); box-shadow: 0 18px 48px rgba(20, 7, 2, .3); backdrop-filter: blur(13px); } .thinking-orbit { position: relative; flex: 0 0 36px; width: 36px; height: 36px; } .thinking-orbit span { position: absolute; inset: 0; border: 1px solid rgba(255, 221, 181, .44); border-radius: 50%; } diff --git a/src/assets/media/disconnected.mp4 b/src/assets/media/disconnected.mp4 deleted file mode 100644 index 04c2f4b..0000000 Binary files a/src/assets/media/disconnected.mp4 and /dev/null differ