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:
@@ -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 # 展播端可访问的服务器局域网 IP(Broker 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 .`
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user