modify: ip音柱插件
This commit is contained in:
@@ -59,7 +59,7 @@ async def _check_offline_devices(interval: float):
|
||||
from .models.orm import BoardHeartbeat
|
||||
from sqlalchemy import select
|
||||
|
||||
stmt = select(BoardHeartbeat).where(BoardHeartbeat.is_online)
|
||||
stmt = select(BoardHeartbeat).where(BoardHeartbeat.is_online) # type: ignore
|
||||
result = await session.execute(stmt)
|
||||
records = result.scalars().all()
|
||||
|
||||
@@ -116,6 +116,11 @@ class AiboxSophonAdapterPlugin(Plugin):
|
||||
doubao_cfg = config.get("doubao_analysis", {})
|
||||
doubao_enabled = doubao_cfg.get("enabled", True)
|
||||
|
||||
# ip 音柱 TTS 播放开关
|
||||
ip_sound_column_cfg = config.get("ip_sound_column", {})
|
||||
ip_sound_column_enabled = ip_sound_column_cfg.get("enabled", True)
|
||||
ip_sound_column_volume = ip_sound_column_cfg.get("volume", 50) # 播放音量 0-100
|
||||
|
||||
# 告警转发配置
|
||||
alarm_fwd_cfg = config.get("alarm_forward", {})
|
||||
alarm_fwd_enabled = alarm_fwd_cfg.get("enabled", False)
|
||||
@@ -152,6 +157,8 @@ class AiboxSophonAdapterPlugin(Plugin):
|
||||
set_global_var("upload_root", upload_root_path)
|
||||
set_global_var("alarm_max_records", alarm_max)
|
||||
set_global_var("doubao_analysis_enabled", doubao_enabled)
|
||||
set_global_var("ip_sound_column_enabled", ip_sound_column_enabled)
|
||||
set_global_var("ip_sound_column_volume", ip_sound_column_volume)
|
||||
set_global_var("alarm_forward_enabled", alarm_fwd_enabled)
|
||||
set_global_var("alarm_forward_url", alarm_fwd_url)
|
||||
set_global_var("alarm_forward_video_url", alarm_fwd_video_url)
|
||||
|
||||
@@ -45,6 +45,12 @@ alarm_max_records: int = 0 # 0 表示不限制
|
||||
# 豆包 AI 分析开关
|
||||
doubao_analysis_enabled: bool = True
|
||||
|
||||
# ip 音柱 TTS 播放开关
|
||||
ip_sound_column_enabled: bool = True
|
||||
|
||||
# ip 音柱 TTS 播放音量(0-100)
|
||||
ip_sound_column_volume: int = 50
|
||||
|
||||
# 告警转发配置
|
||||
alarm_forward_enabled: bool = False
|
||||
alarm_forward_url: str = "http://dg.datagoo.cn:60014/open-api/event-alarm/receive"
|
||||
|
||||
@@ -106,7 +106,7 @@ async def board_ping(request: BoardPingRequest, worker_id: str = "") -> dict:
|
||||
stream_id = media["MediaName"]
|
||||
source_url = media["MediaUrl"].replace("/Link", link_path)
|
||||
try:
|
||||
await stream_manager.start_stream(f"/{stream_id}", source_url)
|
||||
await stream_manager.start_stream(f"/{stream_id}", source_url) # type: ignore
|
||||
except Exception as e:
|
||||
logger.warning(f"[Heartbeat] 通道 {stream_id} 拉流失败: {e}")
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ async def list_alarms(
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
# 分页数据
|
||||
stmt = select(AlarmRecord).order_by(desc(AlarmRecord.created_at))
|
||||
stmt = select(AlarmRecord).order_by(desc(AlarmRecord.created_at)) # type: ignore
|
||||
if conditions:
|
||||
from sqlalchemy import and_
|
||||
stmt = stmt.where(and_(*conditions))
|
||||
|
||||
@@ -70,6 +70,41 @@ class _FileStream:
|
||||
assert self._file is not None, "必须在 with 上下文内使用"
|
||||
return self._file.seek(offset, whence)
|
||||
|
||||
# ============================================================
|
||||
# 辅助函数 - 播放 tts 语音
|
||||
# ============================================================
|
||||
|
||||
async def ip_sound_column_tts_play(zenoh_session, payload: dict) -> dict:
|
||||
"""
|
||||
播放 ip 音柱 tts 语音
|
||||
|
||||
可通过配置 ip_sound_column.enabled 关闭。失败时返回 {"command": ..., "result": -1}。
|
||||
|
||||
Args:
|
||||
zenoh_session: Zenoh 会话(用于调用 ip_sound_column Worker)
|
||||
payload: 请求体字典,包含 command / volume / play_count / queue / sync_response /
|
||||
play_interval / tts_pitch / tts_speed / tts_voice_name / tts_text
|
||||
"""
|
||||
from .global_var import ip_sound_column_enabled
|
||||
|
||||
if not ip_sound_column_enabled:
|
||||
logger.info("[TTS] ip_sound_column 播放已关闭 (ip_sound_column.enabled=false)")
|
||||
return {"command": payload.get("command", ""), "result": -1}
|
||||
|
||||
async with Client(session=zenoh_session, logger=logger) as client:
|
||||
try:
|
||||
result = await client.call(
|
||||
worker_type="ip_sound_column",
|
||||
subject="ip_sound_column.tts",
|
||||
method="POST",
|
||||
payload=payload,
|
||||
timeout=10.0,
|
||||
retries=1,
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"[TTS] ip_sound_column 调用失败: {type(e).__name__}: {e}")
|
||||
return {"command": payload.get("command", ""), "result": -1}
|
||||
|
||||
# ============================================================
|
||||
# 辅助函数 — 告警处理各步骤拆分为独立函数
|
||||
@@ -463,10 +498,29 @@ async def alarm_report(
|
||||
Result=AlarmReportResult(Code=0, Desc="Doubao 分析判定为 False,忽略告警")
|
||||
)
|
||||
|
||||
# 2. 写入本地数据库(按 AlarmId 去重 + 裁剪旧记录)
|
||||
from .global_var import ip_sound_column_volume
|
||||
|
||||
# 2. ip 音柱 TTS 播放(可通过配置开关关闭),失败时仅记录日志,不影响主流程
|
||||
tts_payload = {
|
||||
"command": "play_local_tts",
|
||||
"volume": ip_sound_column_volume,
|
||||
"play_count": 1,
|
||||
"queue": True,
|
||||
"sync_response": False,
|
||||
"play_interval": 2,
|
||||
"tts_pitch": 50,
|
||||
"tts_speed": 50,
|
||||
"tts_voice_name": "xiaoyan",
|
||||
"tts_text": f"告警: {full_data.get('Summary', '')}",
|
||||
}
|
||||
tts_result = await ip_sound_column_tts_play(request.app.state.zenoh_session, tts_payload)
|
||||
if tts_result.get("result") != 0:
|
||||
logger.warning(f"[AlarmReport] ip_sound_column TTS 播放失败: {tts_result}")
|
||||
|
||||
# 3. 写入本地数据库(按 AlarmId 去重 + 裁剪旧记录)
|
||||
await _save_alarm_record(full_data)
|
||||
|
||||
# 3. 转发外部平台(先上传视频,再上报)
|
||||
# 4. 转发外部平台(先上传视频,再上报)
|
||||
await _forward_alarm_to_external(full_data)
|
||||
|
||||
return AlarmReportResponse(
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ class DoubaoPlugin(Plugin):
|
||||
client = AsyncOpenAI(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
http_client=AsyncSecureHttpClient(aicc_config=aicc_config),
|
||||
http_client=AsyncSecureHttpClient(aicc_config=aicc_config), # type: ignore
|
||||
)
|
||||
set_aicc_client(client)
|
||||
logger.info("豆包 AICC 客户端已创建")
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Ip_sound_column Worker 插件
|
||||
|
||||
统一风格模板 — 基于 chongming_worker.plugin.Plugin 基类。
|
||||
|
||||
工作流:
|
||||
1. 编辑 config/workers/ip_sound_column/*.toml 定义 RPC 端点
|
||||
2. uv run gen_models config/workers/ip_sound_column src/plugins/ip_sound_column
|
||||
3. 在 router/<操作>.py 中实现业务逻辑
|
||||
4. 在 on_init / on_cleanup 中初始化 / 清理资源
|
||||
"""
|
||||
import logging
|
||||
|
||||
from chongming_worker.plugin import Plugin
|
||||
|
||||
from . import router # noqa: F401 触发 @rpc_method 注册
|
||||
# from .routes import router # 可选: 自定义 FastAPI 路由(网关自动发现)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IpSoundColumnPlugin(Plugin):
|
||||
"""ip_sound_column 插件 — 统一生命周期。"""
|
||||
|
||||
type = "ip_sound_column"
|
||||
db_name = "ip_sound_column.db" # 自动在 data_dir 下建 SQLite 库(不需要则置 None)
|
||||
auto_bind = True # 首次初始化时自动 auto_bind_models(self.type)
|
||||
|
||||
async def on_init(self):
|
||||
"""首次 create_workers 时调用一次:初始化资源、启动后台任务。
|
||||
|
||||
数据库已由基类 _bootstrap 自动初始化(self.db)。
|
||||
"""
|
||||
config = self.config
|
||||
logger.info(
|
||||
"[ip_sound_column] 初始化开始 (count=%s, ip=%s)",
|
||||
config.get("count", 1),
|
||||
config.get("ip", "unknown")
|
||||
)
|
||||
ip = config.get("ip", "unknown")
|
||||
self.set_var("ip", ip)
|
||||
|
||||
logger.info("[ip_sound_column] 初始化完成")
|
||||
|
||||
async def on_cleanup(self):
|
||||
"""cleanup 时调用:关闭自定义客户端等(后台任务与数据库由基类处理)。"""
|
||||
logger.info("[ip_sound_column] 清理完成")
|
||||
|
||||
|
||||
# 暴露给 app lifespan 的模块级入口(bound method,签名与旧写法完全一致)
|
||||
_plugin = IpSoundColumnPlugin()
|
||||
create_workers = _plugin.create_workers
|
||||
cleanup = _plugin.cleanup
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Ip_sound_column 插件全局运行时状态 — 统一容器
|
||||
|
||||
约定:
|
||||
- 模块只做"声明默认值",不产生任何导入期副作用
|
||||
- 所有值统一在 Plugin.on_init 中通过 set_global_var 显式写入
|
||||
- set_global_var 仅允许写入 _ALLOWED 白名单,拼写错误会立即抛错
|
||||
- require() 用于取"必须已初始化"的值,未初始化时抛清晰错误
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from chongming_database import Database
|
||||
|
||||
# 允许写入的全局变量白名单(防止拼写错误静默创建无用的新变量)
|
||||
_ALLOWED = frozenset({"ip"})
|
||||
|
||||
# 显式声明所有运行时状态(默认 None,运行时由 on_init 写入)
|
||||
# db: Database | None = None
|
||||
|
||||
|
||||
def set_global_var(name: str, value: Any) -> None:
|
||||
"""白名单校验版 setter — 设置模块级运行时状态。"""
|
||||
if name not in _ALLOWED:
|
||||
raise KeyError(
|
||||
f"global_var: 未知变量 '{name}'(允许: {sorted(_ALLOWED)})"
|
||||
)
|
||||
globals()[name] = value
|
||||
|
||||
|
||||
def require(name: str) -> Any:
|
||||
"""获取必须已初始化的全局变量,未初始化时抛清晰错误。"""
|
||||
if name not in _ALLOWED:
|
||||
raise KeyError(f"global_var: 未知变量 '{name}'(允许: {sorted(_ALLOWED)})")
|
||||
value = globals().get(name)
|
||||
if value is None:
|
||||
raise RuntimeError(
|
||||
f"global_var: '{name}' 未初始化 —— 请确认插件 create_workers 已执行且配置正确"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
__all__ = sorted(_ALLOWED) + ["set_global_var", "require"]
|
||||
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
模型导出 — ip_sound_column
|
||||
自动生成,请勿手动修改
|
||||
"""
|
||||
|
||||
from .pydantic import IpSoundColumnResponse
|
||||
from .pydantic import TtsPostRequest
|
||||
from .pydantic import TtsPostResponse
|
||||
|
||||
__all__ = [
|
||||
"IpSoundColumnResponse",
|
||||
"TtsPostRequest",
|
||||
"TtsPostResponse",
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
自动生成的 Pydantic 模型 — ip_sound_column
|
||||
来源: config/workers/ip_sound_column/
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class TtsPostRequest(BaseModel):
|
||||
"""ip 音柱 tts 播放 — 请求体"""
|
||||
command: str = Field(...)
|
||||
volume: int = Field(...)
|
||||
play_count: int = Field(...)
|
||||
queue: bool = Field(...)
|
||||
sync_response: bool = Field(...)
|
||||
play_interval: int = Field(...)
|
||||
tts_pitch: int = Field(...)
|
||||
tts_speed: int = Field(...)
|
||||
tts_voice_name: str = Field(...)
|
||||
tts_text: str = Field(...)
|
||||
|
||||
class TtsPostResponse(BaseModel):
|
||||
"""ip 音柱 tts 播放 — 响应体"""
|
||||
command: str = Field(...)
|
||||
result: int = Field(...)
|
||||
|
||||
|
||||
# 所有响应模型的联合类型
|
||||
IpSoundColumnResponse = TtsPostResponse
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Ip_sound_column RPC 端点"""
|
||||
|
||||
from . import tts
|
||||
|
||||
__all__ = ['tts']
|
||||
@@ -0,0 +1,41 @@
|
||||
"""RPC 端点: ip_sound_column.ping
|
||||
|
||||
模型由 gen_models 生成(编辑 config/workers/ip_sound_column/ping.toml 后重新生成)。
|
||||
"""
|
||||
import httpx
|
||||
|
||||
from chongming_worker.decorators import rpc_method
|
||||
|
||||
from ..models.pydantic import TtsPostRequest, TtsPostResponse
|
||||
from ..global_var import require
|
||||
|
||||
|
||||
@rpc_method(
|
||||
subject="ip_sound_column.tts",
|
||||
method="POST",
|
||||
summary="tts 操作",
|
||||
response_model=TtsPostResponse,
|
||||
)
|
||||
async def tts(request: TtsPostRequest) -> TtsPostResponse:
|
||||
"""业务逻辑实现。"""
|
||||
|
||||
ip_addr: str = require("ip")
|
||||
if ip_addr == "unknown":
|
||||
raise RuntimeError("ip_sound_column: 未初始化 ip 地址,请检查配置")
|
||||
|
||||
tts_url = f"http://{ip_addr}/v1/api"
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
tts_url,
|
||||
json=request.model_dump(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
if resp.is_success:
|
||||
return TtsPostResponse(**resp.json())
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"ip_sound_column: tts 请求失败,状态码 {resp.status_code},响应内容: {resp.text}"
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
raise RuntimeError(f"ip_sound_column: tts 请求异常: {str(e)}") from e
|
||||
Reference in New Issue
Block a user