@tencent-rtc/trtc-agent-skills 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/CODEBUDDY.md +1 -1
  4. package/README.md +10 -7
  5. package/README.zh.md +10 -7
  6. package/bin/cli.js +196 -44
  7. package/knowledge-base/conference/web/index.yaml +6 -6
  8. package/knowledge-base/slices/conference/web/official-roomkit-api.md +119 -8
  9. package/package.json +1 -1
  10. package/skills/trtc/SKILL.md +45 -10
  11. package/skills/trtc-ai-oral-coach/README.ja.md +3 -3
  12. package/skills/trtc-ai-oral-coach/README.md +3 -3
  13. package/skills/trtc-ai-oral-coach/README.zh-CN.md +3 -3
  14. package/skills/trtc-ai-oral-coach/SKILL.md +7 -4
  15. package/skills/trtc-ai-realtime-interpreter/README.ja.md +197 -0
  16. package/skills/trtc-ai-realtime-interpreter/README.md +197 -0
  17. package/skills/trtc-ai-realtime-interpreter/README.zh-CN.md +197 -0
  18. package/skills/trtc-ai-realtime-interpreter/SKILL.md +748 -0
  19. package/skills/trtc-ai-realtime-interpreter/auto_adapters/integration_templates/generic-rest-api.md +85 -0
  20. package/skills/trtc-ai-realtime-interpreter/auto_adapters/integration_templates/room-owner-authz-note.md +53 -0
  21. package/skills/trtc-ai-realtime-interpreter/auto_adapters/manifest.yaml +49 -0
  22. package/skills/trtc-ai-realtime-interpreter/auto_adapters/python/README.md +11 -0
  23. package/skills/trtc-ai-realtime-interpreter/auto_adapters/python/fastapi_reverse_proxy.py.tpl +84 -0
  24. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/.env.example +17 -0
  25. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/frontend/silent-listener.ts +91 -0
  26. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/manifest.yaml +110 -0
  27. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/requirements.txt +5 -0
  28. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/__init__.py +0 -0
  29. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/_capability_loader.py +89 -0
  30. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/agent.py +153 -0
  31. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/credentials.py +112 -0
  32. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/health.py +218 -0
  33. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/log_filter.py +33 -0
  34. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/server.py +266 -0
  35. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/trtc_client.py +188 -0
  36. package/skills/trtc-ai-realtime-interpreter/capabilities/conversation-core/src/usersig.py +53 -0
  37. package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/README.md +46 -0
  38. package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/manifest.yaml +63 -0
  39. package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/src/__init__.py +0 -0
  40. package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/src/fanout.py +149 -0
  41. package/skills/trtc-ai-realtime-interpreter/capabilities/meeting-ops/src/router.py +83 -0
  42. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/frontend/subtitle-parser.ts +145 -0
  43. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/manifest.yaml +68 -0
  44. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/__init__.py +0 -0
  45. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/modes.py +73 -0
  46. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/router.py +83 -0
  47. package/skills/trtc-ai-realtime-interpreter/capabilities/realtime-translation/src/service.py +77 -0
  48. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/README.md +23 -0
  49. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/app/__init__.py +0 -0
  50. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/app/server.py +277 -0
  51. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/requirements.txt +5 -0
  52. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/backend/start.sh +28 -0
  53. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/recipe.yaml +72 -0
  54. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/index.html +12 -0
  55. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/legacy/index.html +738 -0
  56. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/package-lock.json +4302 -0
  57. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/package.json +33 -0
  58. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/postcss.config.js +6 -0
  59. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/App.vue +18 -0
  60. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/api/backend.ts +82 -0
  61. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/SetupScreen.vue +362 -0
  62. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/SummaryScreen.vue +203 -0
  63. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/ChatPanel.vue +188 -0
  64. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/ConferenceRoom.vue +453 -0
  65. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/ParticipantViewUI.vue +170 -0
  66. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/PeoplePanel.vue +206 -0
  67. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/SidePanel.vue +77 -0
  68. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/Toolbar.vue +371 -0
  69. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/TopBar.vue +310 -0
  70. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/components/conference/TranscriptPanel.vue +225 -0
  71. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/composables/useAiInterpreter.ts +263 -0
  72. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/composables/useConference.ts +98 -0
  73. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/env.d.ts +7 -0
  74. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/main.ts +5 -0
  75. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/store.ts +103 -0
  76. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/src/style.css +19 -0
  77. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/tailwind.config.js +25 -0
  78. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/tsconfig.json +25 -0
  79. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/tsconfig.node.json +10 -0
  80. package/skills/trtc-ai-realtime-interpreter/scenarios/meeting-interpreter/ui/vite.config.ts +26 -0
  81. package/skills/trtc-ai-realtime-interpreter/scripts/add-capability.py +163 -0
  82. package/skills/trtc-ai-realtime-interpreter/scripts/deploy-demo.sh +64 -0
  83. package/skills/trtc-ai-realtime-interpreter/scripts/lib/__init__.py +0 -0
  84. package/skills/trtc-ai-realtime-interpreter/scripts/lib/credential_validators.py +143 -0
  85. package/skills/trtc-ai-realtime-interpreter/scripts/lib/manifest_resolver.py +60 -0
  86. package/skills/trtc-ai-realtime-interpreter/scripts/lib/stack_detector.py +50 -0
  87. package/skills/trtc-ai-realtime-interpreter/scripts/post-install-patch.py +79 -0
  88. package/skills/trtc-ai-realtime-interpreter/scripts/verify-credentials.py +76 -0
  89. package/skills/trtc-ai-realtime-interpreter/start.sh +85 -0
  90. package/skills/trtc-ai-realtime-interpreter/triggers.yaml +29 -0
  91. package/skills/trtc-ai-service/README.ja.md +3 -3
  92. package/skills/trtc-ai-service/README.md +3 -3
  93. package/skills/trtc-ai-service/README.zh-CN.md +3 -3
  94. package/skills/trtc-ai-service/SKILL.md +9 -7
  95. package/skills/trtc-ai-service/capabilities/conversation-core/src/credentials.py +1 -1
  96. package/skills/trtc-chat/SKILL.md +1 -1
  97. package/skills/trtc-chat/docs/SKILL.md +1 -1
  98. package/skills/trtc-conference/flows/onboarding.md +6 -0
  99. package/skills/trtc-conference/flows/topic.md +6 -2
  100. package/skills/trtc-conference/playbooks/official-roomkit.md +3 -1
  101. package/skills/trtc-conference/tests/test_conference_index_contract.py +16 -0
  102. package/skills/trtc-conference/tests/test_conference_onboarding_contract.py +9 -0
  103. package/skills/trtc-conference/tests/test_conference_topic_flow_contract.py +16 -0
  104. package/skills/trtc-push/SKILL.md +118 -0
  105. package/skills/trtc-push/issues/ROUTER.json +429 -0
  106. package/skills/trtc-push/issues/cards/android/fcm-gms-domestic.md +53 -0
  107. package/skills/trtc-push/issues/cards/android/vendor-huawei.md +72 -0
  108. package/skills/trtc-push/issues/cards/common/console-certificate-quota.md +46 -0
  109. package/skills/trtc-push/issues/cards/common/registration-binding.md +67 -0
  110. package/skills/trtc-push/issues/cards/ios/aps-environment-3000.md +55 -0
  111. package/skills/trtc-push/issues/cards/ios/certificate-businessid.md +56 -0
  112. package/skills/trtc-push/issues/cards/ios/xcodegen-cocoapods-module.md +54 -0
  113. package/skills/trtc-push/issues/flows/android/delivered-not-displayed.md +52 -0
  114. package/skills/trtc-push/issues/flows/android/vendor-not-received.md +57 -0
  115. package/skills/trtc-push/issues/flows/common/badge.md +49 -0
  116. package/skills/trtc-push/issues/flows/common/console-product-limits.md +48 -0
  117. package/skills/trtc-push/issues/flows/common/server-api.md +51 -0
  118. package/skills/trtc-push/issues/flows/cross-platform/harmonyos.md +51 -0
  119. package/skills/trtc-push/issues/flows/cross-platform/uniapp-integration.md +57 -0
  120. package/skills/trtc-push/issues/flows/ios/offline-not-received.md +57 -0
  121. package/skills/trtc-push/references/code-templates.md +386 -0
  122. package/skills/trtc-push/references/hard-rules.md +112 -0
  123. package/skills/trtc-push/references/timpush-sdk-api.md +72 -0
  124. package/skills/trtc-push/references/workflow-protocol.md +82 -0
  125. /package/.cursor/rules/{main.mdc → ui-mode.mdc} +0 -0
