feat: add SFX volume control and improve audio feedback

- Implemented separate sound effects volume control in settings.
- Updated backend to handle new `sfx_volume` parameter.
- Enhanced UI to include sound effects volume slider in admin panel.
- Integrated sound effects for various user interactions (clicks, navigation, alerts).
- Added a new global sound engine to manage audio synthesis without external files.
- Updated documentation for Docker deployment and Windows packaging.
- Refactored audio context management to ensure sound effects are available post user interaction.
This commit is contained in:
Pine
2026-08-18 07:14:13 +08:00
parent 18d28be943
commit d70d9d5fbd
22 changed files with 561 additions and 28 deletions
+29
View File
@@ -84,3 +84,32 @@ curl -X POST http://localhost:10085/api/display/command \
- 大屏数据由后端模拟引擎产生(`sim_engine.py`,与原前端 `parkData.js` 逻辑一致);前端离线时也有本地兜底
- 媒体文件存放于 `backend/media/``/file/...` 直接返回
- 安全:`backend/.env` 与根 `.env.local` 已加入 `.gitignore`,含阿里云密钥与 MQTT 凭据,勿提交
## Docker 部署(推荐)
一键构建并启动 **后端 + EMQX Broker**
```bash
# 1) (可选)配置部署参数
export MQTT_PUBLIC_HOST=192.168.1.50 # 展播端可访问的服务器局域网 IPBroker WebSocket
export EMQX_USER=dpmserver
export EMQX_PASS=你的密码
export DASHSCOPE_API_KEY=sk-xxx # AI/语音密钥(未配置时 AI 走本地规则引擎)
export DPM_S2S_ENABLED=1 # 实时语音对话开关
# 2) 构建并启动
docker compose up -d --build
# 3) 验证
curl http://127.0.0.1:10085/api/health # {"ok":true,...}
docker compose logs -f backend
```
- 端口:`10085`API/后台/媒体)、`8765`s2s 语音)、`1883/8083/18083`EMQX
- 数据卷:`dpm_media`(媒体)、`dpm_knowledge`(知识库 park.md,可热更新)、`dpm_data`(设置/播放列表)
- 展播端(Tauri 大屏)启动时经 `GET /api/config` 自动获取 MQTT WebSocket 地址
- 常用命令:`docker compose down` 停止;`docker compose up -d --build backend` 仅重建后端;
`docker compose ps` 查看状态
> 单独构建镜像:`docker build -f backend/Dockerfile -t dpm-backend .`
+1
View File
@@ -72,6 +72,7 @@ async def admin_page(request: Request):
"play_mode": s.get("play_mode", "sequential"),
"image_duration": s.get("image_duration", 5),
"volume": s.get("volume", 80),
"sfx_volume": s.get("sfx_volume", 60),
"fullscreen": s.get("fullscreen", True),
"autostart": s.get("autostart", False),
"from_screen": request.query_params.get("from") == "screen",
+5 -2
View File
@@ -38,6 +38,7 @@ def _media_type(path):
class SettingsBody(BaseModel):
volume: int | None = None
sfx_volume: int | None = None
play_mode: str | None = None
image_duration: int | None = None
fullscreen: bool | None = None
@@ -106,7 +107,7 @@ async def login(request: Request):
@router.get("/api/settings")
async def get_settings():
s = storage.get_settings()
return {k: s[k] for k in ("volume", "play_mode", "image_duration", "fullscreen", "autostart")}
return {k: s[k] for k in ("volume", "sfx_volume", "play_mode", "image_duration", "fullscreen", "autostart")}
@router.get("/api/config")
@@ -135,7 +136,9 @@ async def runtime_config():
async def update_settings(body: SettingsBody):
storage.update_settings(**body.model_dump(exclude_none=True))
s = storage.get_settings()
hub.publish_command("settings_changed", {"volume": s["volume"], "play_mode": s["play_mode"]})
hub.publish_command("settings_changed", {
"volume": s["volume"], "sfx_volume": s["sfx_volume"], "play_mode": s["play_mode"],
})
return {"ok": True}
+1
View File
@@ -12,6 +12,7 @@ DEFAULT_SETTINGS = {
"username": "admin",
"password": "123456",
"volume": 80,
"sfx_volume": 60,
"auto_play": True,
"play_mode": "sequential",
"image_duration": 5,
+1 -1
View File
@@ -67,7 +67,7 @@ def main():
print(f" 本地资源 : TORCH_HOME={os.environ.get('TORCH_HOME')}")
print(f" NLTK_DATA={os.environ.get('NLTK_DATA')}")
print("=" * 64)
uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=True)
uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=False)
if __name__ == "__main__":
+14 -1
View File
@@ -222,7 +222,20 @@
volumeLabel.textContent = volumeRange.value + '%';
if (volTimer) clearTimeout(volTimer);
volTimer = setTimeout(function () {
post('/api/settings', { volume: Number(volumeRange.value) }, function () { toast('音量已更新'); });
post('/api/settings', { volume: Number(volumeRange.value) }, function () { toast('媒体音量已更新'); });
}, 300);
});
}
var sfxVolumeRange = document.getElementById('sfxVolumeRange');
var sfxVolumeLabel = document.getElementById('sfxVolumeLabel');
var sfxVolTimer = null;
if (sfxVolumeRange && sfxVolumeLabel) {
sfxVolumeRange.addEventListener('input', function () {
sfxVolumeLabel.textContent = sfxVolumeRange.value + '%';
if (sfxVolTimer) clearTimeout(sfxVolTimer);
sfxVolTimer = setTimeout(function () {
post('/api/settings', { sfx_volume: Number(sfxVolumeRange.value) }, function () { toast('音效音量已更新'); });
}, 300);
});
}
+6 -1
View File
@@ -230,10 +230,15 @@
<input type="number" id="imageDuration" value="{{ image_duration }}" min="1" max="300">
</div>
<div class="form-group">
<label>音量</label>
<label>音量(媒体)</label>
<input type="range" id="volumeRange" min="0" max="100" value="{{ volume }}">
<div class="volume-label" id="volumeLabel">{{ volume }}%</div>
</div>
<div class="form-group">
<label>音量(音效)</label>
<input type="range" id="sfxVolumeRange" min="0" max="100" value="{{ sfx_volume }}">
<div class="volume-label" id="sfxVolumeLabel">{{ sfx_volume }}%</div>
</div>
</div>
</div>