Compare commits

...

16 Commits

Author SHA1 Message Date
libingxiang
d5f0a21f96 开始对话界面背景图切换+开始对话框居中
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m28s
2026-07-17 09:41:45 +08:00
xupan
1be7759250 feat:调整1080*1920样式
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m0s
2026-07-13 19:11:21 +08:00
xupan
36cd9aeaf0 feat:增加状态提示文本
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m56s
2026-07-11 21:31:08 +08:00
xupan
e3f81d4f7b feat:部分功能优化
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m1s
2026-07-11 15:39:48 +08:00
libingxiang
bfde0250e3 按钮文字放大
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m56s
2026-07-08 11:22:05 +08:00
libingxiang
3b280eb579 按钮位置调整+商标挂断
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m53s
2026-07-06 15:15:43 +08:00
libingxiang
4bf7369f92 播报中说话会停掉声音,而且不拾音bug修复
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m56s
2026-06-25 15:47:38 +08:00
libingxiang
beba6b81ce 开场白
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m48s
2026-06-24 10:38:10 +08:00
libingxiang
89c261e13f 开场白
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m49s
2026-06-24 10:01:33 +08:00
libingxiang
afdcae1e8e ci
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m55s
2026-06-24 09:44:24 +08:00
libingxiang
b198d73f9e ci
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m51s
2026-06-24 09:34:29 +08:00
libingxiang
9360d598b0 打断词 2026-06-24 09:26:56 +08:00
libingxiang
7a47b4d7ab 播放中打断
(cherry picked from commit 1a9cd283a1ac24c2a09d2aaf3aa5e095a2c6923a)
2026-06-24 09:18:33 +08:00
libingxiang
55502295ca 仅看ai,字幕滚动条 2026-06-16 11:10:04 +08:00
libingxiang
bb7fea71f1 仅看ai 2026-06-16 10:16:38 +08:00
libingxiang
006aa04a7f 香港数字人接口对接 2026-06-15 16:29:09 +08:00
12 changed files with 1463 additions and 1770 deletions

View File

@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: on:
push: push:
branches: branches:
- 'main**' - 'release-xg'
env: env:
BUILD: staging BUILD: staging
@ -35,12 +35,12 @@ jobs:
uses: https://gitea.yantootech.com/neil/build-push-action@v6 uses: https://gitea.yantootech.com/neil/build-push-action@v6
with: with:
push: true push: true
tags: hub.kce.ksyun.com/yantoo/yantoo-repository/baidu-digital-human-app:${{ gitea.run_id }} tags: hub.kce.ksyun.com/yantoo/yantoo-repository/xg-digital-human-app:${{ gitea.run_id }}
- name: Install - name: Install
run: | run: |
helm upgrade --install baidu-digital-human-app ./.gitea/charts \ helm upgrade --install xg-digital-human-app ./.gitea/charts \
--namespace digital-human-prod \ --namespace digital-human-prod \
--create-namespace \ --create-namespace \
--set image.repository=hub.kce.ksyun.com/yantoo/yantoo-repository/baidu-digital-human-app \ --set image.repository=hub.kce.ksyun.com/yantoo/yantoo-repository/xg-digital-human-app \
--set image.tag=${{ gitea.run_id }} --set image.tag=${{ gitea.run_id }}
- run: echo "🍏 This job's status is ${{ job.status }}." - run: echo "🍏 This job's status is ${{ job.status }}."

519
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@bddh/starling-realtime-client": "^2.0.9", "@bddh/starling-realtime-client": "^2.0.9",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"element-plus": "^2.9.7", "element-plus": "^2.9.7",
"opencc-js": "^1.4.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "^3.5.13" "vue": "^3.5.13"
}, },
@ -2010,24 +2011,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
@ -2045,24 +2028,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
@ -2080,24 +2045,6 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.21.5", "version": "0.21.5",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
@ -4546,6 +4493,12 @@
"node": ">=12.20.0" "node": ">=12.20.0"
} }
}, },
"node_modules/opencc-js": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/opencc-js/-/opencc-js-1.4.0.tgz",
"integrity": "sha512-SEb4huI5+lT4CFPA4sgs+NAOOgpcmdHZ+vYxhRLR1bQWIt6VRwpFPjWgTJAWAg1MviDcXTBe5zVoUOVxVxNK8w==",
"license": "MIT AND Apache-2.0"
},
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
@ -5258,420 +5211,6 @@
} }
} }
}, },
"node_modules/vitest/node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/vitest/node_modules/@vitest/mocker": { "node_modules/vitest/node_modules/@vitest/mocker": {
"version": "4.1.8", "version": "4.1.8",
"resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.8.tgz", "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.1.8.tgz",
@ -5699,50 +5238,6 @@
} }
} }
}, },
"node_modules/vitest/node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/vitest/node_modules/estree-walker": { "node_modules/vitest/node_modules/estree-walker": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",

View File

@ -14,6 +14,7 @@
"@bddh/starling-realtime-client": "^2.0.9", "@bddh/starling-realtime-client": "^2.0.9",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"element-plus": "^2.9.7", "element-plus": "^2.9.7",
"opencc-js": "^1.4.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "^3.5.13" "vue": "^3.5.13"
}, },

