feat: 同步 V9.3 生图工作流、用户意图解析和参考图一致性
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'server'))
|
||||
import server as app
|
||||
|
||||
|
||||
class ImageUserIntentTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
app.IMAGE_INTENT_CACHE.clear()
|
||||
self.report={'facts':{'product':{'title':'被子','category':'床品'},'attributes':{'尺寸':'200x230'}}}
|
||||
self.plan={'name':'方案内部名称','productAction':'舒适睡眠','pageAction':'保持信息层级'}
|
||||
self.slots=[{'assetType':'main','index':i,'task':['默认槽位任务'],
|
||||
'referenceTextInfo':'被子尺寸200x230',
|
||||
'referenceAnalysis':{'theme':'舒适支撑','composition':'上方文字、下方商品','imageObservation':{'textLayout':'左对齐'}}}
|
||||
for i in (1,2)]
|
||||
self.response={'productUpdates':[{'field':'category','value':'枕头','sourceText':'品类换成枕头'}],
|
||||
'globalInstructions':['整套产品换成枕头,保留舒适睡眠方案方向'],
|
||||
'perSlot':{f'main:{i}':{'instructions':['文案随枕头调整'], 'theme':'枕头舒适支撑',
|
||||
'display':'保持参考构图,枕头不执行被子折叠动作',
|
||||
'copy':'生成枕头支撑文案,保持参考文字位置大小,不沿用被子尺寸'} for i in (1,2)}}
|
||||
|
||||
def resolve(self,text='品类换成枕头,文案跟随改变',edits=None):
|
||||
return app._resolve_image_user_intent(self.report,self.plan,self.slots,app._image_user_direction(text),edits or {},[],True)
|
||||
|
||||
def test_empty_input_uses_plan_without_analysis(self):
|
||||
direction=app._image_user_direction('')
|
||||
with patch.object(app,'chat_json') as model:
|
||||
self.assertIs(direction,app._resolve_image_user_intent(self.report,self.plan,self.slots,direction,{},[],True))
|
||||
model.assert_not_called()
|
||||
prompt=app.build_image_prompt(self.report,self.plan,slot=self.slots[0],user_direction=direction)
|
||||
self.assertIn('舒适睡眠',prompt)
|
||||
self.assertNotIn('统一产品—方案—参考关系',prompt)
|
||||
|
||||
def test_category_copy_and_reference_are_resolved_for_every_image(self):
|
||||
original=copy.deepcopy(self.report)
|
||||
with patch.object(app,'chat_json',return_value=self.response) as model:
|
||||
direction=self.resolve()
|
||||
request=json.loads(model.call_args.args[1])
|
||||
self.assertEqual(self.plan,request['plan'])
|
||||
self.assertEqual(self.slots[0]['referenceAnalysis'],request['images'][0]['referenceAnalysis'])
|
||||
lock=app.build_identity_lock(self.report,self.plan,direction,True)
|
||||
self.assertEqual('枕头',lock['productName'])
|
||||
for slot in self.slots:
|
||||
per_slot=app._user_direction_for_slot(direction,slot)
|
||||
prompt=app.build_image_prompt(self.report,self.plan,slot=slot,user_direction=per_slot,
|
||||
identity_lock=lock,product_reference_mode='uploaded_identity_collected_reference',match_reference_shooting=True)
|
||||
self.assertIn('当前品类以用户要求为准:枕头',prompt)
|
||||
self.assertIn('舒适睡眠',prompt)
|
||||
self.assertIn('生成枕头支撑文案',prompt)
|
||||
self.assertIn('不沿用被子尺寸',prompt)
|
||||
self.assertNotIn('商品外观完全由第1张上传产品图决定',prompt)
|
||||
self.assertNotIn('商品品类完全一致',prompt)
|
||||
self.assertEqual(original,self.report)
|
||||
|
||||
def test_preview_generation_share_result_but_changed_input_is_reinterpreted(self):
|
||||
with patch.object(app,'chat_json',return_value=self.response) as model:
|
||||
preview=self.resolve()
|
||||
execution=self.resolve()
|
||||
self.assertEqual(preview,execution)
|
||||
model.assert_called_once()
|
||||
self.resolve('品类换成枕头,标题改为舒适支撑')
|
||||
self.assertEqual(2,model.call_count)
|
||||
|
||||
def test_long_global_and_per_image_input_arrives_complete(self):
|
||||
text='品类换成枕头;'+('要求保持每张参考图的文字位置并调整文案;'*100)
|
||||
edit='标题写成舒适支撑;'+('保留文字层级和完整商品展示;'*600)
|
||||
with patch.object(app,'chat_json',return_value=self.response) as model:
|
||||
self.resolve(text,{'main:1':edit})
|
||||
request=json.loads(model.call_args.args[1])
|
||||
self.assertEqual(text,request['userInput'])
|
||||
self.assertEqual(edit,request['perImageInput']['main:1'])
|
||||
self.assertEqual(text,app._image_user_direction(text)['raw'])
|
||||
|
||||
def test_per_image_visual_edit_is_not_promoted_to_other_images(self):
|
||||
response=copy.deepcopy(self.response)
|
||||
response['productUpdates']=[]
|
||||
response['globalInstructions']=[]
|
||||
response['perSlot']['main:1']['instructions']=['标题写成舒适支撑']
|
||||
response['perSlot']['main:2']['instructions']=[]
|
||||
with patch.object(app,'chat_json',return_value=response):
|
||||
direction=self.resolve('',{'main:1':'标题写成舒适支撑'})
|
||||
self.assertEqual(['标题写成舒适支撑'],app._user_direction_for_slot(direction,self.slots[0])['accepted'])
|
||||
self.assertEqual([],app._user_direction_for_slot(direction,self.slots[1])['accepted'])
|
||||
base='已经完成统一判断的提示词'
|
||||
result,_=app.merge_image_prompt_with_user_edit(base,'标题写成舒适支撑',self.slots[0],direction)
|
||||
self.assertEqual(base,result)
|
||||
|
||||
def test_product_change_in_single_image_input_is_shared(self):
|
||||
with patch.object(app,'chat_json',return_value=self.response):
|
||||
direction=self.resolve('',{'main:1':'品类换成枕头'})
|
||||
for slot in self.slots:
|
||||
self.assertEqual('枕头',app._user_direction_for_slot(direction,slot)['productUpdates'][0]['value'])
|
||||
|
||||
def test_missing_image_or_invented_update_preserves_input_without_adopting_invention(self):
|
||||
for invalid in ('missing_slot','invented_update'):
|
||||
response=copy.deepcopy(self.response)
|
||||
if invalid=='missing_slot':del response['perSlot']['main:2']
|
||||
else:response['productUpdates'][0]['sourceText']='用户没有要求的新品'
|
||||
with self.subTest(invalid=invalid),patch.object(app,'chat_json',return_value=response):
|
||||
result=self.resolve()
|
||||
self.assertEqual('品类换成枕头,文案跟随改变',result['raw'])
|
||||
self.assertIn('main:2',result['resolvedBySlot'])
|
||||
if invalid=='invented_update':self.assertEqual([],result['productUpdates'])
|
||||
app.IMAGE_INTENT_CACHE.clear()
|
||||
|
||||
def test_expression_request_and_analysis_failure_never_block_generation_prompt(self):
|
||||
for response in ({'productUpdates':[{'field':'text','value':'无字','sourceText':'图片上不要有文字'}]},None):
|
||||
app.IMAGE_INTENT_CACHE.clear()
|
||||
with patch.object(app,'chat_json',return_value=response):
|
||||
direction=self.resolve('图片上不要有文字')
|
||||
slot=self.slots[0]
|
||||
per_slot=app._user_direction_for_slot(direction,slot)
|
||||
prompt=app.build_image_prompt(self.report,self.plan,slot=slot,user_direction=per_slot)
|
||||
self.assertIn('图片上不要有文字',prompt)
|
||||
self.assertNotIn('生成并真实绘制当前产品中文文案',prompt)
|
||||
self.assertNotIn('Render Chinese copy',prompt)
|
||||
self.assertLess(prompt.index('本图用户完整输入'),prompt.index('对应参考图职责'))
|
||||
app.IMAGE_INTENT_CACHE.clear()
|
||||
with patch.object(app,'chat_json',side_effect=RuntimeError('分析接口不可用')):
|
||||
direction=self.resolve('背景换成室内,主体不变')
|
||||
self.assertEqual('背景换成室内,主体不变',direction['raw'])
|
||||
self.assertEqual('分析接口不可用',direction['analysisRecord']['error'])
|
||||
|
||||
def test_rejected_identity_inference_cannot_leak_into_reference_display(self):
|
||||
response=copy.deepcopy(self.response)
|
||||
response['productUpdates']=[{'field':'category','value':'四件套','sourceText':'图片是床品套装'}]
|
||||
response['perSlot']['main:1']['display']='被套和枕套铺展在床上'
|
||||
response['globalInstructions']=['保留床铺场景']
|
||||
self.slots[0]['referenceAnalysis']['composition']='折叠商品居中,背景纯色'
|
||||
with patch.object(app,'chat_json',return_value=response):
|
||||
direction=self.resolve('图片上不要有文字')
|
||||
per_slot=app._user_direction_for_slot(direction,self.slots[0])
|
||||
prompt=app.build_image_prompt(self.report,self.plan,slot=self.slots[0],user_direction=per_slot,match_reference_shooting=True)
|
||||
self.assertIn('折叠商品居中,背景纯色',prompt)
|
||||
self.assertIn('图片上不要有文字',prompt)
|
||||
self.assertNotIn('被套和枕套铺展在床上',prompt)
|
||||
self.assertNotIn('保留床铺场景',prompt)
|
||||
self.assertEqual(response,direction['analysisRecord']['response'])
|
||||
self.assertLess(prompt.index('对应图展示状态执行依据'),prompt.index('辅助解析资料'))
|
||||
|
||||
def test_spatial_relationships_are_shared_by_default_and_user_paths(self):
|
||||
self.slots[0]['referenceAnalysis']['displayRelations']={
|
||||
'visibleParts':[{'id':'a','description':'悬挂主体'},{'id':'b','description':'支撑位置'}],
|
||||
'relations':[{'type':'接触','from':'a','to':'b','observation':'顶部接触支撑,右侧遮挡'}],
|
||||
'uncertainAreas':['背面连接不可见']}
|
||||
with patch.object(app,'chat_json',return_value={}):
|
||||
direction=self.resolve('保持产品,调整文字')
|
||||
for d in (app._image_user_direction(''),app._user_direction_for_slot(direction,self.slots[0])):
|
||||
prompt=app.build_image_prompt(self.report,self.plan,slot=self.slots[0],user_direction=d,match_reference_shooting=True)
|
||||
self.assertIn('顶部接触支撑,右侧遮挡',prompt)
|
||||
self.assertIn('背面连接不可见',prompt)
|
||||
self.assertIn('可见层数不等于商品件数',prompt)
|
||||
disabled=app.build_image_prompt(self.report,self.plan,slot=self.slots[0],match_reference_shooting=False)
|
||||
self.assertNotIn('可见展示空间关系|对应原图约束',disabled)
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
Reference in New Issue
Block a user