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