File diff suppressed because it is too large Load Diff

BIN
src/GP.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
src/assets/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

BIN
src/assets/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

View File

@ -1,60 +1,7 @@
<template> <template>
<div class="subtitle-panel"> <div class="subtitle-panel">
<!-- 字幕模式下拉框 --> <!-- AI字幕 -->
<div class="subtitle-header"> <div class="subtitle-content ai-only-mode" ref="scrollContainerRef">
<div class="dropdown-wrapper" :class="{ open: dropdownOpen }">
<button class="dropdown-trigger" @click="dropdownOpen = !dropdownOpen">
<span class="dropdown-label">{{ modeLabel }}</span>
<svg
class="dropdown-arrow"
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
<Transition name="dropdown">
<div v-if="dropdownOpen" class="dropdown-menu">
<button
v-for="opt in modeOptions"
:key="opt.value"
class="dropdown-item"
:class="{ active: currentMode === opt.value }"
@click="selectMode(opt.value)"
>
<span class="item-icon">{{ opt.icon }}</span>
<span class="item-label">{{ opt.label }}</span>
<svg
v-if="currentMode === opt.value"
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</button>
</div>
</Transition>
</div>
</div>
<!-- 模式1仅AI字幕 -->
<div
v-if="currentMode === 'ai-only'"
class="subtitle-content ai-only-mode"
ref="scrollContainerRef"
>
<div v-if="aiSubtitle" class="ai-subtitle-bar" ref="aiSubtitleBarRef"> <div v-if="aiSubtitle" class="ai-subtitle-bar" ref="aiSubtitleBarRef">
<div <div
class="ai-subtitle-text" class="ai-subtitle-text"
@ -96,217 +43,19 @@
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/> />
</svg> </svg>
等待AI回复... 等待AI回覆...
</div>
</div>
<!-- 模式2对话气泡 -->
<div
v-if="currentMode === 'bubble'"
class="subtitle-content bubble-mode"
ref="scrollContainerRef"
>
<div v-if="messages.length === 0" class="subtitle-placeholder">
<svg
viewBox="0 0 24 24"
width="20"
height="20"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/>
</svg>
等待对话开始...
</div>
<div class="chat-list" ref="chatListRef">
<div
v-for="msg in messages"
:key="msg.id"
:class="[
'chat-bubble',
msg.role === 'user' ? 'bubble-user' : 'bubble-ai',
]"
>
<div class="bubble-body">
<div
class="bubble-text"
:class="{
collapsed: isCollapsed(msg.id) && isLongText(msg.content),
'text-user': msg.role === 'user',
'text-ai': msg.role === 'ai',
}"
:ref="(el) => setMsgRef(msg.id, el)"
>
{{ msg.content }}
</div>
<button
v-if="isLongText(msg.content)"
class="expand-btn"
@click="toggleCollapse(msg.id)"
>
{{ isCollapsed(msg.id) ? "展开全部" : "收起" }}
<svg
viewBox="0 0 24 24"
width="12"
height="12"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
:style="{
transform: isCollapsed(msg.id)
? 'rotate(0deg)'
: 'rotate(180deg)',
transition: 'transform 0.2s',
}"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- 模式3合并气泡 -->
<div
v-if="currentMode === 'merged'"
class="subtitle-content merged-mode"
ref="scrollContainerRef"
>
<div v-if="messages.length === 0" class="subtitle-placeholder">
<svg
viewBox="0 0 24 24"
width="20"
height="20"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/>
</svg>
等待对话开始...
</div>
<div class="merged-list" ref="mergedListRef">
<div
v-for="(pair, idx) in pairedMessages"
:key="idx"
class="merged-bubble"
>
<div v-if="pair.user" class="merged-user-section">
<div
class="merged-text text-user"
:class="{
collapsed:
isCollapsed(pair.user.id) && isLongText(pair.user.content),
}"
>
{{ pair.user.content }}
</div>
<button
v-if="isLongText(pair.user.content)"
class="expand-btn"
@click="toggleCollapse(pair.user.id)"
>
{{ isCollapsed(pair.user.id) ? "展开全部" : "收起" }}
<svg
viewBox="0 0 24 24"
width="12"
height="12"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
:style="{
transform: isCollapsed(pair.user.id)
? 'rotate(0deg)'
: 'rotate(180deg)',
transition: 'transform 0.2s',
}"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
</div>
<div v-if="pair.ai" class="merged-ai-section">
<div
class="merged-text text-ai"
:class="{
collapsed:
isCollapsed(pair.ai.id) && isLongText(pair.ai.content),
}"
>
{{ pair.ai.content }}
</div>
<button
v-if="isLongText(pair.ai.content)"
class="expand-btn"
@click="toggleCollapse(pair.ai.id)"
>
{{ isCollapsed(pair.ai.id) ? "展开全部" : "收起" }}
<svg
viewBox="0 0 24 24"
width="12"
height="12"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
:style="{
transform: isCollapsed(pair.ai.id)
? 'rotate(0deg)'
: 'rotate(180deg)',
transition: 'transform 0.2s',
}"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import { ref, watch, nextTick, onMounted, onBeforeUnmount } from "vue";
ref,
computed,
watch,
nextTick,
onMounted,
onBeforeUnmount,
} from "vue";
import type { ChatMessage, SubtitleMode } from "@/types";
const props = defineProps<{ const props = defineProps<{
mode: SubtitleMode;
aiSubtitle: string; aiSubtitle: string;
userSubtitle: string;
messages: ChatMessage[];
}>(); }>();
const emit = defineEmits<{
"update:mode": [value: SubtitleMode];
}>();
const currentMode = computed({
get: () => props.mode,
set: (val: SubtitleMode) => emit("update:mode", val),
});
const dropdownOpen = ref(false);
// ref .subtitle-content // ref .subtitle-content
const scrollContainerRef = ref<HTMLElement | null>(null); const scrollContainerRef = ref<HTMLElement | null>(null);
const aiSubtitleBarRef = ref<HTMLElement | null>(null); const aiSubtitleBarRef = ref<HTMLElement | null>(null);
@ -316,20 +65,59 @@ const aiSubtitleTextRef = ref<HTMLElement | null>(null);
const aiFontSize = ref(18); const aiFontSize = ref(18);
const aiTextOverflow = ref(false); const aiTextOverflow = ref(false);
// AI // 1440×2560
const isTargetResolution = ref(false);
/**
* 检测当前视口是否为 1440×2560 分辨率
* 使用 window.innerWidth innerHeight 精确匹配
*/
const checkResolution = () => {
const TARGET_WIDTH = 1440;
const TARGET_HEIGHT = 2560;
console.log("------------", window.innerWidth, window.innerHeight);
isTargetResolution.value =
(window.innerWidth === TARGET_WIDTH &&
window.innerHeight === TARGET_HEIGHT) ||
(window.innerWidth > 1000 && window.innerHeight > 1680);
};
/**
* 获取目标分辨率下的字号基准值
* 1440×2560 下使用更大的字号以确保清晰可读
*/
const getTargetFontSizeBase = (textLength: number): number => {
if (textLength <= 50) return 36;
if (textLength <= 100) return 32;
if (textLength <= 200) return 28;
if (textLength <= 400) return 24;
return 22;
};
/**
* 获取普通分辨率下的字号基准值
*/
const getNormalFontSizeBase = (textLength: number): number => {
if (textLength <= 50) return 18;
if (textLength <= 100) return 16;
if (textLength <= 200) return 14;
if (textLength <= 400) return 13;
return 12;
};
// AI
const computeAIFontSize = () => { const computeAIFontSize = () => {
const text = props.aiSubtitle; const text = props.aiSubtitle;
console.log("------------", text);
if (!text) { if (!text) {
aiFontSize.value = 18; aiFontSize.value = isTargetResolution.value ? 36 : 18;
return; return;
} }
const len = text.length; const len = text.length;
if (len <= 50) aiFontSize.value = 18; aiFontSize.value = isTargetResolution.value
else if (len <= 100) aiFontSize.value = 16; ? getTargetFontSizeBase(len)
else if (len <= 200) aiFontSize.value = 14; : getNormalFontSizeBase(len);
else if (len <= 400) aiFontSize.value = 13;
else aiFontSize.value = 12;
}; };
// AI // AI
@ -352,111 +140,32 @@ const scrollAIToBottom = () => {
} }
}; };
// ===== / ===== /**
const COLLAPSE_THRESHOLD = 120; // 120 * 处理视口尺寸变化重新检测分辨率并更新字号
const collapsedIds = ref<Set<string>>(new Set()); */
const msgRefs = new Map<string, HTMLElement>(); const handleResize = () => {
const wasTarget = isTargetResolution.value;
const isLongText = (text: string): boolean => text.length > COLLAPSE_THRESHOLD; checkResolution();
//
const isCollapsed = (id: string): boolean => collapsedIds.value.has(id); if (wasTarget !== isTargetResolution.value) {
computeAIFontSize();
const toggleCollapse = (id: string) => {
if (collapsedIds.value.has(id)) {
collapsedIds.value.delete(id);
} else {
collapsedIds.value.add(id);
}
//
collapsedIds.value = new Set(collapsedIds.value);
};
const setMsgRef = (id: string, el: any) => {
if (el) msgRefs.set(id, el as HTMLElement);
};
// ===== =====
const modeOptions = [
{ value: "ai-only" as SubtitleMode, label: "AI字幕", icon: "💬" },
{ value: "bubble" as SubtitleMode, label: "对话气泡", icon: "💭" },
{ value: "merged" as SubtitleMode, label: "合并气泡", icon: "📋" },
];
const modeLabel = computed(() => {
return (
modeOptions.find((o) => o.value === currentMode.value)?.label || "AI字幕"
);
});
const selectMode = (val: SubtitleMode) => {
currentMode.value = val;
dropdownOpen.value = false;
};
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest(".dropdown-wrapper")) {
dropdownOpen.value = false;
} }
}; };
onMounted(() => { onMounted(() => {
document.addEventListener("click", handleClickOutside); //
checkResolution();
computeAIFontSize();
//
window.addEventListener("resize", handleResize);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener("click", handleClickOutside); window.removeEventListener("resize", handleResize);
});
// ===== =====
const pairedMessages = computed(() => {
const pairs: { user: ChatMessage | null; ai: ChatMessage | null }[] = [];
let currentPair: { user: ChatMessage | null; ai: ChatMessage | null } = {
user: null,
ai: null,
};
for (const msg of props.messages) {
if (msg.role === "user") {
if (currentPair.user) {
pairs.push(currentPair);
currentPair = { user: msg, ai: null };
} else {
currentPair.user = msg;
}
} else {
currentPair.ai = msg;
pairs.push(currentPair);
currentPair = { user: null, ai: null };
}
}
if (currentPair.user || currentPair.ai) {
pairs.push(currentPair);
}
return pairs;
}); });
// ===== ===== // ===== =====
//
watch(
() => props.mode,
() => {
computeAIFontSize();
nextTick(() => {
checkAIOverflow();
scrollToBottom();
// AI
if (props.mode === "ai-only" && aiSubtitleBarRef.value) {
aiSubtitleBarRef.value.scrollTo({
top: aiSubtitleBarRef.value.scrollHeight,
behavior: "instant" as ScrollBehavior,
});
}
});
},
);
// AI + + // AI + +
watch( watch(
() => props.aiSubtitle, () => props.aiSubtitle,
@ -474,55 +183,6 @@ watch(
}); });
}, },
); );
// +
watch(
() => props.messages.length,
(newLen, oldLen) => {
//
if (newLen > (oldLen ?? 0)) {
for (let i = oldLen ?? 0; i < newLen; i++) {
const msg = props.messages[i];
if (msg && isLongText(msg.content)) {
collapsedIds.value.add(msg.id);
}
}
collapsedIds.value = new Set(collapsedIds.value);
}
nextTick(() => {
scrollToBottom();
});
},
);
//
watch(
() => {
const msgs = props.messages;
if (msgs.length === 0) return "";
//
return msgs[msgs.length - 1].content;
},
() => {
nextTick(() => {
scrollToBottom();
});
},
);
/**
* 将对话列表滚动到底部
* 兼容不同内容量内容少时无需滚动内容超出可见区域时平滑滚动到底部
*/
const scrollToBottom = () => {
const el = scrollContainerRef.value;
if (!el) return;
el.scrollTo({
top: el.scrollHeight,
behavior: "smooth",
});
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -532,108 +192,6 @@ const scrollToBottom = () => {
height: 100%; height: 100%;
} }
/* ===== 下拉框 ===== */
.subtitle-header {
display: flex;
align-items: center;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
margin-bottom: 8px;
flex-shrink: 0;
}
.dropdown-wrapper {
position: relative;
}
.dropdown-trigger {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: rgba(255, 255, 255, 0.75);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.18);
}
.dropdown-wrapper.open & {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(108, 140, 255, 0.4);
}
}
.dropdown-arrow {
transition: transform 0.2s ease;
.dropdown-wrapper.open & {
transform: rotate(180deg);
}
}
.dropdown-menu {
position: absolute;
bottom: calc(100% + 8px);
left: 0;
min-width: 180px;
background: rgba(20, 25, 50, 0.97);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
padding: 6px;
z-index: 100;
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.5);
}
.dropdown-item {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 14px;
border-radius: 8px;
border: none;
background: none;
color: rgba(255, 255, 255, 0.65);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.15s;
&:hover {
background: rgba(255, 255, 255, 0.08);
color: #fff;
}
&.active {
background: rgba(108, 140, 255, 0.15);
color: #6c8cff;
}
.item-icon {
font-size: 14px;
}
.item-label {
flex: 1;
text-align: left;
}
}
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.2s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(6px) scale(0.96);
}
/* ===== 字幕内容 ===== */ /* ===== 字幕内容 ===== */
.subtitle-content { .subtitle-content {
flex: 1; flex: 1;
@ -672,7 +230,7 @@ const scrollToBottom = () => {
font-size: 14px; font-size: 14px;
} }
/* ===== 模式1AI字幕 ===== */ /* ===== AI字幕 ===== */
.ai-only-mode { .ai-only-mode {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
@ -738,170 +296,40 @@ const scrollToBottom = () => {
} }
} }
/* ===== 模式2对话气泡 ===== */ /* ===== 1440×2560 分辨率专用样式 ===== */
.bubble-mode { @media (min-width: 1000px) and (min-height: 1680px) {
.chat-list { .subtitle-placeholder {
display: flex; font-size: 24px;
flex-direction: column; line-height: 1.75;
gap: 8px; letter-spacing: 0.5px;
padding: 2px 0; font-weight: 400;
}
} }
.chat-bubble { /* AI字幕字号由 JavaScript 动态控制,此处仅设置辅助样式 */
animation: bubbleSlide 0.35s ease; .ai-subtitle-text {
line-height: 1.75;
&.bubble-user { letter-spacing: 0.6px;
.bubble-body { font-weight: 400;
background: rgba(240, 242, 255, 0.92); text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
border-color: rgba(79, 110, 247, 0.12);
border-radius: 14px 14px 4px 14px;
margin-left: 24px;
}
} }
&.bubble-ai { /* 字幕容器:背景与边框适配大字号 */
.bubble-body { .ai-subtitle-bar {
background: rgba(20, 30, 50, 0.85); padding: 18px 24px;
border-color: rgba(52, 211, 153, 0.12);
border-radius: 14px 14px 14px 4px;
margin-right: 24px;
}
}
}
@keyframes bubbleSlide {
from {
opacity: 0;
transform: translateY(10px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.bubble-body {
padding: 8px 12px;
border: 1px solid;
min-width: 0;
}
.bubble-text {
font-size: 14px;
line-height: 1.6;
word-break: break-word;
overflow: hidden;
transition: max-height 0.35s ease;
&.text-user {
color: #1a1a1a;
text-shadow: none;
}
&.text-ai {
color: #ffffff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
&.collapsed {
max-height: 4.8em;
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2em;
background: linear-gradient(transparent, rgba(10, 14, 30, 0.5));
pointer-events: none;
border-radius: 0 0 8px 8px;
}
}
}
/* ===== 展开/收起按钮 ===== */
.expand-btn {
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 6px;
padding: 3px 10px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: rgba(255, 255, 255, 0.55);
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);
border-color: rgba(255, 255, 255, 0.2);
}
}
/* ===== 模式3合并气泡 ===== */
.merged-mode {
.merged-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 2px 0;
}
}
.merged-bubble {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 12px; border-radius: 12px;
overflow: hidden; background: linear-gradient(
animation: bubbleSlide 0.35s ease; 135deg,
rgba(108, 140, 255, 0.12) 0%,
rgba(79, 110, 247, 0.06) 100%
);
border: 1px solid rgba(108, 140, 255, 0.15);
} }
.merged-user-section { /* 滚动指示器:尺寸适配 */
padding: 8px 12px; .scroll-hint {
border-bottom: 1px solid rgba(79, 110, 247, 0.08); width: 40px;
} height: 40px;
margin-top: 12px;
.merged-ai-section {
padding: 8px 12px;
}
.merged-text {
font-size: 14px;
line-height: 1.6;
word-break: break-word;
overflow: hidden;
transition: max-height 0.35s ease;
&.text-user {
color: #303030ab;
text-shadow: none;
}
&.text-ai {
color: #ffffff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
&.collapsed {
max-height: 4.8em;
position: relative;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2em;
background: linear-gradient(transparent, rgba(10, 14, 30, 0.5));
pointer-events: none;
}
} }
} }
</style> </style>

