5050# ── Proactive audio prompt cache ──────────────────────────────────────
5151_PROACTIVE_AUDIO_DIR = Path (__file__ ).resolve ().parent .parent / "static" / "proactive_audio"
5252_PROACTIVE_AUDIO_CACHE : Dict [str , bytes ] = {}
53+ _IMAGE_ANALYSIS_PENDING_CONTEXT = (
54+ "[实时屏幕截图或相机画面正在分析中。先不要瞎编内容,可以稍等片刻。"
55+ "在此期间不要用搜索功能应付。等收到画面分析结果后再描述画面。]"
56+ )
5357
5458
5559def _load_proactive_audio (filename : str ) -> bytes :
@@ -261,7 +265,7 @@ def __init__(
261265 self ._image_recognized_this_turn = False
262266 self ._image_sent_this_turn = False
263267 self ._image_being_analyzed = False
264- self ._image_description = "[实时屏幕截图或相机画面正在分析中。先不要瞎编内容,可以稍等片刻。在此期间不要用搜索功能应付。等收到画面分析结果后再描述画面。]"
268+ self ._image_description = _IMAGE_ANALYSIS_PENDING_CONTEXT
265269 self ._latest_image_b64 = None # Cached latest screenshot for proactive injection
266270 self ._proactive_image_consumed = True # Whether the cached image has been used by a proactive nudge
267271 self ._proactive_injecting = False # True while prompt_ephemeral is injecting audio — suppresses mic input
@@ -1268,15 +1272,63 @@ async def _analyze_image_with_vision_model(self, image_b64: str) -> str:
12681272
12691273 except Exception as e :
12701274 logger .error (f"Error analyzing image with vision model: { e } " )
1271- self .image_recognized_this_turn = True
1272- self ._image_being_analyzed = False
1275+ self ._image_recognized_this_turn = True
12731276 self ._image_description = f"[实时屏幕截图或相机画面]: 分析出错: { str (e )} "
12741277 # 检测内容审查错误并发送中文提示到前端(不关闭session)
12751278 error_str = str (e )
12761279 if 'censorship' in error_str :
12771280 if self .on_status_message :
12781281 await self .on_status_message (json .dumps ({"code" : "IMAGE_BLOCKED" }))
12791282 return "图片识别发生严重错误!"
1283+
1284+ async def _stream_image_with_vision_model (self , image_b64 : str ) -> None :
1285+ """Analyze one frame and inject both pending and ready context.
1286+
1287+ Realtime backends without native image input need a text description
1288+ from VISION_MODEL. The previous first-frame fast path only updated
1289+ ``_image_description`` locally, so the realtime model could answer
1290+ before the description ever reached its conversation. Serialize the
1291+ expensive analysis and inject the completed description immediately;
1292+ this does not depend on another frame arriving while the user speaks.
1293+ """
1294+ async with self ._image_lock :
1295+ if self ._image_recognized_this_turn :
1296+ return
1297+
1298+ self ._image_being_analyzed = True
1299+ try :
1300+ pending_event = {
1301+ "type" : "conversation.item.create" ,
1302+ "item" : {
1303+ "type" : "message" ,
1304+ "role" : "user" ,
1305+ "content" : [{
1306+ "type" : "input_text" ,
1307+ "text" : _IMAGE_ANALYSIS_PENDING_CONTEXT ,
1308+ }],
1309+ },
1310+ }
1311+ logger .info ("Sending image analysis pending context." )
1312+ await self .send_event (pending_event )
1313+
1314+ await self ._analyze_image_with_vision_model (image_b64 )
1315+
1316+ ready_event = {
1317+ "type" : "conversation.item.create" ,
1318+ "item" : {
1319+ "type" : "message" ,
1320+ "role" : "user" ,
1321+ "content" : [{
1322+ "type" : "input_text" ,
1323+ "text" : self ._image_description ,
1324+ }],
1325+ },
1326+ }
1327+ logger .info ("Sending image description after recognition." )
1328+ await self .send_event (ready_event )
1329+ self ._image_sent_this_turn = True
1330+ finally :
1331+ self ._image_being_analyzed = False
12801332
12811333 async def stream_image (self , image_b64 : str ) -> None :
12821334 """Stream raw image data to the API."""
@@ -1285,9 +1337,12 @@ async def stream_image(self, image_b64: str) -> None:
12851337 self ._proactive_image_consumed = False
12861338
12871339 try :
1288- # Models without native vision (step, free on lanlan.tech) — first frame triggers VISION_MODEL analysis
1289- if '实时屏幕截图或相机画面正在分析中' in self ._image_description and not self ._supports_native_image :
1290- await self ._analyze_image_with_vision_model (image_b64 )
1340+ # Models without native vision (step, free on lanlan.tech) use
1341+ # VISION_MODEL text context. This path is independent of
1342+ # _audio_in_buffer so a completed analysis is available before
1343+ # the user's next utterance even while the microphone is idle.
1344+ if not self ._supports_native_image :
1345+ await self ._stream_image_with_vision_model (image_b64 )
12911346 return
12921347
12931348 # Rate limiting for native image input (with VAD-based throttling)
@@ -1349,47 +1404,6 @@ async def stream_image(self, image_b64: str) -> None:
13491404 ]
13501405 }
13511406 }
1352- else :
1353- # Model does not support video streaming, use VISION_MODEL to analyze
1354- # Only recognize one image per conversation turn
1355- async with self ._image_lock :
1356- if not self ._image_recognized_this_turn :
1357- if not self ._image_being_analyzed :
1358- self ._image_being_analyzed = True
1359- text_event = {
1360- "type" : "conversation.item.create" ,
1361- "item" : {
1362- "type" : "message" ,
1363- "role" : "user" ,
1364- "content" : [
1365- {
1366- "type" : "input_text" ,
1367- "text" : self ._image_description
1368- }
1369- ]
1370- }
1371- }
1372- logger .info ("Sending image description before recognition." )
1373- await self .send_event (text_event )
1374- await self ._analyze_image_with_vision_model (image_b64 )
1375- elif not self ._image_sent_this_turn :
1376- self ._image_sent_this_turn = True
1377- text_event = {
1378- "type" : "conversation.item.create" ,
1379- "item" : {
1380- "type" : "message" ,
1381- "role" : "user" ,
1382- "content" : [
1383- {
1384- "type" : "input_text" ,
1385- "text" : self ._image_description
1386- }
1387- ]
1388- }
1389- }
1390- logger .info ("Sending image description after recognition." )
1391- await self .send_event (text_event )
1392- return
13931407
13941408 await self .send_event (append_event )
13951409 except Exception as e :
0 commit comments