163 lines
10 KiB
Python
163 lines
10 KiB
Python
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()
|