1313
1414import mimetypes
1515import json
16+ from collections .abc import Mapping
1617mimetypes .add_type ("application/javascript" , ".js" )
1718import asyncio
1819import uuid
@@ -1650,6 +1651,143 @@ async def _emit_agent_status_update(lanlan_name: Optional[str] = None) -> None:
16501651 pass
16511652
16521653
1654+ VOICE_TRANSCRIPT_CUSTOM_EVENT_TYPE = "voice_transcript"
1655+ VOICE_TRANSCRIPT_CUSTOM_EVENT_ID = "handle_transcript"
1656+ VOICE_TRANSCRIPT_CUSTOM_EVENT_TIMEOUT_SECONDS = 1.0
1657+
1658+
1659+ def _voice_bridge_noop (reason : str , ** extra : object ) -> Dict [str , Any ]:
1660+ return {
1661+ "action" : "noop" ,
1662+ "reason" : str (reason or "noop" ),
1663+ ** extra ,
1664+ }
1665+
1666+
1667+ def _voice_transcript_request_has_text (event : Mapping [str , object ] | None ) -> bool :
1668+ if not isinstance (event , Mapping ):
1669+ return False
1670+ return bool (str (event .get ("transcript" ) or "" ).strip ())
1671+
1672+
1673+ def _voice_transcript_custom_event_args (event : Mapping [str , object ]) -> Dict [str , object ]:
1674+ metadata = event .get ("metadata" )
1675+ return {
1676+ "transcript" : str (event .get ("transcript" ) or "" ).strip (),
1677+ "lanlan_name" : str (event .get ("lanlan_name" ) or "" ),
1678+ "metadata" : dict (metadata ) if isinstance (metadata , Mapping ) else {},
1679+ }
1680+
1681+
1682+ def _voice_bridge_action_from_dispatch_results (dispatch_results : object ) -> Dict [str , Any ]:
1683+ if not isinstance (dispatch_results , list ) or not dispatch_results :
1684+ return _voice_bridge_noop ("no_subscribers" )
1685+
1686+ from plugin .server .application .plugins .voice_contracts import (
1687+ arbitrate_voice_transcript_results ,
1688+ )
1689+
1690+ arbitration_items : list [dict [str , object ]] = []
1691+ failure_count = 0
1692+ for item in dispatch_results :
1693+ if not isinstance (item , Mapping ):
1694+ continue
1695+ if not bool (item .get ("success" )):
1696+ failure_count += 1
1697+ continue
1698+ result = item .get ("result" )
1699+ if not isinstance (result , Mapping ):
1700+ continue
1701+ action = str (result .get ("action" ) or "" ).strip ()
1702+ if not action :
1703+ continue
1704+ payload : Dict [str , Any ] = dict (result )
1705+ payload ["action" ] = action
1706+ plugin_id = str (item .get ("plugin_id" ) or "" ).strip ()
1707+ if plugin_id :
1708+ payload .setdefault ("source_plugin" , plugin_id )
1709+ source_event_id = str (item .get ("event_id" ) or "" ).strip ()
1710+ if source_event_id :
1711+ payload .setdefault ("source_event_id" , source_event_id )
1712+ arbitration_items .append (
1713+ {
1714+ "plugin_id" : payload .get ("source_plugin" ) or plugin_id ,
1715+ "event_id" : payload .get ("source_event_id" ) or source_event_id ,
1716+ "success" : True ,
1717+ "result" : payload ,
1718+ }
1719+ )
1720+
1721+ if not arbitration_items :
1722+ return _voice_bridge_noop ("no_handler_result" , failures = failure_count )
1723+ payload = arbitrate_voice_transcript_results (arbitration_items )
1724+ if failure_count :
1725+ try :
1726+ existing_failures = int (payload .get ("failures" ) or 0 )
1727+ except (TypeError , ValueError ):
1728+ existing_failures = 0
1729+ payload ["failures" ] = existing_failures + failure_count
1730+ return payload
1731+
1732+
1733+ async def _dispatch_voice_transcript_custom_event (
1734+ event : Mapping [str , object ],
1735+ ) -> Dict [str , Any ]:
1736+ from plugin .server .application .plugins .dispatch_service import PluginDispatchService
1737+
1738+ dispatch_results = await PluginDispatchService ().trigger_custom_event_subscribers (
1739+ event_type = VOICE_TRANSCRIPT_CUSTOM_EVENT_TYPE ,
1740+ event_id = VOICE_TRANSCRIPT_CUSTOM_EVENT_ID ,
1741+ args = _voice_transcript_custom_event_args (event ),
1742+ timeout = VOICE_TRANSCRIPT_CUSTOM_EVENT_TIMEOUT_SECONDS ,
1743+ )
1744+ return _voice_bridge_action_from_dispatch_results (dispatch_results )
1745+
1746+
1747+ async def _handle_voice_transcript_request (event : Dict [str , Any ]) -> None :
1748+ event_id = str ((event or {}).get ("event_id" ) or "" )
1749+ lanlan_name = (event or {}).get ("lanlan_name" )
1750+ result : Dict [str , Any ] = _voice_bridge_noop ("unavailable" )
1751+
1752+ try :
1753+ if not _voice_transcript_request_has_text (event ):
1754+ result = _voice_bridge_noop ("empty_transcript" )
1755+ elif not Modules .analyzer_enabled :
1756+ result = _voice_bridge_noop ("agent_disabled" )
1757+ elif not Modules .agent_flags .get ("user_plugin_enabled" , False ):
1758+ result = _voice_bridge_noop ("user_plugin_disabled" )
1759+ else :
1760+ lifecycle_ready = bool (Modules .plugin_lifecycle_started )
1761+ if not lifecycle_ready :
1762+ lifecycle_ready = await _ensure_plugin_lifecycle_started ()
1763+
1764+ if not lifecycle_ready :
1765+ result = _voice_bridge_noop ("plugin_lifecycle_start_failed" )
1766+ else :
1767+ result = await _dispatch_voice_transcript_custom_event (event )
1768+ except asyncio .CancelledError :
1769+ raise
1770+ except Exception as exc :
1771+ logger .debug (
1772+ "[VoiceBridge] plugin dispatch failed: event_id=%s lanlan=%s err=%s" ,
1773+ event_id ,
1774+ lanlan_name ,
1775+ exc ,
1776+ )
1777+ result = {
1778+ "action" : "noop" ,
1779+ "reason" : "dispatch_failed" ,
1780+ "error_type" : type (exc ).__name__ ,
1781+ }
1782+
1783+ await _emit_main_event (
1784+ "voice_bridge_result" ,
1785+ lanlan_name if isinstance (lanlan_name , str ) else None ,
1786+ event_id = event_id ,
1787+ result = result ,
1788+ )
1789+
1790+
16531791async def _on_session_event (event : Dict [str , Any ]) -> None :
16541792 event_type = (event or {}).get ("event_type" )
16551793 if event_type == "agent_intent_restore_signal" :
@@ -1662,6 +1800,20 @@ async def _on_session_event(event: Dict[str, Any]) -> None:
16621800 # has its own once-flag, so this is safe to spam.
16631801 await _maybe_restore_agent_intent ()
16641802 return
1803+ if event_type == "voice_transcript_request" :
1804+ seen_task = asyncio .create_task (
1805+ _emit_main_event (
1806+ "voice_bridge_request_seen" ,
1807+ event .get ("lanlan_name" ) if isinstance (event .get ("lanlan_name" ), str ) else None ,
1808+ event_id = str (event .get ("event_id" ) or "" ),
1809+ )
1810+ )
1811+ Modules ._background_tasks .add (seen_task )
1812+ seen_task .add_done_callback (Modules ._background_tasks .discard )
1813+ task = asyncio .create_task (_handle_voice_transcript_request (event ))
1814+ Modules ._background_tasks .add (task )
1815+ task .add_done_callback (Modules ._background_tasks .discard )
1816+ return
16651817 if event_type == "analyze_request" :
16661818 messages = event .get ("messages" , [])
16671819 lanlan_name = event .get ("lanlan_name" )
0 commit comments