feat: update device interaction views
All checks were successful
Build and Deploy LTY / build-and-deploy (push) Successful in 11m6s
All checks were successful
Build and Deploy LTY / build-and-deploy (push) Successful in 11m6s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3b7c5c85f5
commit
c70bee7295
@ -47,6 +47,24 @@ from common.swagger_utils import get_standardized_response_schema
|
|||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_subv_segments(segments):
|
||||||
|
"""合并字幕分片:自动检测累积式(每段以前段为前缀)vs 增量式(直接拼接)。
|
||||||
|
segments: 已按 seq 升序排好的 [{seq, text}, ...]
|
||||||
|
"""
|
||||||
|
if not segments:
|
||||||
|
return ''
|
||||||
|
if len(segments) == 1:
|
||||||
|
return segments[0]['text']
|
||||||
|
is_cumulative = all(
|
||||||
|
segments[i]['text'].startswith(segments[i - 1]['text'])
|
||||||
|
for i in range(1, len(segments))
|
||||||
|
)
|
||||||
|
if is_cumulative:
|
||||||
|
return segments[-1]['text']
|
||||||
|
return ''.join(s['text'] for s in segments)
|
||||||
|
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
||||||
# 设备相关视图集
|
# 设备相关视图集
|
||||||
@ -1303,7 +1321,7 @@ class VolcEngineTokenViewSet(viewsets.ViewSet):
|
|||||||
subtitle_items.append(subtitle)
|
subtitle_items.append(subtitle)
|
||||||
logger.info('Subtitle: %s', subtitle)
|
logger.info('Subtitle: %s', subtitle)
|
||||||
|
|
||||||
# === 字幕落库(策略 A:仅在 definite && paragraph 时写入)===
|
# === 字幕落库(策略 B:按 paragraph 累积拼接 + AI 归属锁)===
|
||||||
try:
|
try:
|
||||||
# 主路径:尝试从 webhook 上下文提取 task_id(火山如带)
|
# 主路径:尝试从 webhook 上下文提取 task_id(火山如带)
|
||||||
webhook_task_id = (
|
webhook_task_id = (
|
||||||
@ -1323,58 +1341,88 @@ class VolcEngineTokenViewSet(viewsets.ViewSet):
|
|||||||
if not rtc_bot_id:
|
if not rtc_bot_id:
|
||||||
logger.error('RTC_Voice_Agent Bot 未配置,跳过字幕落库')
|
logger.error('RTC_Voice_Agent Bot 未配置,跳过字幕落库')
|
||||||
else:
|
else:
|
||||||
|
MAX_BUFFER_SEGMENTS = 50 # paragraph 始终不来时的兜底强制 flush
|
||||||
for item in subtitle_items:
|
for item in subtitle_items:
|
||||||
text = (item.get('text') or '').strip()
|
text = item.get('text') or ''
|
||||||
if not text:
|
# 中间识别结果不入库
|
||||||
|
if not item.get('definite'):
|
||||||
continue
|
continue
|
||||||
# 策略 A:只在一句话最终段落时落库
|
if not text.strip():
|
||||||
if not (item.get('definite') and item.get('paragraph')):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
user_id_in_subtitle = item.get('userId') or ''
|
user_id_in_subtitle = item.get('userId') or ''
|
||||||
sequence = item.get('sequence', 0)
|
sequence = item.get('sequence', 0)
|
||||||
|
is_paragraph_end = bool(item.get('paragraph'))
|
||||||
|
|
||||||
# 解析 ParadiseUser 归属
|
# 解析 ParadiseUser 归属
|
||||||
paradise_user_id = None
|
paradise_user_id = None
|
||||||
if user_id_in_subtitle == 'bot01':
|
if user_id_in_subtitle == 'bot01':
|
||||||
# AI 字幕:主路径 task_id 索引 -> 兜底 last_active_user
|
# AI 字幕:先看"当前 AI 回合归属锁"
|
||||||
|
paradise_user_id = cache.get('rtc_current_bot_owner')
|
||||||
|
if not paradise_user_id:
|
||||||
|
# 没锁就用主路径/兜底解析,并加锁
|
||||||
if webhook_task_id:
|
if webhook_task_id:
|
||||||
paradise_user_id = cache.get(f"rtc_task_user:{webhook_task_id}")
|
paradise_user_id = cache.get(f"rtc_task_user:{webhook_task_id}")
|
||||||
if not paradise_user_id:
|
if not paradise_user_id:
|
||||||
paradise_user_id = cache.get('rtc_last_active_user')
|
paradise_user_id = cache.get('rtc_last_active_user')
|
||||||
|
if paradise_user_id:
|
||||||
|
# 续期归属锁,确保整段 AI 回复内一致(无论中间多长)
|
||||||
|
cache.set('rtc_current_bot_owner', str(paradise_user_id), 300)
|
||||||
sender = ChatMessage.SENDER_BOT
|
sender = ChatMessage.SENDER_BOT
|
||||||
elif user_id_in_subtitle.isdigit():
|
elif user_id_in_subtitle.isdigit():
|
||||||
paradise_user_id = user_id_in_subtitle
|
paradise_user_id = user_id_in_subtitle
|
||||||
sender = ChatMessage.SENDER_USER
|
sender = ChatMessage.SENDER_USER
|
||||||
# 用户字幕到达,刷新最近活跃用户(给后续 bot01 字幕兜底)
|
# 用户字幕到达,刷新最近活跃用户(给后续 bot01 兜底)
|
||||||
cache.set('rtc_last_active_user', user_id_in_subtitle, 60)
|
cache.set('rtc_last_active_user', user_id_in_subtitle, 60)
|
||||||
else:
|
else:
|
||||||
logger.warning('字幕 userId %r 无法识别,跳过', user_id_in_subtitle)
|
logger.warning('字幕 userId %r 无法识别,跳过', user_id_in_subtitle)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not paradise_user_id:
|
if not paradise_user_id:
|
||||||
logger.warning('字幕无法归属用户: userId=%s task_id=%s',
|
logger.warning('字幕无法归属用户: userId=%s task_id=%s sequence=%s',
|
||||||
user_id_in_subtitle, webhook_task_id)
|
user_id_in_subtitle, webhook_task_id, sequence)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 防重:同一 (paradise_user_id, sequence) 只写一次
|
# 累积到 buffer
|
||||||
dedup_key = f"rtc_subv_seen:{paradise_user_id}:{sequence}"
|
buffer_key = f"rtc_subv_buffer:{sender}:{paradise_user_id}"
|
||||||
|
buf = cache.get(buffer_key) or []
|
||||||
|
buf.append({'seq': sequence, 'text': text})
|
||||||
|
|
||||||
|
force_flush = len(buf) > MAX_BUFFER_SEGMENTS
|
||||||
|
if force_flush:
|
||||||
|
logger.warning('字幕 buffer 超过 %d 段仍未见 paragraph 边界,强制落库 user=%s sender=%s',
|
||||||
|
MAX_BUFFER_SEGMENTS, paradise_user_id, sender)
|
||||||
|
|
||||||
|
if is_paragraph_end or force_flush:
|
||||||
|
# 防重:同一 (sender, paradise_user_id, last_sequence) 只落一次
|
||||||
|
dedup_key = f"rtc_subv_flushed:{sender}:{paradise_user_id}:{sequence}"
|
||||||
if cache.get(dedup_key):
|
if cache.get(dedup_key):
|
||||||
|
cache.delete(buffer_key)
|
||||||
continue
|
continue
|
||||||
cache.set(dedup_key, 1, 300)
|
cache.set(dedup_key, 1, 300)
|
||||||
|
|
||||||
# 写入 ChatMessage(截断超长,DB 字段上限 2048)
|
# 排序拼接(自适应累积/增量)
|
||||||
|
buf_sorted = sorted(buf, key=lambda x: x.get('seq', 0))
|
||||||
|
full_text = _merge_subv_segments(buf_sorted).strip()
|
||||||
|
|
||||||
|
if full_text:
|
||||||
try:
|
try:
|
||||||
ChatMessage.objects.create(
|
ChatMessage.objects.create(
|
||||||
user_id=int(paradise_user_id),
|
user_id=int(paradise_user_id),
|
||||||
bot_id=rtc_bot_id,
|
bot_id=rtc_bot_id,
|
||||||
message=text[:2048],
|
message=full_text[:2048],
|
||||||
sender=sender,
|
sender=sender,
|
||||||
message_type=ChatMessage.MESSAGE_TYPE_TEXT,
|
message_type=ChatMessage.MESSAGE_TYPE_TEXT,
|
||||||
)
|
)
|
||||||
|
logger.info('字幕落库成功: sender=%s user=%s len=%d segs=%d',
|
||||||
|
sender, paradise_user_id, len(full_text), len(buf_sorted))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('字幕落库失败: %s, sender=%s, text=%r',
|
logger.error('字幕落库失败: %s, sender=%s, text=%r',
|
||||||
e, sender, text[:100])
|
e, sender, full_text[:100])
|
||||||
|
cache.delete(buffer_key)
|
||||||
|
else:
|
||||||
|
# 还在回合中,刷新 buffer TTL
|
||||||
|
cache.set(buffer_key, buf, 300)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('字幕落库流程异常: %s', e)
|
logger.error('字幕落库流程异常: %s', e)
|
||||||
# === 字幕落库结束 ===
|
# === 字幕落库结束 ===
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user