View File

@ -4,18 +4,27 @@ import RealTimeHuman from "@bddh/starling-realtime-client";
import { checkPlayUnMute } from "@/utils/audioUtils"; import { checkPlayUnMute } from "@/utils/audioUtils";
import { SilenceDetector } from "@/utils/SilenceDetector"; import { SilenceDetector } from "@/utils/SilenceDetector";
import { TokenManager } from "@/utils/tokenManager"; import { TokenManager } from "@/utils/tokenManager";
import type { ChatMessage, SubtitleMode } from "@/types"; import type { ChatMessage } from "@/types";
const DEFAULT_TEXT = const DEFAULT_TEXT = "你好!我是皮特教授,歡迎向我提問任何有關電池的問題。";
"你好,我是东航小云,请问有什么需要帮忙的吗?";
const OPENING_TTS_DELAY_MS = 200; const OPENING_TTS_DELAY_MS = 200;
const SILENCE_TIMEOUT_MS = 5000; const SILENCE_TIMEOUT_MS = 5000;
// 最短有效语音时长(秒),低于此时长视为无效输入 // 最短有效语音时长(秒),低于此时长视为无效输入
const MIN_VALID_DURATION_SEC = 0.5; const MIN_VALID_DURATION_SEC = 0.5;
// 播报超时保护毫秒如果FINISHED回调未触发自动恢复状态 // 播报超时保护毫秒如果FINISHED回调未触发自动恢复状态
const BROADCAST_TIMEOUT_MS = 60000; const BROADCAST_TIMEOUT_MS = 60000;
const APPID = "i-sfkmd7h8ex6nd"; const APPID = "i-sfyrkdr0ubepa";
const APP_KEY = "hs6b9nu9b080ap9hswgp"; const APP_KEY = "72vpdansbpg8nxjbxzhp";
// 打断词列表:识别到这些词时自动打断播报
const INTERRUPT_KEYWORDS = [
"稍等一下",
"停一下",
"抱歉打断一下",
"不好意思",
"对不起",
"停",
"闭嘴",
];
/** /**
* *
@ -29,6 +38,17 @@ const hasSubstantialContent = (text: string): boolean => {
return cleaned.length > 0; return cleaned.length > 0;
}; };
/**
*
*/
const containsInterruptKeyword = (text: string): boolean => {
const cleaned = text.replace(
/[\s\u3000.,!?;:,。!?;:、…—\-_·""''()\[\]【】《》<>]/g,
"",
);
return INTERRUPT_KEYWORDS.some((keyword) => cleaned.includes(keyword));
};
enum ReadyState { enum ReadyState {
UNINSTANTIATED = -1, UNINSTANTIATED = -1,
CONNECTING = 0, CONNECTING = 0,
@ -114,7 +134,6 @@ export function useDigitalHuman(options?: {
const isBroadcasting = ref(false); const isBroadcasting = ref(false);
// 字幕相关 // 字幕相关
const subtitleMode = ref<SubtitleMode>("ai-only");
const currentAISubtitle = ref(""); const currentAISubtitle = ref("");
const currentUserSubtitle = ref(""); const currentUserSubtitle = ref("");
const chatMessages = ref<ChatMessage[]>([]); const chatMessages = ref<ChatMessage[]>([]);
@ -141,6 +160,8 @@ export function useDigitalHuman(options?: {
// 播报超时保护计时器 // 播报超时保护计时器
let broadcastTimeoutId: ReturnType<typeof setTimeout> | null = null; let broadcastTimeoutId: ReturnType<typeof setTimeout> | null = null;
// 打断词检测时间戳用于忽略打断后延迟到达的ASR结果
let lastInterruptTime = 0;
const clearBroadcastTimeout = () => { const clearBroadcastTimeout = () => {
if (broadcastTimeoutId !== null) { if (broadcastTimeoutId !== null) {
@ -164,19 +185,23 @@ export function useDigitalHuman(options?: {
}; };
/** /**
* + *
* RTC模式下保持麦克风开启ASR识别打断词
*/ */
const enterBroadcasting = () => { const enterBroadcasting = () => {
isBroadcasting.value = true; isBroadcasting.value = true;
setPickPhase("broadcasting", "数字人开始播报"); setPickPhase("broadcasting", "数字人开始播报");
startBroadcastTimeout(); startBroadcastTimeout();
// RTC模式播报期间禁用拾音 // RTC模式播报期间保持麦克风开启允许ASR识别打断词
if (audioMode.value === "rtc") { if (audioMode.value === "rtc") {
humanInstanceRef.value?.muteMicrophone?.(true); humanInstanceRef.value?.muteMicrophone?.(false);
isMuted.value = true; isMuted.value = false;
canTransfer.value = false; canTransfer.value = true;
silenceDetector.pause(); const resumed = silenceDetector.resume();
console.info("[播报] RTC模式麦克风已静音静音检测器已暂停"); if (!resumed) {
startSilenceDetection();
}
console.info("[播报] RTC模式麦克风保持开启允许ASR识别打断词");
} }
console.info("[播报] 开始播报等待FINISHED信号"); console.info("[播报] 开始播报等待FINISHED信号");
}; };
@ -306,9 +331,19 @@ export function useDigitalHuman(options?: {
type, type,
} = JSON.parse(content.body); } = JSON.parse(content.body);
if (type === "QUERY") { if (type === "QUERY") {
// 播报期间忽略ASR结果双重守卫麦克风已静音但防止残余回调 // 播报期间检测打断词识别到打断词后仅中断播报并恢复拾音不调用LLM接口
if (pickPhase.value === "broadcasting") { if (pickPhase.value === "broadcasting") {
console.info("[状态机] 播报中收到ASR结果忽略"); const hasInterrupt = containsInterruptKeyword(subtitleContent);
if (hasInterrupt) {
console.info(
`[打断词检测] 播报中识别到打断词: "${subtitleContent}"自动打断仅中断播报不调用LLM`,
);
interrupt();
} else {
console.info(
`[打断词检测] 播报中收到ASR结果: "${subtitleContent}",非打断词,忽略`,
);
}
break; break;
} }
@ -321,6 +356,11 @@ export function useDigitalHuman(options?: {
// idle 状态下收到 QUERY必须验证内容有实质才允许状态转换 // idle 状态下收到 QUERY必须验证内容有实质才允许状态转换
// 环境噪音可能触发 ASR 产生空白/标点内容的 QUERY不应进入识别 // 环境噪音可能触发 ASR 产生空白/标点内容的 QUERY不应进入识别
if (pickPhase.value === "idle") { if (pickPhase.value === "idle") {
// 忽略打断后3秒内到达的延迟ASR结果避免打断词被当作新输入
if (Date.now() - lastInterruptTime < 3000) {
console.info("[状态机] 打断后延迟到达的ASR结果忽略");
break;
}
const hasSubstance = hasSubstantialContent(subtitleContent); const hasSubstance = hasSubstantialContent(subtitleContent);
if (!hasSubstance) { if (!hasSubstance) {
console.info( console.info(
@ -496,8 +536,8 @@ export function useDigitalHuman(options?: {
token: token.value, token: token.value,
wrapperId: "human-wrapper", wrapperId: "human-wrapper",
connectParams: { connectParams: {
ttsPer: "CAP_4189", ttsPer: "20007751",
figureId: "3999826", figureId: "4000821",
resolutionHeight: 1920, resolutionHeight: 1920,
resolutionWidth: 1080, resolutionWidth: 1080,
inactiveDisconnectSec: 300, inactiveDisconnectSec: 300,
@ -514,7 +554,7 @@ export function useDigitalHuman(options?: {
config.connectParams.pullAudioFromRtc = mode === "rtc"; config.connectParams.pullAudioFromRtc = mode === "rtc";
config.connectParams.pickAudioMode = config.connectParams.pickAudioMode =
mode === "btn" ? "pressButton" : "free"; mode === "btn" ? "pressButton" : "free";
config.connectParams.enableInterrupt = enableInterruptRef.value; config.connectParams.enableInterrupt = false;
} }
humanInstanceRef.value = new RealTimeHuman(config); humanInstanceRef.value = new RealTimeHuman(config);
@ -590,6 +630,8 @@ export function useDigitalHuman(options?: {
console.info("发送打断"); console.info("发送打断");
clearBroadcastTimeout(); clearBroadcastTimeout();
await humanInstanceRef.value?.interrupt?.(); await humanInstanceRef.value?.interrupt?.();
// 记录打断时间戳用于忽略打断后延迟到达的ASR结果
lastInterruptTime = Date.now();
// 打断后播报结束,恢复状态 // 打断后播报结束,恢复状态
if (isBroadcasting.value) { if (isBroadcasting.value) {
isBroadcasting.value = false; isBroadcasting.value = false;
@ -812,7 +854,6 @@ export function useDigitalHuman(options?: {
// 播报状态 // 播报状态
isBroadcasting, isBroadcasting,
// 字幕 // 字幕
subtitleMode,
currentAISubtitle, currentAISubtitle,
currentUserSubtitle, currentUserSubtitle,
chatMessages, chatMessages,

View File

@ -1,15 +1,26 @@
import { ref } from "vue"; import { ref } from "vue";
import OpenCC from "opencc-js";
// 火山方舟 Chat Completions API 配置 // 火山方舟 Chat Completions API 配置
const LLM_API_URL = const LLM_API_URL =
"https://ai.yantootech.com/polyhedron/api/ark/chat/completions"; "https://ai.yantootech.com/polyhedron/api/ark/chat/completions";
const DEFAULT_MODEL = "bot-20260605135355-68pth"; const DEFAULT_MODEL = "bot-20260605135355-68pth";
// ChatBase API 配置
const CHATBASE_API_URL = "https://www.chatbase.co/api/v1/chat";
const CHATBASE_API_KEY = "ca4616db-417c-4dce-8c65-b9909621f53e";
const CHATBASE_CHATBOT_ID = "TRIe58TuVExBm6K0HKTfz";
interface LLMMessage { interface LLMMessage {
role: "system" | "user" | "assistant"; role: "system" | "user" | "assistant";
content: string; content: string;
} }
interface ChatBaseMessage {
content: string;
role: "user";
}
interface LLMChoice { interface LLMChoice {
index: number; index: number;
message: { message: {
@ -41,7 +52,7 @@ export function useLLM() {
const SYSTEM_PROMPT: LLMMessage = { const SYSTEM_PROMPT: LLMMessage = {
role: "system", role: "system",
content: "你是一个友好的数字人助手,请用简洁自然的语言回答用户的问题。", content: "你是一個友善的數位人助手,請用簡潔自然的語言回答使用者的問題。",
}; };
const sendToLLM = async (userMessage: string): Promise<string> => { const sendToLLM = async (userMessage: string): Promise<string> => {
@ -92,7 +103,7 @@ export function useLLM() {
return aiReply; return aiReply;
} catch (err: any) { } catch (err: any) {
const errorMsg = err.message || "请求失败"; const errorMsg = err.message || "請求失敗";
error.value = errorMsg; error.value = errorMsg;
console.error("LLM请求错误:", err); console.error("LLM请求错误:", err);
throw err; throw err;
@ -105,11 +116,65 @@ export function useLLM() {
conversationHistory.value = []; conversationHistory.value = [];
}; };
const sendToChatBase = async (userMessage: string): Promise<string> => {
if (!userMessage.trim()) return "";
isLoading.value = true;
error.value = "";
try {
const messages: ChatBaseMessage[] = [
{ content: userMessage, role: "user" },
];
const response = await fetch(CHATBASE_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${CHATBASE_API_KEY}`,
},
body: JSON.stringify({
messages,
chatbotId: CHATBASE_CHATBOT_ID,
}),
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data = await response.json();
// 将简体中文回复转换为繁体中文
const rawText = data.text || "";
const aiReply = OpenCC.Converter({ from: "cn", to: "tw" })(rawText);
conversationHistory.value.push({
role: "user",
content: userMessage,
});
conversationHistory.value.push({
role: "assistant",
content: aiReply,
});
return aiReply;
} catch (err: any) {
const errorMsg = err.message || "請求失敗";
error.value = errorMsg;
console.error("ChatBase请求错误:", err);
throw err;
} finally {
isLoading.value = false;
}
};
return { return {
isLoading, isLoading,
error, error,
conversationHistory, conversationHistory,
sendToLLM, sendToLLM,
sendToChatBase,
clearHistory, clearHistory,
}; };
} }

View File

@ -5,11 +5,3 @@ export interface ChatMessage {
timestamp: number; timestamp: number;
completed: boolean; completed: boolean;
} }
export type SubtitleMode = 'ai-only' | 'bubble' | 'merged';
export const SUBTITLE_MODE_LABELS: Record<SubtitleMode, string> = {
'ai-only': 'AI字幕',
'bubble': '对话气泡',
'merged': '合并气泡',
};

View File

@ -4,7 +4,7 @@ import legacy from "@vitejs/plugin-legacy";
import path from "path"; import path from "path";
export default defineConfig({ export default defineConfig({
base: "/baidu-digital-human-app", base: "/xg-digital-human-app",
plugins: [ plugins: [
vue(), vue(),
legacy({ legacy({