合并分支
* Merge branch 'main' into cluster-gemini * FIX Gemini推理配置修复langfuse来源 * PERF Gemini推理配置适配langfuse来源 FIX 时间转换适配更多时间格式 * PERF prompt接入langfuse * Merge branch 'main' into cluster-gemini * FIX 修复pydantic导致的问题 PERF 预先使用prompt传输格式要求,如果失败fallback到调用gemini处理格式错误 * Merge branch 'main' into cluster-gemini * FIX 修复上传问题 * PERF 规范化gemini调用方式, 使用response_schema规范化输出 * PERF 修改gemini upload调用为规范调用 * PERF 优化prompt词提高识别准确率 FIX 修复升级httpx导致部署失败的问题 * Merge branch 'main' into cluster-gemini * PERF 优化prompt词提高识别准确率 * FIX 修复prompt时间问题 * Merge branch 'main' into cluster-gemini * PERF 修复时间最大限制问题 * Merge branch 'main' into cluster-gemini * FIX 修复缩放分辨率计算问题 ADD Gemini推理改为二阶段 FIX 修复时间合并计算问题 * Merge branch 'main' into cluster-gemini * ADD gemini数据源使用cloud storage --------- Merge request URL: https://g-ldyi2063.coding.net/p/dev/d/modalDeploy/git/merge/4861 Co-authored-by: 康宇佳
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@@ -652,3 +653,25 @@ class GeminiSecondStageResponseModel(RootModel):
|
|||||||
表示一个包含多个商品及其时间线信息的列表。
|
表示一个包含多个商品及其时间线信息的列表。
|
||||||
"""
|
"""
|
||||||
root: List[ProductTimeline]
|
root: List[ProductTimeline]
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiFirstStagePromptVariables(BaseModel):
|
||||||
|
product_list: List[str] = Field(description="商品名列表")
|
||||||
|
|
||||||
|
@computed_field(description="xml格式排列的商品列表")
|
||||||
|
@property
|
||||||
|
def product_list_xml(self) -> str:
|
||||||
|
xml_items = [f" <product>{product}</product>" for product in self.product_list]
|
||||||
|
xml_string = "\n".join(xml_items)
|
||||||
|
return f"<products>\n{xml_string}\n </products>"
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiSecondStagePromptVariables(BaseModel):
|
||||||
|
product_json_list: List[dict] = Field(description="识别出的商品JSON列表")
|
||||||
|
|
||||||
|
@computed_field(description="xml格式排列的识别出的商品JSON列表")
|
||||||
|
@property
|
||||||
|
def product_json_list_xml(self) -> str:
|
||||||
|
xml_items = [f" <product>{json.dumps(product,ensure_ascii=False)}</product>" for product in self.product_json_list]
|
||||||
|
xml_string = "\n".join(xml_items)
|
||||||
|
return f"<products>\n{xml_string}\n </products>"
|
||||||
@@ -26,19 +26,29 @@ def parse_time(time_str):
|
|||||||
if re.match(r"^\d{2}:\d{2}:\d{2}\.\d{3}$",time_str):
|
if re.match(r"^\d{2}:\d{2}:\d{2}\.\d{3}$",time_str):
|
||||||
# 先尝试完整格式 HH:MM:SS.fff
|
# 先尝试完整格式 HH:MM:SS.fff
|
||||||
parsed_time = datetime.strptime(time_str, '%H:%M:%S.%f')
|
parsed_time = datetime.strptime(time_str, '%H:%M:%S.%f')
|
||||||
|
elif re.match(r"^\d{2}:\d{2}:\d{2}:\d{3}$",time_str):
|
||||||
|
# 如果失败,尝试 HH:MM:SS:fff 格式
|
||||||
|
parsed_time = datetime.strptime(time_str, '%H:%M:%S:%f')
|
||||||
elif re.match(r"^\d{2}:\d{2}\.\d{3}$",time_str):
|
elif re.match(r"^\d{2}:\d{2}\.\d{3}$",time_str):
|
||||||
# 如果失败,尝试 MM:SS.fff 格式
|
# 如果失败,尝试 MM:SS.fff 格式
|
||||||
dt = datetime.strptime(time_str, '%M:%S.%f')
|
dt = datetime.strptime(time_str, '%M:%S.%f')
|
||||||
# 将小时设为0,只保留分钟和秒
|
# 将小时设为0,只保留分钟和秒
|
||||||
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
|
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
|
||||||
|
elif re.match(r"^\d{2}\.\d{2}:\d{3}$",time_str):
|
||||||
|
# 如果失败,尝试 MM.SS:fff 格式
|
||||||
|
dt = datetime.strptime(time_str, '%M.%S:%f')
|
||||||
|
# 将小时设为0,只保留分钟和秒
|
||||||
|
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
|
||||||
|
elif re.match(r"^\d{2}\.\d{2}\.\d{3}$",time_str):
|
||||||
|
# 如果失败,尝试 MM.SS.fff 格式
|
||||||
|
dt = datetime.strptime(time_str, '%M.%S.%f')
|
||||||
|
# 将小时设为0,只保留分钟和秒
|
||||||
|
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
|
||||||
elif re.match(r"^\d{2}:\d{2}:\d{3}$",time_str):
|
elif re.match(r"^\d{2}:\d{2}:\d{3}$",time_str):
|
||||||
# 如果失败,尝试 MM:SS:fff 格式
|
# 如果失败,尝试 MM:SS:fff 格式
|
||||||
dt = datetime.strptime(time_str, '%M:%S:%f')
|
dt = datetime.strptime(time_str, '%M:%S:%f')
|
||||||
# 将小时设为0,只保留分钟和秒
|
# 将小时设为0,只保留分钟和秒
|
||||||
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
|
parsed_time = datetime.combine(dt.date(), dt.time().replace(hour=0))
|
||||||
elif re.match(r"^\d{2}:\d{2}:\d{2}:\d{3}$",time_str):
|
|
||||||
# 如果失败,尝试 HH:MM:SS:fff 格式
|
|
||||||
parsed_time = datetime.strptime(time_str, '%H:%M:%S:%f')
|
|
||||||
else:
|
else:
|
||||||
logger.error(f"转换时间格式失败 {time_str}")
|
logger.error(f"转换时间格式失败 {time_str}")
|
||||||
return parsed_time
|
return parsed_time
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import uuid
|
|||||||
|
|
||||||
import modal
|
import modal
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
|
from jinja2 import Template
|
||||||
|
from langfuse._client.client import Langfuse
|
||||||
|
from langfuse.model import TextPromptClient
|
||||||
|
|
||||||
from BowongModalFunctions.utils.HTTPUtils import GoogleAuthUtils, FlatJsonSchemaGenerator
|
from BowongModalFunctions.utils.HTTPUtils import GoogleAuthUtils, FlatJsonSchemaGenerator
|
||||||
from ..video import downloader_image, app, config
|
from ..video import downloader_image, app, config
|
||||||
@@ -16,8 +19,8 @@ with downloader_image.imports():
|
|||||||
from starlette import status
|
from starlette import status
|
||||||
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
from BowongModalFunctions.utils.SentryUtils import SentryUtils
|
||||||
from BowongModalFunctions.models.media_model import MediaSource
|
from BowongModalFunctions.models.media_model import MediaSource
|
||||||
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, \
|
from BowongModalFunctions.models.web_model import SentryTransactionInfo, WebhookNotify, GeminiFirstStagePromptVariables, \
|
||||||
GeminiFirstStageResponseModel, GeminiSecondStageResponseModel
|
GeminiSecondStagePromptVariables, GeminiFirstStageResponseModel, GeminiSecondStageResponseModel
|
||||||
from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
|
from BowongModalFunctions.models.ffmpeg_worker_model import FFMpegSliceSegment
|
||||||
from BowongModalFunctions.utils.TimeUtils import TimeDelta, merge_product_data
|
from BowongModalFunctions.utils.TimeUtils import TimeDelta, merge_product_data
|
||||||
from BowongModalFunctions.utils.VideoUtils import FFMPEGSliceOptions, VideoUtils
|
from BowongModalFunctions.utils.VideoUtils import FFMPEGSliceOptions, VideoUtils
|
||||||
@@ -25,7 +28,6 @@ with downloader_image.imports():
|
|||||||
|
|
||||||
@app.function(cpu=(0.5, 64), timeout=1800,
|
@app.function(cpu=(0.5, 64), timeout=1800,
|
||||||
max_containers=config.video_downloader_concurrency,
|
max_containers=config.video_downloader_concurrency,
|
||||||
secrets=[modal.Secret.from_name("gemini-prompt")],
|
|
||||||
volumes={
|
volumes={
|
||||||
config.S3_mount_dir: modal.CloudBucketMount(
|
config.S3_mount_dir: modal.CloudBucketMount(
|
||||||
bucket_name=config.S3_bucket_name,
|
bucket_name=config.S3_bucket_name,
|
||||||
@@ -65,6 +67,11 @@ with downloader_image.imports():
|
|||||||
"europe-west8",
|
"europe-west8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
langfuse = Langfuse(host="https://us.cloud.langfuse.com",
|
||||||
|
secret_key="sk-lf-dd20cb0b-ef2e-49f6-80f0-b2d9cff1bb11",
|
||||||
|
public_key="pk-lf-15f9d809-0bf6-4a84-ae1c-18f7a7d927c7",
|
||||||
|
tracing_enabled=False)
|
||||||
|
|
||||||
logger.info(f"\nmedia || {media.urn},"
|
logger.info(f"\nmedia || {media.urn},"
|
||||||
f"\nstart_time || {start_time}, "
|
f"\nstart_time || {start_time}, "
|
||||||
f"\nend_time || {end_time}, "
|
f"\nend_time || {end_time}, "
|
||||||
@@ -80,10 +87,20 @@ with downloader_image.imports():
|
|||||||
regions=gemini_region, access_token=google_api_key,
|
regions=gemini_region, access_token=google_api_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 动态Prompt
|
def split_prompt_config(c:dict):
|
||||||
if "IMAGE_PRODUCT_IDENTIFICATION_PROMPT" in os.environ:
|
return {
|
||||||
IMAGE_PRODUCT_IDENTIFICATION_PROMPT = os.environ["IMAGE_PRODUCT_IDENTIFICATION_PROMPT"]
|
"temperature": c["temperature"],
|
||||||
|
"top_p": c["top_p"],
|
||||||
|
"safety_settings": c["safety_settings"]
|
||||||
|
},c
|
||||||
|
|
||||||
|
# 动态Prompt, langfuse获取失败使用默认值
|
||||||
|
IMAGE_PRODUCT_IDENTIFICATION_PROMPT = langfuse.get_prompt("Gemini自动切条/商品识别", type="text", label="latest")
|
||||||
|
if IMAGE_PRODUCT_IDENTIFICATION_PROMPT:
|
||||||
|
prompt_config = IMAGE_PRODUCT_IDENTIFICATION_PROMPT.config
|
||||||
|
first_stage_generate_config, first_stage_correct_config = split_prompt_config(prompt_config)
|
||||||
else:
|
else:
|
||||||
|
logger.warning("🥺获取一阶段Prompt失败,使用内置Prompt")
|
||||||
IMAGE_PRODUCT_IDENTIFICATION_PROMPT = """<prompt>
|
IMAGE_PRODUCT_IDENTIFICATION_PROMPT = """<prompt>
|
||||||
<instruction>
|
<instruction>
|
||||||
你是专业的商品识别专家。我上传了商品图片网格,需要你识别图片中的商品并与商品列表进行匹配。
|
你是专业的商品识别专家。我上传了商品图片网格,需要你识别图片中的商品并与商品列表进行匹配。
|
||||||
@@ -108,32 +125,66 @@ with downloader_image.imports():
|
|||||||
- ✅ 输出商品数量不得超过图片中的黑色边框区域数量
|
- ✅ 输出商品数量不得超过图片中的黑色边框区域数量
|
||||||
|
|
||||||
**商品列表-标准商品名称**:
|
**商品列表-标准商品名称**:
|
||||||
<items>
|
{{PRODUCT_LIST}}
|
||||||
{PRODUCT_LIST}
|
|
||||||
</items>
|
|
||||||
</instruction>
|
</instruction>
|
||||||
|
|
||||||
<output_format>
|
<output_format>
|
||||||
请按以下JSON格式输出:
|
请按以下JSON格式输出:
|
||||||
[
|
[
|
||||||
{{
|
{
|
||||||
"image_order": 1,
|
"image_order": 1,
|
||||||
"image_name": "图片上显示的原始文字",
|
"image_name": "图片上显示的原始文字",
|
||||||
"matched_product": "匹配到的标准商品名称(必须与商品列表-标准商品名称一致)或null",
|
"matched_product": "匹配到的标准商品名称(必须与商品列表-标准商品名称一致)或null",
|
||||||
"match_confidence": 95,
|
"match_confidence": 95,
|
||||||
"visual_features": {{
|
"visual_features": {
|
||||||
"color": "商品详细配色等有辨识度的色彩特征",
|
"color": "商品详细配色等有辨识度的色彩特征",
|
||||||
"pattern": "商品详细图案纹理布料等有辨识度的材质特征,材质可根据商品名以及图片综合进行判断",
|
"pattern": "商品详细图案纹理布料等有辨识度的材质特征,材质可根据商品名以及图片综合进行判断",
|
||||||
"style": "商品详细款式风格等有辨识度的款式特征"
|
"style": "商品详细款式风格等有辨识度的款式特征"
|
||||||
}}
|
}
|
||||||
}}
|
}
|
||||||
]
|
]
|
||||||
</output_format>
|
</output_format>
|
||||||
</prompt>"""
|
</prompt>"""
|
||||||
|
first_stage_correct_config = {
|
||||||
|
"temperature": 0.01,
|
||||||
|
"top_p": 0.7,
|
||||||
|
"safety_settings": [
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_HARASSMENT",
|
||||||
|
"threshold": "BLOCK_NONE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_CIVIC_INTEGRITY",
|
||||||
|
"threshold": "BLOCK_NONE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||||
|
"threshold": "BLOCK_NONE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||||
|
"threshold": "BLOCK_NONE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||||
|
"threshold": "BLOCK_NONE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"response_mime_type": "application/json",
|
||||||
|
"response_schema": GeminiFirstStageResponseModel.model_json_schema(schema_generator=FlatJsonSchemaGenerator)
|
||||||
|
}
|
||||||
|
first_stage_generate_config = {
|
||||||
|
"temperature": first_stage_correct_config["temperature"],
|
||||||
|
"top_p": first_stage_correct_config["top_p"],
|
||||||
|
"safety_settings": first_stage_correct_config["safety_settings"]
|
||||||
|
}
|
||||||
|
|
||||||
if "VIDEO_TIMELINE_ANALYSIS_PROMPT" in os.environ:
|
VIDEO_TIMELINE_ANALYSIS_PROMPT = langfuse.get_prompt("Gemini自动切条/视频时间点识别", type="text", label="latest")
|
||||||
VIDEO_TIMELINE_ANALYSIS_PROMPT = os.environ["VIDEO_TIMELINE_ANALYSIS_PROMPT"]
|
if VIDEO_TIMELINE_ANALYSIS_PROMPT:
|
||||||
|
prompt_config = VIDEO_TIMELINE_ANALYSIS_PROMPT.config
|
||||||
|
second_stage_generate_config, second_stage_correct_config = split_prompt_config(prompt_config)
|
||||||
else:
|
else:
|
||||||
|
logger.warning("🥺获取二阶段Prompt失败,使用内置Prompt")
|
||||||
VIDEO_TIMELINE_ANALYSIS_PROMPT = """<prompt>
|
VIDEO_TIMELINE_ANALYSIS_PROMPT = """<prompt>
|
||||||
<instruction>
|
<instruction>
|
||||||
基于已识别的商品清单,分析视频中每个商品的出现时间段。
|
基于已识别的商品清单,分析视频中每个商品的出现时间段。
|
||||||
@@ -176,24 +227,55 @@ with downloader_image.imports():
|
|||||||
- 常见错误:将05:23.500解释为5分钟23.5秒
|
- 常见错误:将05:23.500解释为5分钟23.5秒
|
||||||
|
|
||||||
**已识别商品清单(product-商品名 feature-商品特征 feature.color-商品详细颜色配色等色彩特征 feature.pattern-商品详细图案纹理布料等材质特征 feature.style-商品详细款式风格等有辨识度的款式特征)**:
|
**已识别商品清单(product-商品名 feature-商品特征 feature.color-商品详细颜色配色等色彩特征 feature.pattern-商品详细图案纹理布料等材质特征 feature.style-商品详细款式风格等有辨识度的款式特征)**:
|
||||||
<items>
|
{{IDENTIFIED_PRODUCTS}}
|
||||||
{IDENTIFIED_PRODUCTS}
|
|
||||||
</items>
|
|
||||||
</instruction>
|
</instruction>
|
||||||
|
|
||||||
<output_format>
|
<output_format>
|
||||||
请按以下JSON格式输出,确保输出格式复核JSON格式要求:
|
请按以下JSON格式输出,确保输出格式复核JSON格式要求:
|
||||||
[
|
[
|
||||||
{{
|
{
|
||||||
"product": "标准商品名称",
|
"product": "标准商品名称",
|
||||||
"timeline": [
|
"timeline": [
|
||||||
"01:15.200 - 02:30.800 (穿着本品+介绍本品)",
|
"01:15.200 - 02:30.800 (穿着本品+介绍本品)",
|
||||||
"05:10.100 - 06:25.600 (穿着本品+他品)"
|
"05:10.100 - 06:25.600 (穿着本品+他品)"
|
||||||
]
|
]
|
||||||
}}
|
}
|
||||||
]
|
]
|
||||||
</output_format>
|
</output_format>
|
||||||
</prompt>"""
|
</prompt>"""
|
||||||
|
second_stage_correct_config = {
|
||||||
|
"temperature": 0.01,
|
||||||
|
"top_p": 0.7,
|
||||||
|
"safety_settings": [
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_HARASSMENT",
|
||||||
|
"threshold": "BLOCK_NONE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_CIVIC_INTEGRITY",
|
||||||
|
"threshold": "BLOCK_NONE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||||
|
"threshold": "BLOCK_NONE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||||
|
"threshold": "BLOCK_NONE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||||
|
"threshold": "BLOCK_NONE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"response_mime_type": "application/json",
|
||||||
|
"response_schema": GeminiSecondStageResponseModel.model_json_schema(schema_generator=FlatJsonSchemaGenerator)
|
||||||
|
}
|
||||||
|
second_stage_generate_config = {
|
||||||
|
"temperature": second_stage_correct_config["temperature"],
|
||||||
|
"top_p": second_stage_correct_config["top_p"],
|
||||||
|
"safety_settings": second_stage_correct_config["safety_settings"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def upload(file_path):
|
async def upload(file_path):
|
||||||
@@ -215,7 +297,7 @@ with downloader_image.imports():
|
|||||||
|
|
||||||
return upload_response.urn
|
return upload_response.urn
|
||||||
|
|
||||||
def convert_json(json_like_str:str, json_model):
|
def convert_json(json_like_str:str, correct_config):
|
||||||
try:
|
try:
|
||||||
resp, resp_code = client.generate_content(model_id="gemini-2.5-flash",
|
resp, resp_code = client.generate_content(model_id="gemini-2.5-flash",
|
||||||
contents=[types.Content(role='user',
|
contents=[types.Content(role='user',
|
||||||
@@ -232,35 +314,7 @@ with downloader_image.imports():
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
config=types.GenerateContentConfig(
|
config=types.GenerateContentConfig.model_validate(correct_config), timeout=900)
|
||||||
temperature=0.01,
|
|
||||||
top_p=0.7,
|
|
||||||
safety_settings=[
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_HARASSMENT,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_CIVIC_INTEGRITY,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
response_mime_type="application/json",
|
|
||||||
response_schema=json_model.model_json_schema(
|
|
||||||
schema_generator=FlatJsonSchemaGenerator)
|
|
||||||
), timeout=900)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"😭格式化json请求失败 {e}")
|
logger.exception(f"😭格式化json请求失败 {e}")
|
||||||
return None
|
return None
|
||||||
@@ -282,9 +336,7 @@ with downloader_image.imports():
|
|||||||
logger.warning("🥵请求负载过高, 随机更换地区重试")
|
logger.warning("🥵请求负载过高, 随机更换地区重试")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def parse_stage1_result(result_text, correct_config):
|
||||||
|
|
||||||
def parse_stage1_result(result_text):
|
|
||||||
"""解析第一阶段结果,提取已识别的商品列表"""
|
"""解析第一阶段结果,提取已识别的商品列表"""
|
||||||
try:
|
try:
|
||||||
# 清理结果文本,提取JSON部分
|
# 清理结果文本,提取JSON部分
|
||||||
@@ -298,7 +350,7 @@ with downloader_image.imports():
|
|||||||
try:
|
try:
|
||||||
stage1_data = json.loads(clean_result)
|
stage1_data = json.loads(clean_result)
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
stage1_data = convert_json(clean_result, GeminiFirstStageResponseModel)
|
stage1_data = convert_json(clean_result, correct_config)
|
||||||
if not stage1_data:
|
if not stage1_data:
|
||||||
raise Exception("json为空")
|
raise Exception("json为空")
|
||||||
identified_products = []
|
identified_products = []
|
||||||
@@ -321,7 +373,6 @@ with downloader_image.imports():
|
|||||||
@SentryUtils.sentry_tracker(sentry_trace.x_trace_id, sentry_trace.x_baggage, op="inference_gemini",
|
@SentryUtils.sentry_tracker(sentry_trace.x_trace_id, sentry_trace.x_baggage, op="inference_gemini",
|
||||||
name="Gemini推理", fn_id=current_function_call_id())
|
name="Gemini推理", fn_id=current_function_call_id())
|
||||||
async def _handler(media: MediaSource,
|
async def _handler(media: MediaSource,
|
||||||
google_api_key: str,
|
|
||||||
product_grid_list: List[str],
|
product_grid_list: List[str],
|
||||||
product_list: List[any],
|
product_list: List[any],
|
||||||
start_time: str,
|
start_time: str,
|
||||||
@@ -372,9 +423,6 @@ with downloader_image.imports():
|
|||||||
async def inference_api_first_stage():
|
async def inference_api_first_stage():
|
||||||
try:
|
try:
|
||||||
logger.info("🎈开始一阶段推理")
|
logger.info("🎈开始一阶段推理")
|
||||||
product_list_str = ""
|
|
||||||
for i, product in enumerate(product_title_list):
|
|
||||||
product_list_str += f"<item>{i}. {product}</item>\n"
|
|
||||||
image_parts = []
|
image_parts = []
|
||||||
for i in product_grid_list:
|
for i in product_grid_list:
|
||||||
image_parts.append(
|
image_parts.append(
|
||||||
@@ -387,7 +435,10 @@ with downloader_image.imports():
|
|||||||
)
|
)
|
||||||
image_parts.append(
|
image_parts.append(
|
||||||
types.Part.from_text(
|
types.Part.from_text(
|
||||||
text=IMAGE_PRODUCT_IDENTIFICATION_PROMPT.format(PRODUCT_LIST=product_list_str)
|
text=Template(IMAGE_PRODUCT_IDENTIFICATION_PROMPT.prompt
|
||||||
|
if isinstance(IMAGE_PRODUCT_IDENTIFICATION_PROMPT, TextPromptClient)
|
||||||
|
else IMAGE_PRODUCT_IDENTIFICATION_PROMPT).render(
|
||||||
|
PRODUCT_LIST=GeminiFirstStagePromptVariables(product_list=product_title_list).product_list_xml)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
resp, resp_code = client.generate_content(model_id="gemini-2.5-flash",
|
resp, resp_code = client.generate_content(model_id="gemini-2.5-flash",
|
||||||
@@ -395,32 +446,7 @@ with downloader_image.imports():
|
|||||||
parts=image_parts
|
parts=image_parts
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
config=types.GenerateContentConfig(
|
config=types.GenerateContentConfig.model_validate(first_stage_generate_config), timeout=900)
|
||||||
temperature=0.01,
|
|
||||||
top_p=0.7,
|
|
||||||
safety_settings=[
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_HARASSMENT,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_CIVIC_INTEGRITY,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
), timeout=900)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"😭Gemini一阶段推理请求失败: {e}")
|
logger.exception(f"😭Gemini一阶段推理请求失败: {e}")
|
||||||
raise e
|
raise e
|
||||||
@@ -430,7 +456,7 @@ with downloader_image.imports():
|
|||||||
if reason == "STOP":
|
if reason == "STOP":
|
||||||
result_text: str = resp.candidates[0].content.parts[0].text
|
result_text: str = resp.candidates[0].content.parts[0].text
|
||||||
# 解析识别结果
|
# 解析识别结果
|
||||||
identified_products = parse_stage1_result(result_text)
|
identified_products = parse_stage1_result(result_text, first_stage_correct_config)
|
||||||
return identified_products
|
return identified_products
|
||||||
else:
|
else:
|
||||||
logger.error(f"😭Gemini一阶段推理失败, Reason {reason}")
|
logger.error(f"😭Gemini一阶段推理失败, Reason {reason}")
|
||||||
@@ -444,11 +470,6 @@ with downloader_image.imports():
|
|||||||
async def inference_api_second_stage(identified_products):
|
async def inference_api_second_stage(identified_products):
|
||||||
try:
|
try:
|
||||||
logger.info("🎈开始二阶段推理")
|
logger.info("🎈开始二阶段推理")
|
||||||
# 构建已识别商品清单字符串
|
|
||||||
identified_products_str = ""
|
|
||||||
for i, product in enumerate(identified_products):
|
|
||||||
product_info_json = json.dumps(product,ensure_ascii=False)
|
|
||||||
identified_products_str += f"<item>{i}. {product_info_json}<item>\n"
|
|
||||||
video_parts = []
|
video_parts = []
|
||||||
video_parts.append(
|
video_parts.append(
|
||||||
types.Part(
|
types.Part(
|
||||||
@@ -460,7 +481,10 @@ with downloader_image.imports():
|
|||||||
)
|
)
|
||||||
video_parts.append(
|
video_parts.append(
|
||||||
types.Part.from_text(
|
types.Part.from_text(
|
||||||
text=VIDEO_TIMELINE_ANALYSIS_PROMPT.format(IDENTIFIED_PRODUCTS=identified_products_str)
|
text=Template(VIDEO_TIMELINE_ANALYSIS_PROMPT.prompt
|
||||||
|
if isinstance(VIDEO_TIMELINE_ANALYSIS_PROMPT, TextPromptClient)
|
||||||
|
else VIDEO_TIMELINE_ANALYSIS_PROMPT).render(
|
||||||
|
IDENTIFIED_PRODUCTS=GeminiSecondStagePromptVariables(product_json_list=identified_products).product_json_list_xml)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
resp, resp_code = client.generate_content(model_id="gemini-2.5-flash",
|
resp, resp_code = client.generate_content(model_id="gemini-2.5-flash",
|
||||||
@@ -468,32 +492,7 @@ with downloader_image.imports():
|
|||||||
parts=video_parts
|
parts=video_parts
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
config=types.GenerateContentConfig(
|
config=types.GenerateContentConfig.model_validate(second_stage_generate_config), timeout=900)
|
||||||
temperature=0.01,
|
|
||||||
top_p=0.7,
|
|
||||||
safety_settings=[
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_HARASSMENT,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_CIVIC_INTEGRITY,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
),
|
|
||||||
types.SafetySetting(
|
|
||||||
category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
|
||||||
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
), timeout=900)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"😭Gemini二阶段推理请求失败: {e}")
|
logger.exception(f"😭Gemini二阶段推理请求失败: {e}")
|
||||||
raise e
|
raise e
|
||||||
@@ -512,7 +511,7 @@ with downloader_image.imports():
|
|||||||
try:
|
try:
|
||||||
parts = json.loads(parts_text)
|
parts = json.loads(parts_text)
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
parts = convert_json(parts_text, GeminiSecondStageResponseModel)
|
parts = convert_json(parts_text, second_stage_correct_config)
|
||||||
if not parts:
|
if not parts:
|
||||||
raise Exception("json为空")
|
raise Exception("json为空")
|
||||||
logger.info(f"👌合并前 {json.dumps(parts, indent=4, ensure_ascii=False)}")
|
logger.info(f"👌合并前 {json.dumps(parts, indent=4, ensure_ascii=False)}")
|
||||||
@@ -562,5 +561,5 @@ with downloader_image.imports():
|
|||||||
logger.exception(f"🥲推理失败, {e}")
|
logger.exception(f"🥲推理失败, {e}")
|
||||||
raise Exception(f"🥲推理失败, {e}")
|
raise Exception(f"🥲推理失败, {e}")
|
||||||
|
|
||||||
return await _handler(media, google_api_key, product_grid_list, product_list, start_time, end_time, options,
|
return await _handler(media, product_grid_list, product_list, start_time, end_time, options,
|
||||||
sentry_trace, retry_time, scale)
|
sentry_trace, retry_time, scale)
|
||||||
|
|||||||
Reference in New Issue
Block a user