@@ -0,0 +1,149 @@
1
+ # -*- coding: utf-8 -*-
2
+ """扇出编排器:给定一组 targetUserId,批量起停 conversation-core 会话 + 维护房间级状态。
3
+
4
+ 职责边界(务必保持纯粹):
5
+ - 只知道"批量起停会话 + 记住这个房间当前跑的是什么",不知道"翻译""客服""会议纪要"
6
+ 这些具体业务是什么 —— 具体业务能力包通过 `capability` 参数指定,本模块用
7
+ conversation-core 的 _capability_loader 动态加载该能力包的 `build_lifecycle_config`
8
+ 约定函数(每个业务能力包都应该在自己的 service.py 里实现这个函数签名,参见
9
+ realtime-translation/src/service.py 的 build_lifecycle_config)。
10
+ - **完全不做权限校验**。"谁能调用扇出接口"是集成方自己业务系统的职责——本能力包
11
+ 只是一个特权编排原语,调用方(比如 Path A 场景层的会议室 demo,或 Path B 集成方
12
+ 自己的后端)必须在转发到这里之前,自行完成"调用者是否有权限触发"的校验。
13
+ 参见 manifest.yaml 的安全声明与 README。
14
+
15
+ 状态存储:内存字典,demo/中小规模场景够用;生产场景如需持久化,由集成方自己接管
16
+ (比如换成 Redis),本能力包不内置持久化适配。
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import sys
22
+ from pathlib import Path
23
+ from threading import RLock
24
+ from typing import Any, Dict, List, Optional
25
+
26
+ _CORE_SRC = Path(__file__).resolve().parents[2] / "conversation-core" / "src"
27
+ if str(_CORE_SRC) not in sys.path:
28
+ sys.path.insert(0, str(_CORE_SRC))
29
+
30
+ from agent import ConversationAgent # noqa: E402
31
+ from _capability_loader import load_capability # noqa: E402
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class FanoutOrchestrator:
37
+ """房间级扇出编排器(内存态,单进程内单例即可)。"""
38
+
39
+ def __init__(self) -> None:
40
+ self._rooms: Dict[str, Dict[str, Any]] = {}
41
+ self._lock = RLock()
42
+
43
+ # ------------------------------------------------------------------
44
+ def start(
45
+ self,
46
+ agent: ConversationAgent,
47
+ room_id: str,
48
+ participants: List[str],
49
+ capability: str,
50
+ params: Dict[str, Any],
51
+ room_id_type: int = 1,
52
+ agent_user_id_prefix: str = "ai",
53
+ ) -> Dict[str, Any]:
54
+ """按参会人列表批量起会话。已有活跃扇出则先全部停掉再重开(幂等重启)。
55
+
56
+ capability: 业务能力包目录名(如 "realtime-translation"),必须实现
57
+ `src/service.py` 里的 `build_lifecycle_config(params) -> AgentLifecycleConfig`。
58
+ params: 原样传给该能力包的 build_lifecycle_config,本模块不关心内容。
59
+ """
60
+ existing = self._rooms.get(room_id)
61
+ if existing:
62
+ self._stop_tasks(agent, existing.get("tasks", []))
63
+
64
+ service_mod = load_capability(capability, "src/service.py")
65
+ if not hasattr(service_mod, "build_lifecycle_config"):
66
+ raise ValueError(f"capability '{capability}' does not implement build_lifecycle_config(params)")
67
+
68
+ tasks: List[Dict[str, str]] = []
69
+ errors: List[Dict[str, str]] = []
70
+ for idx, target_user_id in enumerate(participants):
71
+ # 仅第一路播报欢迎语,避免多路 TTS 欢迎语在同一房间叠放
72
+ local_params = dict(params)
73
+ local_params.setdefault("suppress_welcome", idx != 0)
74
+ cfg = service_mod.build_lifecycle_config(local_params)
75
+ agent_uid = f"{agent_user_id_prefix}_{target_user_id[:16]}"[:32]
76
+ try:
77
+ info = agent.start(
78
+ room_id=room_id,
79
+ target_user_id=target_user_id,
80
+ config=cfg,
81
+ room_id_type=room_id_type,
82
+ agent_user_id=agent_uid,
83
+ )
84
+ tasks.append({
85
+ "session_id": info.session_id,
86
+ "task_id": info.task_id or "",
87
+ "agent_user_id": info.agent_user_id,
88
+ "target_user_id": target_user_id,
89
+ })
90
+ except Exception as exc: # noqa: BLE001
91
+ logger.error("fan-out start failed for target=%s: %s", target_user_id, exc)
92
+ errors.append({"target_user_id": target_user_id, "error": str(exc)})
93
+
94
+ # 全部失败时抛异常,让调用方(scenario backend)返回错误响应给前端
95
+ if not tasks and errors:
96
+ error_details = "; ".join(f"{e['target_user_id']}: {e['error']}" for e in errors)
97
+ raise RuntimeError(f"所有翻译会话启动均失败({len(errors)}路全败): {error_details}")
98
+
99
+ with self._lock:
100
+ self._rooms[room_id] = {"capability": capability, "params": params, "tasks": tasks}
101
+ logger.info("fanout started room=%s capability=%s tasks=%d", room_id, capability, len(tasks))
102
+ return {
103
+ "active": True,
104
+ "capability": capability,
105
+ "params": params,
106
+ "bots": [{"agentUserId": t["agent_user_id"], "targetUserId": t["target_user_id"]} for t in tasks],
107
+ }
108
+
109
+ # ------------------------------------------------------------------
110
+ def stop(self, agent: ConversationAgent, room_id: str) -> int:
111
+ with self._lock:
112
+ room = self._rooms.pop(room_id, None)
113
+ if not room:
114
+ return 0
115
+ return self._stop_tasks(agent, room.get("tasks", []))
116
+
117
+ def _stop_tasks(self, agent: ConversationAgent, tasks: List[Dict[str, str]]) -> int:
118
+ stopped = 0
119
+ for t in tasks:
120
+ try:
121
+ agent.stop(t["session_id"])
122
+ stopped += 1
123
+ except Exception as exc: # noqa: BLE001
124
+ logger.warning("stop session %s failed: %s", t.get("session_id"), exc)
125
+ return stopped
126
+
127
+ # ------------------------------------------------------------------
128
+ def state(self, room_id: str) -> Dict[str, Any]:
129
+ with self._lock:
130
+ room = self._rooms.get(room_id)
131
+ if not room:
132
+ return {"active": False, "capability": None, "params": {}, "bots": []}
133
+ return {
134
+ "active": True,
135
+ "capability": room["capability"],
136
+ "params": room["params"],
137
+ "bots": [{"agentUserId": t["agent_user_id"], "targetUserId": t["target_user_id"]} for t in room.get("tasks", [])],
138
+ }
139
+
140
+
141
+ # 单进程内共享单例(跟 conversation-core 的 Agent 单例模式一致)
142
+ _orchestrator: Optional[FanoutOrchestrator] = None
143
+
144
+
145
+ def get_orchestrator() -> FanoutOrchestrator:
146
+ global _orchestrator
147
+ if _orchestrator is None:
148
+ _orchestrator = FanoutOrchestrator()
149
+ return _orchestrator
@@ -0,0 +1,83 @@
1
+ # -*- coding: utf-8 -*-
2
+ """meeting-ops 的 FastAPI 子路由:扇出编排的 HTTP 接口。
3
+
4
+ 挂载于 conversation-core 的 /api/v1/meeting 前缀下(见 conversation-core/src/server.py)。
5
+
6
+ 安全提示(务必阅读 README/manifest 的安全声明):
7
+ 这些接口是**特权操作端点**,本能力包不做任何调用者身份/权限校验。
8
+ 集成方必须在自己的后端完成"调用者是否有权触发"的校验后,才转发请求到这里
9
+ (例如:先校验 JWT/Session 里的角色是房主/管理员,再调用 /api/v1/meeting/session/start)。
10
+ Path A 的会议室 demo 场景里,这层校验由 scenarios/meeting-interpreter 的 demo 专属
11
+ glue 代码实现(内存字典房主登记),不属于本能力包。
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ from fastapi import APIRouter, HTTPException, Query, Request
20
+ from pydantic import BaseModel
21
+
22
+ _SRC_DIR = Path(__file__).resolve().parent
23
+ if str(_SRC_DIR) not in sys.path:
24
+ sys.path.insert(0, str(_SRC_DIR))
25
+
26
+ from fanout import get_orchestrator # noqa: E402
27
+
28
+ router = APIRouter()
29
+
30
+
31
+ def _require_agent(request: Request):
32
+ agent = getattr(request.app.state, "conversation_agent", None)
33
+ if agent is None:
34
+ raise HTTPException(status_code=503, detail={"code": "credentials_missing", "message": "conversation-core not initialized"})
35
+ return agent
36
+
37
+
38
+ class SessionStartRequest(BaseModel):
39
+ room_id: str
40
+ room_id_type: int = 1
41
+ participants: List[str]
42
+ capability: str = "realtime-translation"
43
+ params: Dict[str, Any] = {}
44
+
45
+
46
+ class SessionStopRequest(BaseModel):
47
+ room_id: str
48
+
49
+
50
+ @router.post("/session/start")
51
+ def session_start(req: SessionStartRequest, request: Request) -> Dict[str, Any]:
52
+ agent = _require_agent(request)
53
+ orchestrator = get_orchestrator()
54
+ if not req.participants:
55
+ raise HTTPException(status_code=400, detail="participants must not be empty")
56
+ try:
57
+ result = orchestrator.start(
58
+ agent=agent,
59
+ room_id=req.room_id,
60
+ participants=req.participants,
61
+ capability=req.capability,
62
+ params=req.params,
63
+ room_id_type=req.room_id_type,
64
+ )
65
+ except ValueError as exc:
66
+ raise HTTPException(status_code=400, detail=str(exc))
67
+ except Exception as exc: # noqa: BLE001
68
+ raise HTTPException(status_code=500, detail=str(exc))
69
+ return {"code": 0, "msg": "success", "data": result}
70
+
71
+
72
+ @router.post("/session/stop")
73
+ def session_stop(req: SessionStopRequest, request: Request) -> Dict[str, Any]:
74
+ agent = _require_agent(request)
75
+ orchestrator = get_orchestrator()
76
+ stopped = orchestrator.stop(agent, req.room_id)
77
+ return {"code": 0, "msg": "success", "data": {"stopped": stopped}}
78
+
79
+
80
+ @router.get("/session/state")
81
+ def session_state(room_id: str = Query(...)) -> Dict[str, Any]:
82
+ orchestrator = get_orchestrator()
83
+ return {"code": 0, "data": orchestrator.state(room_id)}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * 实时翻译字幕/状态自定义消息解析器(框架无关,不依赖任何 UI 库)。
3
+ *
4
+ * TRTC Conversational AI 通过 CUSTOM_MESSAGE 广播两类消息:
5
+ * - cmd 10000:字幕(可能是某个真人的 ASR 原文,也可能是 AI 的译文/欢迎语)
6
+ * - cmd 10001:AI 状态(1=本轮结束 / 2=思考中(翻译中) / 3=正在播报)
7
+ *
8
+ * sender 归属判断:消息载荷里的 `data.sender` 是"逻辑发送者"(真人的 userId 或
9
+ * bot 的 userId),不是 TRTC 传输层的 event.userId(后者在单目标模式下也可能不是
10
+ * 真正的说话人)。命中 botUserIds 集合 -> 认为是 AI 译文;否则是某个真人的原文。
11
+ *
12
+ * 使用方式:
13
+ * const parser = createSubtitleParser({
14
+ * isBot: (userId) => botUserIds.has(userId),
15
+ * resolveSpeakerName: (userId) => participantList.find(...)?.nameCard || userId,
16
+ * onBubbleUpdate: (bubble) => { ... }, // 当前气泡(说话人/原文/译文)更新
17
+ * onArchive: (item) => { ... }, // 一轮结束,归档一条完整转写记录
18
+ * onAgentState: (state) => { ... }, // idle/listen/translate/speak
19
+ * })
20
+ * silentListener.onCustomMessage((event) => parser.handle(event))
21
+ */
22
+
23
+ export type AgentState = 'idle' | 'listen' | 'translate' | 'speak'
24
+
25
+ export interface TranscriptItem {
26
+ speaker: string
27
+ orig: string
28
+ trans: string
29
+ time: string
30
+ }
31
+
32
+ export interface CurrentBubble {
33
+ speaker: string
34
+ orig: string
35
+ trans: string
36
+ }
37
+
38
+ export interface SubtitleParserOptions {
39
+ /** 判断某个 userId 是否为 AI bot 身份 */
40
+ isBot: (userId: string) => boolean
41
+ /** 把真人 userId 解析成展示名(昵称),找不到则原样返回 userId */
42
+ resolveSpeakerName: (userId: string) => string
43
+ /** 计时起点距今的 mm:ss 格式化(调用方可传入自己的计时逻辑),默认从第一次调用起计时 */
44
+ nowTime?: () => string
45
+ onBubbleUpdate?: (bubble: CurrentBubble) => void
46
+ onArchive?: (item: TranscriptItem) => void
47
+ onAgentState?: (state: AgentState) => void
48
+ }
49
+
50
+ export interface SubtitleParserHandle {
51
+ handle: (event: any) => void
52
+ reset: () => void
53
+ getTranscript: () => TranscriptItem[]
54
+ getCurrentBubble: () => CurrentBubble
55
+ }
56
+
57
+ function defaultNowTime(startRef: { ts: number }) {
58
+ return () => {
59
+ if (!startRef.ts) startRef.ts = Date.now()
60
+ const t = Math.floor((Date.now() - startRef.ts) / 1000)
61
+ const m = String(Math.floor(t / 60)).padStart(2, '0')
62
+ const s = String(t % 60).padStart(2, '0')
63
+ return `${m}:${s}`
64
+ }
65
+ }
66
+
67
+ export function createSubtitleParser(opts: SubtitleParserOptions): SubtitleParserHandle {
68
+ const transcript: TranscriptItem[] = []
69
+ let curSpeaker = ''
70
+ let curOrig = ''
71
+ let curTrans = ''
72
+ const startRef = { ts: 0 }
73
+ const nowTime = opts.nowTime || defaultNowTime(startRef)
74
+
75
+ function emitBubble() {
76
+ opts.onBubbleUpdate?.({ speaker: curSpeaker, orig: curOrig, trans: curTrans })
77
+ }
78
+
79
+ function handle(event: any): void {
80
+ let text: string
81
+ try {
82
+ text = new TextDecoder().decode(event.data)
83
+ } catch {
84
+ return
85
+ }
86
+ let data: any
87
+ try {
88
+ data = JSON.parse(text)
89
+ } catch {
90
+ return
91
+ }
92
+ const sender: string = data.sender || event.userId || ''
93
+ const payload = data.payload || {}
94
+
95
+ if (data.type === 10000) {
96
+ const raw = (payload.text || '').trim()
97
+ if (!raw) return
98
+ if (opts.isBot(sender)) {
99
+ if (!curSpeaker) curSpeaker = 'AI Interpreter'
100
+ curTrans = raw
101
+ emitBubble()
102
+ opts.onAgentState?.('speak')
103
+ } else {
104
+ curSpeaker = opts.resolveSpeakerName(sender)
105
+ curOrig = raw
106
+ emitBubble()
107
+ if (payload.end !== true) opts.onAgentState?.('translate')
108
+ }
109
+ } else if (data.type === 10001) {
110
+ const st = payload.state
111
+ if (st === 1) {
112
+ if (curOrig || curTrans) {
113
+ const item: TranscriptItem = { speaker: curSpeaker || 'AI Interpreter', orig: curOrig, trans: curTrans, time: nowTime() }
114
+ transcript.push(item)
115
+ opts.onArchive?.(item)
116
+ }
117
+ curSpeaker = ''
118
+ curOrig = ''
119
+ curTrans = ''
120
+ emitBubble()
121
+ opts.onAgentState?.('listen')
122
+ } else if (st === 2) {
123
+ opts.onAgentState?.('translate')
124
+ } else if (st === 3) {
125
+ opts.onAgentState?.('speak')
126
+ }
127
+ }
128
+ }
129
+
130
+ function reset(): void {
131
+ transcript.length = 0
132
+ curSpeaker = ''
133
+ curOrig = ''
134
+ curTrans = ''
135
+ startRef.ts = 0
136
+ emitBubble()
137
+ }
138
+
139
+ return {
140
+ handle,
141
+ reset,
142
+ getTranscript: () => transcript,
143
+ getCurrentBubble: () => ({ speaker: curSpeaker, orig: curOrig, trans: curTrans }),
144
+ }
145
+ }
@@ -0,0 +1,68 @@
1
+ # realtime-translation 能力自描述 manifest
2
+ # 类型:业务能力(可选安装)——「翻译」这件事本身,跟会议/客服/直播场景无关
3
+
4
+ name: "realtime-translation"
5
+ version: "1.0.0"
6
+ type: "capability"
7
+ description: "语言对驱动的实时翻译逻辑:STT 语种/翻译 system_prompt/TTS 音色选型 + 字幕自定义消息解析。单目标场景可独立使用,多目标扇出场景由 meeting-ops 调用本能力的约定接口。"
8
+
9
+ dependencies:
10
+ - name: "conversation-core"
11
+ version: ">=1.0.0,<2.0.0"
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # 约定接口(供 meeting-ops 等编排能力动态调用,而非直接依赖)
15
+ # ---------------------------------------------------------------------------
16
+ provides:
17
+ starter_capability:
18
+ module: "src/service.py"
19
+ function: "build_lifecycle_config"
20
+ description: >
21
+ 入参 {mode, suppress_welcome?, max_idle_time?} -> 返回 conversation-core 的
22
+ AgentLifecycleConfig。任何"批量起会话"的编排能力(如 meeting-ops)都可以通过
23
+ _capability_loader 动态加载本模块并调用这个函数,而不需要认识"翻译"这个具体业务。
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # 配置
27
+ # ---------------------------------------------------------------------------
28
+ config:
29
+ modes:
30
+ description: "语言对配置(zh-en / zh-yue / en-yue),定义于 src/modes.py"
31
+ extensible: true
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # 对外暴露的 API(单目标场景)
35
+ # ---------------------------------------------------------------------------
36
+ endpoints:
37
+ - method: GET
38
+ path: /api/v1/translation/modes
39
+ description: 列出可用语言对
40
+ - method: POST
41
+ path: /api/v1/translation/start
42
+ description: 给单一目标(主播/客服/任意 user)起一路翻译会话
43
+ - method: POST
44
+ path: /api/v1/translation/stop
45
+ description: 停止该翻译会话
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # 前端资产(框架无关,供 Path A Vue 封装 / Path B 各技术栈适配器复用)
49
+ # ---------------------------------------------------------------------------
50
+ frontend_assets:
51
+ - path: "frontend/subtitle-parser.ts"
52
+ description: "解析 cmd 10000 字幕 / cmd 10001 AI 状态自定义消息,按 sender 归属(bot 集合 vs 真人)拼装双语气泡并归档转写记录。框架无关,不依赖任何 UI 库。"
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # 业务契约:无外部业务系统依赖(纯逻辑 + 复用 conversation-core 的平台级依赖)
56
+ # ---------------------------------------------------------------------------
57
+ business_contract:
58
+ external_apis: []
59
+
60
+ security:
61
+ log_redaction:
62
+ enabled: true
63
+ patterns: ["usersig", "api_key"]
64
+
65
+ acceptance:
66
+ - "build_lifecycle_config 是稳定的约定接口,meeting-ops 只依赖这个函数签名,不感知 modes.py 内部实现"
67
+ - "单目标场景可以只装 conversation-core + realtime-translation,不依赖 meeting-ops"
68
+ - "字幕解析模块不依赖任何前端框架,可被 Vue/React/纯JS 三种 Path A/B 场景复用"
@@ -0,0 +1,73 @@
1
+ # -*- coding: utf-8 -*-
2
+ """实时翻译的语言对配置:STT 识别语种 + 翻译 system_prompt + TTS 音色。
3
+
4
+ 这是「翻译」这件事本身的业务知识,跟会议/客服/直播场景无关——单目标场景
5
+ (比如给一个主播/客服配一个翻译)或多目标扇出场景(会议室)都复用同一套配置。
6
+
7
+ 音色全部来自 TRTC 对话式 AI 内置 TTS 音色库(flow_01_turbo,trtc.io/document/79682),
8
+ 全程闭环在 TRTC 生态内,不引入外部 TTS 服务。
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Dict
13
+
14
+ _WELCOME_DEFAULT = (
15
+ "Hello, I'm your AI interpreter. I'll translate everything you say in real-time. "
16
+ "Please go ahead and speak."
17
+ )
18
+
19
+ MODE_CONFIG: Dict[str, Dict[str, Any]] = {
20
+ "zh-en": {
21
+ "source_label": "Chinese", "target_label": "English",
22
+ "stt_language": "zh",
23
+ "tts_voice": "v-female-p9Xy7Q1L",
24
+ "welcome": _WELCOME_DEFAULT,
25
+ "system_prompt": (
26
+ "You are a professional real-time interpreter. "
27
+ "The user may speak either Mandarin Chinese or English. "
28
+ "Detect the language of each sentence and translate it into the OTHER language "
29
+ "(Chinese -> English, English -> Chinese). "
30
+ "Reply with ONLY the translation — no greetings, no explanations, "
31
+ "no extra commentary, no quotation marks."
32
+ ),
33
+ },
34
+ "zh-yue": {
35
+ "source_label": "Chinese", "target_label": "Cantonese",
36
+ "stt_language": "zh",
37
+ "tts_voice": "v-female-k3P8sL0Q",
38
+ "welcome": _WELCOME_DEFAULT,
39
+ "system_prompt": (
40
+ "You are a professional real-time interpreter. "
41
+ "The user may speak either Mandarin Chinese or Cantonese. "
42
+ "Detect the language of each sentence and translate it into the OTHER language "
43
+ "(Mandarin -> Cantonese written form, Cantonese -> Mandarin). "
44
+ "Reply with ONLY the translation — no greetings, no explanations, "
45
+ "no extra commentary, no quotation marks."
46
+ ),
47
+ },
48
+ "en-yue": {
49
+ "source_label": "English", "target_label": "Cantonese",
50
+ "stt_language": "en",
51
+ "tts_voice": "v-female-k3P8sL0Q",
52
+ "welcome": _WELCOME_DEFAULT,
53
+ "system_prompt": (
54
+ "You are a professional real-time interpreter. "
55
+ "The user may speak either English or Cantonese. "
56
+ "Detect the language of each sentence and translate it into the OTHER language "
57
+ "(English -> Cantonese written form, Cantonese -> English). "
58
+ "Reply with ONLY the translation — no greetings, no explanations, "
59
+ "no extra commentary, no quotation marks."
60
+ ),
61
+ },
62
+ }
63
+
64
+
65
+ def list_modes() -> Dict[str, Dict[str, str]]:
66
+ return {k: {"source_label": v["source_label"], "target_label": v["target_label"]} for k, v in MODE_CONFIG.items()}
67
+
68
+
69
+ def get_mode(mode_id: str) -> Dict[str, Any]:
70
+ mode = MODE_CONFIG.get(mode_id)
71
+ if not mode:
72
+ raise ValueError(f"unknown mode_id: {mode_id!r}, available: {list(MODE_CONFIG.keys())}")
73
+ return mode
@@ -0,0 +1,83 @@
1
+ # -*- coding: utf-8 -*-
2
+ """realtime-translation 的 FastAPI 子路由:单目标翻译场景的 HTTP 接口。
3
+
4
+ 挂载于 conversation-core 的 /api/v1/translation 前缀下(见 conversation-core/src/server.py)。
5
+ 面向"给一个人配一路翻译"的场景(主播/客服/任意单一目标),不涉及会议扇出——
6
+ 多目标扇出场景请用 meeting-ops,它会直接调用 service.py 里的 build_lifecycle_config()
7
+ 约定接口,不走这里的 HTTP 层。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+
15
+ from fastapi import APIRouter, HTTPException, Request
16
+ from pydantic import BaseModel
17
+
18
+ _SRC_DIR = Path(__file__).resolve().parent
19
+ if str(_SRC_DIR) not in sys.path:
20
+ sys.path.insert(0, str(_SRC_DIR))
21
+
22
+ from service import list_modes, start_translation # noqa: E402
23
+
24
+ router = APIRouter()
25
+
26
+
27
+ def _require_agent(request: Request):
28
+ agent = getattr(request.app.state, "conversation_agent", None)
29
+ if agent is None:
30
+ raise HTTPException(status_code=503, detail={"code": "credentials_missing", "message": "conversation-core not initialized"})
31
+ return agent
32
+
33
+
34
+ class TranslationStartRequest(BaseModel):
35
+ room_id: str
36
+ room_id_type: int = 0
37
+ target_user_id: str
38
+ mode: str
39
+ agent_user_id: Optional[str] = None
40
+
41
+
42
+ class TranslationStopRequest(BaseModel):
43
+ session_id: str
44
+
45
+
46
+ @router.get("/modes")
47
+ def get_modes() -> Dict[str, Any]:
48
+ return {"code": 0, "data": list_modes()}
49
+
50
+
51
+ @router.post("/start")
52
+ def translation_start(req: TranslationStartRequest, request: Request) -> Dict[str, Any]:
53
+ agent = _require_agent(request)
54
+ try:
55
+ info = start_translation(
56
+ agent=agent,
57
+ room_id=req.room_id,
58
+ target_user_id=req.target_user_id,
59
+ mode=req.mode,
60
+ room_id_type=req.room_id_type,
61
+ agent_user_id=req.agent_user_id,
62
+ )
63
+ except ValueError as exc:
64
+ raise HTTPException(status_code=400, detail=str(exc))
65
+ except Exception as exc: # noqa: BLE001
66
+ raise HTTPException(status_code=500, detail=str(exc))
67
+ return {
68
+ "code": 0, "msg": "success",
69
+ "data": {
70
+ "session_id": info.session_id, "task_id": info.task_id,
71
+ "agent_user_id": info.agent_user_id, "mode": req.mode, "status": "started",
72
+ },
73
+ }
74
+
75
+
76
+ @router.post("/stop")
77
+ def translation_stop(req: TranslationStopRequest, request: Request) -> Dict[str, Any]:
78
+ agent = _require_agent(request)
79
+ try:
80
+ agent.stop(req.session_id)
81
+ except ValueError as exc:
82
+ raise HTTPException(status_code=400, detail=str(exc))
83
+ return {"code": 0, "msg": "success", "data": {"session_id": req.session_id, "status": "stopped"}}
@@ -0,0 +1,77 @@
1
+ # -*- coding: utf-8 -*-
2
+ """实时翻译服务:把「翻译」这件事翻成 conversation-core 认得的启动参数。
3
+
4
+ 对外提供两类接口:
5
+ 1. `build_lifecycle_config(params)` —— **约定接口**,meeting-ops 的扇出编排器会通过
6
+ _capability_loader 动态加载本模块并调用这个函数,拿到 AgentLifecycleConfig 后自己去调
7
+ conversation-core 起会话。这样 meeting-ops 完全不需要认识"翻译"这个概念,只认识
8
+ "给我一个启动配置"这个约定。
9
+ 2. `start_translation(agent, ...)` —— 单目标场景的直接调用封装(配合 router.py 的
10
+ HTTP 接口),给主播/客服配一个翻译时用,不涉及 meeting-ops。
11
+
12
+ welcome_message 的多路去重规则由调用方(router 或 meeting-ops)决定,本模块只提供
13
+ "第一路播报欢迎语,其余路静音"的可选参数,不擅自假设场景。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import sys
18
+ from pathlib import Path
19
+ from typing import Any, Dict, Optional
20
+
21
+ _CORE_SRC = Path(__file__).resolve().parents[2] / "conversation-core" / "src"
22
+ if str(_CORE_SRC) not in sys.path:
23
+ sys.path.insert(0, str(_CORE_SRC))
24
+
25
+ from agent import ConversationAgent, SessionInfo # noqa: E402
26
+ from trtc_client import AgentLifecycleConfig # noqa: E402
27
+
28
+ from modes import get_mode, list_modes # noqa: E402
29
+
30
+ __all__ = ["build_lifecycle_config", "start_translation", "list_modes"]
31
+
32
+
33
+ def build_lifecycle_config(params: Dict[str, Any]) -> AgentLifecycleConfig:
34
+ """约定接口:把 {mode, suppress_welcome?} 换成 AgentLifecycleConfig。
35
+
36
+ 被 meeting-ops 的扇出编排器动态调用(见 meeting-ops/src/fanout.py 里的
37
+ `starter_capability` 约定),也被本模块自己的 start_translation() 复用。
38
+ """
39
+ mode_id = params.get("mode")
40
+ if not mode_id:
41
+ raise ValueError("params.mode is required")
42
+ mode = get_mode(mode_id)
43
+ suppress_welcome = bool(params.get("suppress_welcome", False))
44
+ tts_enabled = bool(params.get("tts_enabled", True))
45
+ return AgentLifecycleConfig(
46
+ instructions=mode["system_prompt"],
47
+ greeting=mode["welcome"],
48
+ welcome_message="" if suppress_welcome else mode["welcome"],
49
+ language=mode["stt_language"],
50
+ voice_id=mode["tts_voice"],
51
+ max_idle_time=int(params.get("max_idle_time", 60)),
52
+ tts_enabled=tts_enabled,
53
+ )
54
+
55
+
56
+ def start_translation(
57
+ agent: ConversationAgent,
58
+ room_id: str,
59
+ target_user_id: str,
60
+ mode: str,
61
+ room_id_type: int = 0,
62
+ agent_user_id: Optional[str] = None,
63
+ suppress_welcome: bool = False,
64
+ ) -> SessionInfo:
65
+ """单目标翻译场景:给一个人(主播/客服/任意 target_user_id)配一路翻译。
66
+
67
+ 不涉及"参会人列表""房主"等会议概念——这些是 meeting-ops 的职责,本函数
68
+ 只管"起一路翻译会话"这一件事。
69
+ """
70
+ cfg = build_lifecycle_config({"mode": mode, "suppress_welcome": suppress_welcome})
71
+ return agent.start(
72
+ room_id=room_id,
73
+ target_user_id=target_user_id,
74
+ config=cfg,
75
+ room_id_type=room_id_type,
76
+ agent_user_id=agent_user_id,
77
+ )