Files
sansyn-pc-extension/tests/test_image_user_intent.py
T

163 lines
10 KiB
Python
Raw Normal View History

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()