Files
modalDeploy/src/cluster/video_apps/hls_slice_inference.py
康宇佳 253e68166f 合并分支
* PERF 优化prompt
* PERF 优化prompt
* PERF 降低scale倍率
* PERF 提升ffmpeg_slice_media预留核心数,防止资源不足超时
* PERF 部署video_hls_slice_inference到aws容器

---------

Merge request URL: https://g-ldyi2063.coding.net/p/dev/d/modalDeploy/git/merge/4808
Co-authored-by: 康宇佳
2025-06-16 19:09:48 +08:00

780 lines
62 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import modal
from ..video import downloader_image, app, config
with downloader_image.imports():
import os, httpx, json, time, requests
from typing import List
from loguru import logger
from modal import current_function_call_id
from ffmpy import FFmpeg, FFprobe
from fastapi import HTTPException
from httpx import Timeout
from starlette import status
import asyncio
import random
import subprocess
from BowongModalFunctions.utils.SentryUtils import SentryUtils
from BowongModalFunctions.models.media_model import MediaSource
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify
from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
from BowongModalFunctions.utils.TimeUtils import TimeDelta, merge_product_data
from BowongModalFunctions.utils.VideoUtils import FFMPEGSliceOptions, VideoUtils
@app.function(cpu=(0.5, 64), timeout=1800,
max_containers=config.video_downloader_concurrency,
volumes={
config.S3_mount_dir: modal.CloudBucketMount(
bucket_name=config.S3_bucket_name,
secret=modal.Secret.from_name("aws-s3-secret", environment_name=config.modal_environment),
),
},
region="us",
cloud='aws'
)
@modal.concurrent(max_inputs=1)
async def video_hls_slice_inference(media: MediaSource,
google_api_key: str,
product_grid_list: List[str],
product_list: List[any],
start_time: str,
end_time: str,
options: FFMPEGSliceOptions,
sentry_trace: SentryTransactionInfo,
webhook: WebhookNotify = None,
retry_time: int = 3,
scale:float = 0.9
):
logger.info(
f"region {os.environ.get('MODAL_REGION', 'unknown')}, provider {os.environ.get('MODAL_CLOUD_PROVIDER', 'unknown')}")
def calculate_target_dimensions(original_width, original_height, target_height=1280):
"""计算保持宽高比的目标尺寸,高度固定为1280"""
aspect_ratio = original_width / original_height
target_width = int(target_height * aspect_ratio)
# 确保宽度是偶数(H.265编码要求)
if target_width % 2 != 0:
target_width -= 1
return target_width, target_height
def convert_video(input_file, output_file=None):
"""将视频转换为30fps、720p、H.264编码(CRF=16)"""
if not output_file:
# 生成默认输出文件名
base_name, ext = os.path.splitext(input_file)
base_name = base_name.split(os.sep)[-1]
output_file = f"/root/{base_name}_720p_h265.mp4"
try:
# 第一步:获取原始视频宽高
ffprobe = FFprobe(
inputs={input_file: ['-v', 'error', '-select_streams', 'v:0',
'-show_entries', 'stream=width,height',
'-of', 'csv=p=0']}
)
info = ffprobe.run(stdout=subprocess.PIPE, stderr=subprocess.PIPE)[0].decode('utf-8').strip()
width, height = map(int, info.split(','))
# 计算目标尺寸
target_width, target_height = calculate_target_dimensions(width, height, target_height=int(
0.5 * height) if height > 1600 else 1138)
# 第二步:执行视频转换
ff = FFmpeg(
inputs={input_file: None},
outputs={output_file: [
'-vf', f'scale={target_width}:{target_height}',
'-r', '30', # 帧率设置为30fps
'-c:v', 'libx264', # 使用H.264编码
"-b:v", "2500K",
'-crf', '18', # 质量设置(CRF 16)
'-c:a', 'copy', # 音频编码复制
"-fs", "1536MB",
'-y' # 覆盖已存在文件
]}
)
logger.info(f"开始转换视频: {input_file}")
logger.info(f"目标分辨率: {target_width}x{target_height}")
logger.info(f"输出文件: {output_file}")
ff.run()
logger.success("视频转换完成!")
return output_file
except Exception as e:
logger.exception(f"视频转换失败 {e}")
return
def upload(file_path):
with open(file_path, "rb") as video:
video = video.read()
content_length = len(video)
content_type = f"video/{file_path.split('.')[-1]}"
filename = file_path.split("\\")[-1]
if content_type not in ['video/mp4', 'video/mpeg', 'video/mov', 'video/avi', 'video/x-flv', 'video/mpg',
'video/webm', 'video/wmv', 'video/3gpp']:
raise HTTPException(status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE)
logger.info(
f"Uploading name = {filename}, size = {content_length}, type = {content_type} to google file")
with httpx.Client(timeout=1800) as client:
pre_upload_response = client.post(
url=f"https://generativelanguage.googleapis.com/upload/v1beta/files?key={google_api_key}",
headers={
"X-Goog-Upload-Protocol": "resumable",
"X-Goog-Upload-Command": "start",
"X-Goog-Upload-Header-Content-Length": str(content_length),
"X-Goog-Upload-Header-Content-Type": content_type
},
json={
"file": {
"display_name": filename.split(".")[0],
}
})
pre_upload_response.raise_for_status()
upload_url = pre_upload_response.headers.get("X-Goog-Upload-Url")
upload_response = client.post(url=upload_url, content=video, headers={
"X-Goog-Upload-Offset": "0",
"X-Goog-Upload-Command": "upload, finalize",
"Content-Type": content_type
})
upload_response.raise_for_status()
return upload_response.json(), upload_response.status_code
@SentryUtils.webhook_handler(webhook, current_function_call_id())
@SentryUtils.sentry_tracker(sentry_trace.x_trace_id, sentry_trace.x_baggage, op="inference_gemini",
name="Gemini推理", fn_id=current_function_call_id())
async def _handler(media: MediaSource,
google_api_key: str,
product_grid_list: List[str],
product_list: List[any],
start_time: str,
end_time: str,
options: FFMPEGSliceOptions,
sentry_trace: SentryTransactionInfo,
retry_time: int = 3,
scale:float = 0.9):
video_gemini_uri = None
try:
# 1、首先获取全量商品列表
logger.info("1、获取直播间商品列表")
if len(product_list) == 0:
logger.error("商品列表为空, 退出推理")
raise Exception("商品列表为空, 退出推理")
product_title_list = []
if isinstance(product_list[0], dict):
for product in product_list:
product_title_list.append(product["title"])
else:
product_title_list = product_list
product_info_txt = "\n".join(product_title_list)
logger.info(f"\nmedia || {media.urn},"
f"\nstart_time || {start_time}, "
f"\nend_time || {end_time}, "
f"\nproduct_grid_list || {product_grid_list}, "
f"\nproduct_list || {product_info_txt}"
f"\noptions || {options.model_dump_json()}, "
f"\nscale || {scale}")
image_parts = [{
"file_data": {
"mime_type": "image/jpeg",
"file_uri": f"{i}"
}
} for i in product_grid_list]
# 2、切20分钟的条
logger.info("2、开始截取指定视频")
# 计算缩放尺寸
metadata = VideoUtils.ffprobe_media_metadata(media.path)
width = metadata.streams[0].width * scale
height = metadata.streams[0].height * scale
if width % 2 == 1:
width = width + 1
if height % 2 == 1:
height = height + 1
options.width = int(width)
options.height = int(height)
slice_fn = modal.Function.from_name(config.modal_app_name, "ffmpeg_slice_media",
environment_name=config.modal_environment)
slice_result, sentry_trace = await slice_fn.remote.aio(media, [FFMpegSliceSegment(
start=TimeDelta.from_format_string(start_time), end=TimeDelta.from_format_string(end_time))], options,
sentry_trace)
video = MediaSource.from_str(slice_result[0].urn)
logger.success("截取完成")
# 3、转换为720p h265
# logger.info("3、转换视频为720p h264")
# video_path = convert_video(os.path.join(config.S3_mount_dir, video.path))
video_path = os.path.join(config.S3_mount_dir, video.path)
# 4、上传到gemini
logger.info("3、视频文件开始上传到Gemini")
video_gemini, code = upload(video_path)
if code == 200:
video_gemini_uri = video_gemini["file"]["uri"]
else:
logger.error("视频文件上传Gemini失败")
raise Exception("视频文件上传Gemini失败")
# 5、检查文件是否已处理完成
def check():
with httpx.Client(timeout=Timeout(timeout=30)) as client:
file = video_gemini_uri.split("/")[-1]
response = client.get(
url=f"https://generativelanguage.googleapis.com/v1beta/files/{file}?key={google_api_key}")
response.raise_for_status()
if response.status_code == 200:
if response.json()["state"] == "ACTIVE":
return True
return False
check_num = 60
logger.info("4、开始检查Gemini文件是否处理完成")
while check_num > 0:
logger.info(f"检查中, 剩余检查次数{check_num}")
ret = check()
if ret:
break
else:
check_num -= 1
time.sleep(5)
if check_num <= 0:
raise Exception("Gemini文件上传处理状态检查超时")
logger.success("Gemini文件处理完成")
# 6、执行Gemini推理操作
async def inference_api():
try:
logger.info("请求推理接口")
product_list_str = ",".join(product_title_list)
image_parts.extend([
{
"file_data": {
"mime_type": "video/mp4",
"file_uri": f"{video_gemini_uri}"
}
},
{
"text": """<prompt>
<instruction>
你是一个专业的AI视频分析师,具备强大的多模态理解能力。我上传了一个主播直播卖货的视频片段和多张商品图片。你需要分阶段进行精确的商品识别任务,确保结果的准确性。
**输入材料说明**:
- 📹 **视频**:主播直播带货片段,包含音频、画面、动作等多维信息
- 🖼️ **商品图片网格**:包含多个商品的网格布局图片,关键特征:
* 每个商品占据一个独立的黑色边框区域
* 每个区域内包含:商品图片 + 商品名称文字
* 区域按网格排列(从左上角开始,按行排列)
* **重要**:只有黑色边框内的内容才是有效商品信息
- 📋 **商品列表**:标准商品名称参考清单,用于匹配图片中识别的商品
**🚨 核心分析原则**:
1. **严格分阶段执行**:必须按照"图片解析 → 四维分析 → 最终检查"的顺序进行
2. **图片为绝对基准**:只有图片中明确存在的商品才能进入后续分析
3. **四维一致性验证**:材质、图片、视频、语音四个维度必须保持一致
4. **宁缺毋滥原则**:有任何疑问的商品都不得输出
**请充分发挥Gemini 2.5的优势,进行严格的分阶段分析**
## 🔍 第一阶段:图片解析与基础匹配(独立完成,不受其他信息干扰)
**🎯 阶段目标**:建立可靠的图片商品基础数据库
**📋 执行步骤**:
1. **完全忽略商品列表和视频内容**,专注于图片分析
2. **逐个扫描黑色边框区域**:
- 从左上角开始,按行扫描每个黑色边框区域
- 为每个区域分配序号:1, 2, 3, 4...
- 精确提取每个区域内的所有文字信息
- 记录区域位置和文字内容的对应关系
3. **建立图片商品档案**:
```
图片商品档案:
序号1: 位置(左上) | 文字内容: "XXXXX" | 视觉特征: 颜色/图案/款式
序号2: 位置(左上第二) | 文字内容: "XXXXX" | 视觉特征: 颜色/图案/款式
...
```
4. **与商品列表进行文字匹配**:
- 将每个图片商品的文字内容与商品列表进行比对
- 只有高相似度匹配才建立关联关系
- 记录匹配成功和匹配失败的商品
5. **生成基础匹配表**:
```
基础匹配表:
✅ 序号1: "图片文字" → "商品列表标准名称" (匹配度: 95%)
✅ 序号3: "图片文字" → "商品列表标准名称" (匹配度: 90%)
❌ 序号2: "图片文字" → 无匹配 (在商品列表中找不到对应项)
```
**🚨 第一阶段严格约束**:
- 🚫 **绝对禁止**参考视频内容影响图片分析
- 🚫 **绝对禁止**基于商品列表推测图片中的商品
- 🚫 **绝对禁止**输出图片中没有明确文字标注的商品
- ✅ **必须确保**每个识别的商品都有明确的图片文字依据
- ✅ **必须确保**匹配表中的商品数量不超过图片中的黑色边框区域数量
## 🎬 第二阶段:四维融合分析(基于第一阶段的匹配表)
**🎯 阶段目标**:对匹配表中的商品进行四维一致性验证
**📋 执行步骤**:
1. **仅处理匹配表中的商品**:
- 只分析第一阶段成功匹配的商品
- 忽略匹配失败的商品
- 不得添加新的商品到分析范围
2. **逐个商品精确分析**:
- **🚨 关键要求**:必须按照第一阶段的序号顺序,逐个分析每个商品
- **📍 位置对应**:严格按照图片中的位置序号,确保分析的是正确的商品
- **🔍 精确定位**:重新确认图片中该序号位置的商品特征,避免混淆
3. **四维信息精确收集**:
- 📋 **材质维度**:从该商品的标准名称中提取材质信息(纯棉、莱赛尔、天丝等)
- 🖼️ **图片维度**:**重新仔细观察图片中该序号位置的商品**,提取其真实的视觉特征:
* **颜色**:该位置商品的实际颜色(白色、黑色、蓝色、粉色等)
* **图案**:该位置商品的实际图案(文字、动物、几何图案等)
* **款式**:该位置商品的实际款式(V领、圆领、短袖、长袖等)
* **⚠️ 严格要求**:图片维度信息必须与图片中该序号位置的实际商品完全一致
- 📹 **视频维度**:识别视频中与该商品匹配的展示片段(款式细节、动态效果)
- 🎤 **语音维度**:提取主播对该商品的描述(特征、卖点)
4. **四维一致性严格验证**:
```
商品A(序号X)四维验证:
📍 位置确认: 图片序号X位置的商品特征
📋 材质一致性: 商品名称"纯棉" ↔ 视频质感"棉质" ✅一致
🖼️ 图片一致性: 图片序号X"实际特征" ↔ 视频"对应特征" ✅一致
📹 视频一致性: 视频"展示效果" ↔ 语音"描述内容" ✅一致
🎤 语音一致性: 语音"材质描述" ↔ 材质"标注信息" ✅一致
综合判断: 四维信息高度一致 → 保留
```
5. **生成验证结果**:
```
四维验证结果:
✅ 商品A(序号1): 四维一致性95% → 保留
✅ 商品B(序号3): 四维一致性90% → 保留
❌ 商品C(序号5): 四维一致性60% → 移除(图片特征与视频不符)
```
**🚨 第二阶段严格约束**:
- 🚫 **绝对禁止**分析匹配表之外的商品
- 🚫 **绝对禁止**基于视频内容添加新商品
- 🚫 **绝对禁止**四维信息明显不一致的商品进入最终结果
- 🚫 **绝对禁止**图片维度信息与图片实际内容不匹配
- 🚫 **绝对禁止**将不同序号位置的商品特征混淆
- ✅ **必须确保**图片维度信息与图片中该序号位置的实际商品完全一致
- ✅ **必须确保**每个商品的四个维度信息都相互支撑
- ✅ **必须确保**四维一致性低于80%的商品被移除
- ✅ **必须确保**重新观察图片确认每个序号位置的真实特征
## 🔍 第三阶段:最终检查与输出生成
**🎯 阶段目标**:生成高质量的最终分析结果
**📋 执行步骤**:
1. **最终商品清单确认**:
- 只包含通过四维验证的商品
- 确认每个商品都有完整的匹配追溯链
- 验证商品数量的合理性
2. **时间段分析**:
- 基于确认的商品清单进行视频时间段识别
- 精确定位每个商品的出现时间
- 标注时间段的内容类型
3. **质量保证检查**:
```
最终质量检查:
✅ 图片依据检查: 每个商品都有图片文字依据
✅ 匹配关系检查: 每个商品都有商品列表对应项
✅ 四维一致性检查: 每个商品四维信息一致
✅ 时间段合理性检查: 时间段符合视频实际内容
```
4. **生成最终JSON结果**
**🚨 第三阶段严格约束**:
- 🚫 **绝对禁止**输出未通过前两阶段验证的商品
- 🚫 **绝对禁止**在最终阶段添加新商品
- ✅ **必须确保**每个输出商品都有完整的验证链条
- ✅ **必须确保**JSON结构完全符合规范
**商品列表(标准名称参考):**
{0}
</instruction>
<rules>
<!-- 🚨 核心铁律(充分利用Gemini 2.5能力)🚨 -->
<rule>**严格分阶段分析流程**:
- 必须严格按照"第一阶段:图片解析与基础匹配 → 第二阶段:四维融合分析 → 第三阶段:最终检查与输出生成"的完整流程
- 每个阶段都有明确的输入、处理和输出要求,不得跨阶段混合处理
- 后一阶段只能基于前一阶段的输出结果进行分析,不得引入新的商品
- 输出必须体现完整的三阶段分析过程和验证结果
</rule>
<rule>**图片基础数据库驱动原则**:
- **🔒 第一阶段独立性**:图片解析必须完全独立于视频内容和商品列表,避免先入为主
- **📋 基础匹配表权威性**:第一阶段生成的基础匹配表是后续所有分析的唯一数据源
- **🚫 严禁跨阶段添加商品**:第二、三阶段不得添加第一阶段未识别的商品
- **📍 位置序号严格对应**:第二阶段分析时必须严格按照第一阶段的序号位置,确保图片维度信息与实际位置完全匹配
- **🔍 图片特征重新确认**:第二阶段必须重新仔细观察图片中每个序号位置的商品,避免特征混淆
- **✅ 逐级验证机制**:每个阶段都有严格的验证标准,不符合标准的商品逐级淘汰
- **🔄 四维一致性门槛**:第二阶段的四维一致性验证是商品进入最终结果的必要条件
- **⚠️ 质量优先原则**:宁可输出较少但准确的商品,也不输出存疑的商品
</rule>
<rule>**结构化输出约束**:
- JSON数组顺序必须严格遵循图片中的`image_order`序列
- 利用你的格式化能力确保输出结构的完美一致性
- 每个对象都必须包含完整的匹配追溯信息
</rule>
<rule>**多模态一致性验证**:
- 图片信息、视频内容、商品列表三者必须保持逻辑一致
- 利用你的跨模态理解能力发现和解决潜在冲突
- 确保所有识别结果都能在多个信息源中得到验证
</rule>
<!-- 格式规则 -->
<rule>**高质量JSON结构化输出**:
- 利用你的格式化优势,生成完美的JSON数组 `[]`
- 每个商品对象 `{{}}` 必须包含:`match_info`, `product`, `timeline` 三个核心字段
- `match_info` 必须提供完整的匹配追溯:
* `image_order`: 图片中的序号位置
* `image_name`: 图片上显示的原始名称
* `matched_list_name`: 匹配到的标准列表名称
- `product` 字段值必须与 `matched_list_name` 保持绝对一致
- 充分利用你的结构化能力确保格式完全符合规范
</rule>
<rule>**严禁使用列表外商品名**:输出的商品名称必须100%来自提供的商品列表,不得使用图片中的文字标注或其他任何名称。</rule>
<rule>**图片维度信息严格准确性**:
- **📍 位置对应准确**:图片维度信息必须与图片中对应序号位置的商品完全一致,不得混淆不同位置的商品特征
- **🔍 重新观察确认**:在第二阶段分析每个商品时,必须重新仔细观察图片中该序号位置的实际特征
- **🚫 严禁特征混淆**:绝对禁止将序号1位置的商品特征描述为序号2位置的特征,或任何其他位置混淆
- **✅ 特征描述精确**:图片维度的颜色、图案、款式描述必须与该位置商品的实际视觉特征完全匹配
- **⚠️ 一致性验证**:如果图片维度信息与图片实际内容不符,该商品必须被移除
</rule>
<rule>**超精准视觉识别能力**:充分利用Gemini 2.5的强大视觉理解能力进行细致分析:
- **🎨 极致色彩识别**:
* 基础色彩:白色、黑色、灰色、米色、卡其色等中性色的精确区分
* 彩色系统:红、橙、黄、绿、蓝、紫及其深浅变化的准确识别
* 特殊色调:莫兰迪色、马卡龙色、大地色等流行色系的识别
* 渐变效果:扎染、渐变、晕染等特殊染色工艺的识别
- **🧵 图案花纹深度解析**:
* 动物图案:鱼、猫、狗、蝴蝶、鸟类等具体动物形象的识别
* 植物图案:花朵、叶子、树木、藤蔓等植物元素的识别
* 几何图案:条纹、格子、波点、菱形、圆形等几何形状的识别
* 文字图案:英文单词、字母组合、数字、符号的具体内容识别
* 抽象图案:艺术图案、涂鸦、抽象线条等创意设计的识别
- **👔 款式结构精细分析**:
* 领型细分:圆领、V领、方领、一字肩、吊带、高领、翻领等
* 袖型细分:短袖、长袖、七分袖、五分袖、无袖、泡泡袖、灯笼袖等
* 版型细分:修身、宽松、oversize、紧身、A字型、H型、X型等
* 裤型细分:直筒、阔腿、紧身、喇叭、工装、哈伦、萝卜等
* 裙型细分:A字裙、包臀裙、百褶裙、蛋糕裙、鱼尾裙等
- **🔍 材质工艺识别**:
* 面料材质:纯棉、丝绸、雪纺、牛仔、针织、莱赛尔、天丝等
* 表面工艺:压褶、拼接、刺绣、印花、烫钻、流苏等装饰工艺
* 质感特征:光滑、粗糙、柔软、挺括、有光泽、哑光等质感表现
- **📐 尺寸比例分析**:
* 长度比例:短款、中长款、长款的具体长度判断
* 宽松程度:紧身、合身、宽松、超宽松的具体程度判断
* 细节尺寸:袖口、领口、下摆等细节部位的尺寸特征</rule>
<rule>**同款商品完整保留**:如果视频中主播穿着或介绍了商品列表中的多个同款不同色商品,必须在结果中保留所有相关商品,不能因为是同款就只选择其中一个。每个配色版本都有其独立的商业价值,都需要完整记录。</rule>
<rule>**图案配色匹配超精确度**:在判断商品匹配时要进行多层次精细分析:
- **🎯 第一层:图案主题识别** - 精确识别图案的核心主题:
* 动物主题:鱼类、猫咪、蝴蝶、鸟类等具体动物种类
* 植物主题:花朵、叶子、果实等具体植物元素
* 几何主题:条纹、格子、波点、菱形等具体几何形状
* 文字主题:英文单词、字母、数字、符号等具体文字内容
- **🎨 第二层:配色方案精确匹配** - 在图案主题相同基础上的颜色分析:
* 纯色款:主色调的精确匹配(白、黑、灰、蓝、粉、绿、黄、红等)
* 双色款:主色+辅色的组合匹配(如白底黑字、黑底白字、蓝底白字等)
* 多色款:3种以上颜色的复杂配色方案匹配
* 渐变款:渐变色的起始色、结束色、渐变方向的匹配
- **📐 第三层:图案布局分析** - 图案在服装上的具体位置和大小:
* 位置分布:胸前、背后、袖子、下摆、全身等位置
* 图案大小:小图案、中等图案、大图案、满版图案
* 排列方式:单个、重复、对称、随机分布等
- **🔍 第四层:细节特征验证** - 图案的具体细节特征:
* 线条粗细:细线条、中等线条、粗线条
* 图案风格:卡通、写实、抽象、简约等风格
* 边缘处理:清晰边缘、模糊边缘、渐变边缘等
- **💡 光线容错处理** - 考虑拍摄条件的影响:
* 室内外光线差异可能造成的色彩偏差
* 不同角度拍摄可能造成的视觉差异
* 但核心配色方案和图案主题必须明显一致</rule>
<rule>**图案款式智能识别**:对于有图案印花的商品,重点关注:
- 如果图案的形状、设计、构图完全相同,但颜色搭配不同,这属于同一款式的不同配色
- 例如:同样的鱼图案,一个是"紫色鱼+黄色底",另一个是"红色鱼+蓝色底",这是同款不同配色
- 例如:同样的条纹图案,一个是"黑白条纹",另一个是"蓝白条纹",这是同款不同配色
- 在识别时,先识别图案本身,再匹配具体的配色方案</rule>
<rule>**商品识别严格约束**:
- **🔍 图片边界严格限制**:只能识别图片中有黑色边框包围的商品区域
- **📝 文字依据必需**:每个商品必须在其黑色边框内有清晰可见的文字标注
- **🚫 绝对禁止推测**:严禁基于以下方式添加商品:
* 根据商品列表推测图片中"应该有"的商品
* 基于视觉相似性猜测没有文字标注的商品
* 将一个商品拆分成多个变体输出
* 添加图片中不存在的商品
- **📊 数量强制一致**:输出商品数量必须严格等于图片中黑色边框区域数量
- **📍 序号严格递增**:image_order必须从1开始,按图片中从左上到右下的顺序连续编号</rule>
<rule>一个商品可以存在多个timeline,请确保这个商品的所有出现过的时间都被识别出来,可以合并的碎片时间段尽量合并。</rule>
<rule>如果一个商品的演示画面中,有多个商品,需要将这些商品都识别出来,分开到不同的商品中。这部分要精细一些</rule>
<rule>如果一个片段,有主播不在画面内的内容,需要从这个商品片段中去除。</rule>
<!-- 智能行为分析(利用Gemini 2.5的多模态理解)-->
<rule>**深度行为语义分析**:利用你的多模态理解能力,精准区分:
- **👔 穿着展示行为识别**:
* 静态穿着:主播穿着某商品但未特意展示
* 动态展示:主播特意转身、走动展示穿着商品的效果
* 细节展示:主播拉扯、整理服装展示质感和版型
- **🎯 专门介绍行为识别**:
* 手持介绍:主播手持商品进行详细介绍
* 指向介绍:主播用手势指向正在介绍的商品
* 对比介绍:主播同时展示多个商品进行对比
- **👁️ 精细视觉线索识别**:
* 手势动作:指向、拿取、展示、比划等具体手势
* 身体姿态:转身、弯腰、抬手、走动等身体动作
* 视线方向:看向商品、看向镜头、看向其他位置
* 表情变化:专注、兴奋、强调等表情状态
- **🎤 语音语义深度理解**:
* 商品名称:主播明确提到的具体商品名称
* 特征描述:颜色、款式、材质、尺寸等特征词汇
* 情感倾向:推荐、强调、对比等语调变化
* 时间标记:开始介绍、结束介绍的语言信号
- **🎬 场景环境分析**:
* 背景变化:室内外、不同房间、不同角度的场景切换
* 道具使用:衣架、镜子、标签等道具的出现
* 光线变化:自然光、室内灯光等光照条件变化
* 拍摄角度:全身、半身、特写等不同拍摄角度</rule>
<rule>**时序逻辑推理**:运用你的推理能力建立时间轴上的商品关系:
- **因果关系链**:主播行为→商品展示→销售转化的逻辑链条
- **注意力转移**:识别主播注意力和观众注意力的转移规律
- **营销节奏**:理解直播带货的节奏感和商品切换的商业逻辑
</rule>
<!-- 精准时间边界识别 -->
<rule>**智能时间边界检测**:综合多维度信息精确定位商品切换时刻:
- **🎨 视觉特征变化检测**:
* 穿着变化:主播更换服装的精确时刻
* 商品切换:从展示一个商品转向另一个商品的时刻
* 颜色变化:同款不同色商品切换的时刻
* 角度变化:从不同角度展示同一商品的时间段
- **🎤 音频语义边界**:
* 商品名称提及:主播开始/结束提及某商品名称的时刻
* 话题转换:从介绍一个商品转向另一个商品的语言转折点
* 语调变化:强调、推荐等语调变化标记的重点时段
- **👁️ 动作行为边界**:
* 手势变化:指向不同商品的手势切换时刻
* 身体动作:展示不同商品时的身体动作变化
* 视线转移:主播视线从一个商品转向另一个商品的时刻
- **🔄 多信号融合验证**:
* 视觉+音频:视觉变化与语音描述的时间对应关系
* 动作+语音:身体动作与语言描述的协调性验证
* 上下文逻辑:确保时间边界符合商品介绍的逻辑连贯性
- **🎯 商业意图时间标记**:
* 重点推广:主播特别强调某商品的时间段
* 对比展示:同时展示多个商品进行对比的时间段
* 细节介绍:深入介绍商品特征的详细时间段</rule>
<!-- timeline时间段规则 -->
<rule>**时间格式标准**:
- 必须严格使用格式:HH:MM:SS.mmm - HH:MM:SS.mmm
- HH是小时(00-23),MM是分钟(00-59),SS是秒(00-59),mmm是毫秒(000-999)
- 示例:00:05:23.500 表示0小时5分钟23.5秒,不是5小时23分钟
- 常见错误:不要将05:23:500解释为5小时23分钟,应该是0小时5分钟23.5秒</rule>
<rule>每个时间段后面必须添加内容类型标识,格式为 (类型),具体类型包括:</rule>
<rule>• (穿着本品+介绍本品) - 主播穿着该商品且正在介绍该商品本身</rule>
<rule>• (穿着本品+他品) - 主播穿着该商品但正在介绍其他商品</rule>
<rule>• (穿着本品+无关) - 主播穿着该商品但在做无关商品的事情(如聊天、互动等)</rule>
<rule>判断时要仔细分析主播的语音内容,确定其介绍的具体是哪个商品,避免混淆。</rule>
<rule>时间范围一定要在视频长度范围内,例如视频长度为15分钟0秒0毫秒,时间不允许超出00:00:00.000到00:15:00.000范围</rule>
<!-- 🔍 智能质量保证(发挥Gemini 2.5推理优势)-->
<rule>**严格质量验证体系**:
- **🔍 图片解析验证**:
* 📊 **数量核查**:"输出商品数量是否不超过图片中黑色边框区域数量?"
* 📍 **序号验证**:"image_order是否对应图片中实际存在的商品位置?"
* 📝 **文字依据**:"每个image_name是否都有图片中的文字标注支撑?"
- **🎯 匹配关系验证**:
* ✅ **匹配有效性**:"每个输出商品都能在商品列表中找到对应项吗?"
* 🚫 **禁止无依据匹配**:"是否存在没有图片文字依据的强行匹配?"
* 📋 **标准名称使用**:"product字段是否使用商品列表中的标准名称?"
- **🔄 四维一致性验证**:
* 📋 **材质一致性**:"商品名称中的材质描述与视频中的质感表现是否一致?"
* 🖼️ **图片一致性**:"图片中的商品特征与视频中的展示是否匹配?"
* 📹 **视频一致性**:"视频中的款式细节(如鸡心领等)是否与商品描述匹配?"
* 🎤 **语音一致性**:"主播的语音描述是否与视觉观察结果一致?"
* ⚖️ **综合判断**:"四个维度的信息是否都指向同一个商品?"
- **⚡ 输出质量保障**:
* 🎯 **宁缺毋滥原则**:四维信息不一致的商品不得强行输出
* 📊 **完整性检查**:每个对象必须包含完整的match_info、product、timeline字段
* 🔧 **JSON格式完美**:确保输出的JSON结构完全符合规范要求
* ⚠️ **错误排除**:严格排除图片中不存在但被错误识别的商品</rule>
</rules>
<examples>
<!-- 正确的分阶段识别和输出流程示例 -->
<example_process>
**第一阶段:图片解析与基础匹配**
- 🔍 图片扫描结果:发现15个黑色边框区域
- 📝 文字提取:
* 序号1(左上角):"FORNI Baby tee 高克重100纯棉V领正肩修身T恤"
* 序号2:"FORNI 三明治空气裤"
* 序号3:"美洋MEIYANG 水面膜T恤"
* ...依次提取所有区域的文字
- 📋 基础匹配表生成:
* ✅ 序号1: "FORNI Baby tee..." → "6月12日16:00新品 FORNI Baby tee 高克重100纯棉V领正肩修身T恤" (匹配度: 95%)
* ✅ 序号3: "美洋MEIYANG 水面膜T恤" → "美洋MEIYANG【商场同款】水面膜T恤 莱赛尔修身圆领短袖-周一" (匹配度: 90%)
* ❌ 序号2: "FORNI 三明治空气裤" → 无匹配 (在商品列表中找不到对应项)
**第二阶段:四维融合分析**
- 🎯 分析范围:仅处理序号1和序号3(基础匹配表中的成功匹配商品)
- 🔄 序号1四维验证(重新确认图片位置):
* 📍 位置确认: 重新观察图片序号1位置(左上角第一个商品)
* 📋 材质一致性: 商品名称"100纯棉" ↔ 视频质感"棉质" ✅一致
* 🖼️ 图片一致性: 图片序号1位置"白色V领短袖T恤,印有天使图案" ↔ 视频"白色V领短袖,天使图案清晰" ✅一致
* 📹 视频一致性: 视频"修身版型,天使图案" ↔ 语音"修身显瘦,天使印花" ✅一致
* 🎤 语音一致性: 语音"纯棉材质,天使图案" ↔ 材质"纯棉" ✅一致
* 综合判断: 四维一致性95% → 保留
- 🔄 序号3四维验证(重新确认图片位置):
* 📍 位置确认: 重新观察图片序号3位置的商品特征
* 📋 材质一致性: 商品名称"莱赛尔" ↔ 视频质感"丝滑" ✅一致
* 🖼️ 图片一致性: 图片序号3位置"实际颜色和款式" ↔ 视频"对应的颜色和款式" ✅一致
* 📹 视频一致性: 视频"实际展示效果" ↔ 语音"对应描述" ✅一致
* 🎤 语音一致性: 语音"莱赛尔面料" ↔ 材质"莱赛尔" ✅一致
* 综合判断: 四维一致性92% → 保留
**第三阶段:最终检查与输出生成**
- ✅ 最终商品清单:序号1、序号3通过所有验证
- ⏰ 时间段分析:基于确认商品进行视频时间段识别
- 📊 质量检查:所有输出商品都有完整验证链条
- 🎯 最终输出:
[
{{
"match_info": {{
"image_order": 1,
"image_name": "FORNI Baby tee 高克重100纯棉V领正肩修身T恤",
"matched_list_name": "6月12日16:00新品 FORNI Baby tee 高克重100纯棉V领正肩修身T恤"
}},
"product": "6月12日16:00新品 FORNI Baby tee 高克重100纯棉V领正肩修身T恤",
"timeline": ["00:01:15.200 - 00:02:30.800 (穿着本品+介绍本品)"]
}},
{{
"match_info": {{
"image_order": 3,
"image_name": "美洋MEIYANG 水面膜T恤",
"matched_list_name": "美洋MEIYANG【商场同款】水面膜T恤 莱赛尔修身圆领短袖-周一"
}},
"product": "美洋MEIYANG【商场同款】水面膜T恤 莱赛尔修身圆领短袖-周一",
"timeline": ["00:03:45.100 - 00:05:12.600 (穿着本品+介绍本品)"]
}}
]
</example_process>
<reason>
这个输出完美体现了严格的分阶段分析逻辑:
**第一阶段成果**:
1. **🔍 独立图片解析**:完全基于图片内容,不受视频或商品列表影响,发现15个区域但只有部分能匹配。
2. **📝 文字依据充分**:每个序号都有明确的图片文字标注作为依据。
3. **📋 基础匹配表可靠**:只有高相似度匹配才建立关联,序号2因无匹配被排除。
**第二阶段成果**:
4. **🎯 范围严格限制**:只分析第一阶段成功匹配的商品,不添加新商品。
5. **📍 位置对应精确**:重新确认图片中每个序号位置的实际商品特征,避免混淆。
6. **🔄 四维验证严格**:每个商品都经过材质、图片、视频、语音四个维度的一致性验证。
7. **🖼️ 图片维度准确**:图片维度信息与图片中对应位置的实际商品完全一致。
8. **⚖️ 质量门槛明确**:四维一致性低于80%的商品会被移除。
**第三阶段成果**:
7. **✅ 最终结果可靠**:只输出通过所有验证的商品,确保准确性。
8. **📊 完整验证链条**:每个输出商品都有从图片到最终结果的完整追溯。
**关键优势体现**:
- ❌ 彻底避免AI幻觉:分阶段验证机制防止输出不存在的商品
- ❌ 杜绝推测匹配:第一阶段的独立性确保基于事实而非推测
- ❌ 消除交叉干扰:各阶段职责明确,避免信息混淆
- ❌ 防止特征混淆:严格的位置对应机制确保图片维度信息与实际位置完全匹配
- ❌ 避免描述错位:重新观察确认机制防止将不同位置商品的特征相互混淆
- ✅ 确保结果质量:多重验证机制保证输出的准确性和可靠性
- ✅ 保证信息一致:图片维度信息与图片实际内容严格对应
</reason>
</examples>
</prompt>"""
.format(product_list_str)
}
])
json_data = {
"safetySettings": [
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}
],
"contents": [
{
"parts": image_parts
}
],
"generationConfig": {
"temperature": 0.01
}
}
resp = requests.post(
"https://gateway.ai.cloudflare.com/v1/67720b647ff2b55cf37ba3ef9e677083/bowong-dev/google-ai-studio/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent",
headers={
"Content-Type": "application/json",
"X-Goog-Api-Key": f"{google_api_key}"
},
json=json_data, timeout=900
)
except Exception as e:
logger.exception(f"Gemini推理请求失败: {e}")
raise e
if resp.status_code == 200:
return resp.json()
else:
logger.error(f"Gemini推理失败, 状态码{resp.status_code}")
if resp.status_code == 429:
logger.warning("请求频率过高, 等待50-70秒后重试")
await asyncio.sleep(random.randint(50, 70))
return None
logger.info("5、发起Gemini推理")
target_json = None
while target_json is None and retry_time > 0:
target_json = await inference_api()
retry_time -= 1
if target_json is None:
raise Exception("推理失败")
logger.info(f"推理完成JSON \n{json.dumps(target_json, indent=4, ensure_ascii=False)}")
reason = target_json["candidates"][0]["finishReason"]
if reason == "STOP":
parts: str = target_json["candidates"][0]["content"]["parts"][0]["text"]
parts = parts.replace("```", "").replace("json\n", "").replace("\n", "").replace("\\", "")
parts = json.loads(parts)
# 合并产品和时间线
parts = merge_product_data(parts, start_time, end_time)
return parts, sentry_trace
else:
raise Exception(f"Gemini推理失败, 失败原因: {reason}")
except Exception as e:
logger.exception(f"推理失败, {e}")
raise Exception(f"推理失败, {e}")
finally:
if video_gemini_uri:
logger.info("6、清除Gemini临时文件")
with httpx.Client(timeout=Timeout(timeout=120)) as client:
resp = client.delete(
f'https://bowongai-{config.modal_environment}--{config.modal_app_name}-fastapi-webapp.modal.run/google/delete',
params={"filename": video_gemini_uri.split("/")[-1]},
headers={"x-google-api-key": google_api_key})
resp.raise_for_status()
if resp.status_code == 200:
logger.success("Gemini临时文件清除成功")
else:
logger.warning("Gemini临时文件清除失败, 请自行清除")
return await _handler(media, google_api_key, product_grid_list, product_list, start_time, end_time, options,
sentry_trace, retry_time, scale)