2121 "OVERVIEW_CONTEXT" ,
2222 "OVERVIEW_QUESTION" ,
2323 "SYSTEM_CONTEXT" ,
24+ "TILE_CONTEXT" ,
2425 "TILE_QUESTION" ,
26+ "TILE_QUESTION_PANELS" ,
2527 "caption_tiles" ,
2628 "encode" ,
2729 "require_captioner" ,
2830 "takes_pictures" ,
31+ "takes_several_pictures" ,
2932]
3033
3134logger = log .get_logger ("sampling.tile_captions" )
3235
3336#: The rules every tile caption is written under.
3437DEFAULT_SYSTEM_PROMPT = (
3538 "You write the prompt an image model uses to repaint a crop of a larger picture.\n "
36- "- Look only at the attached crop. Name each subject and object visible in it, largest "
37- "first, including any only partly in frame, and say which part of it shows. Then describe "
38- "their surfaces, textures, colours and lighting, and any legible text in double quotes.\n "
39- "- The overview describes the whole picture, and most of what it names lies outside this "
40- "crop. Use it only to name and style what the crop shows. Never name anything that is not "
41- "visible in the crop.\n "
39+ "- You are shown the whole picture with its tiles ruled and numbered and the crop's tile "
40+ "outlined in yellow, and the crop itself at full detail. Describe only what lies inside "
41+ "the crop.\n "
42+ "- Name each subject and object inside the crop, largest first, including any only partly "
43+ "in frame. For each, say which part of it shows, where that part sits within the frame and "
44+ "how much of the frame it fills. For a person or animal only partly in the crop, name "
45+ "exactly which part of the body shows and whose it is, as the whole picture shows it. Then "
46+ "describe their surfaces, textures, colours and lighting, and any legible text in double "
47+ "quotes.\n "
48+ "- Use the whole picture and the overview only to recognise and name what the crop shows. "
49+ "Most of what they show lies outside the crop. Never name anything that is not inside it.\n "
4250 "- Keep the medium and style of the whole picture.\n "
4351 "- Where the crop holds only empty background, sky, water or a plain surface, name that "
4452 "surface and its colour in a few words.\n "
4553 "- Describe what is there directly. Write one plain paragraph of at most 60 words, with no "
46- "preamble and no list. Never write the words tile, crop, section, corner, overview , "
47- "attached or image, and never say where in the picture the crop sits ."
54+ "preamble and no list. Never write the words tile, crop, overview, outlined, numbered , "
55+ "attached or image, and never say where the crop sits in the whole picture ."
4856)
4957
5058#: What the model is asked about the whole picture.
6068 "Call things what the prompt calls them.\n \n "
6169)
6270
71+ #: What precedes each tile question where the picture's own prompt is known; ``{context}`` is
72+ #: filled in.
73+ TILE_CONTEXT = "The whole picture was made from this prompt, given for names only: {context}\n \n "
74+
6375#: What is added to the system prompt where the picture's own prompt is known.
6476SYSTEM_CONTEXT = "\n \n The original prompt and context of the image was:\n \n {context}"
6577
66- #: What the model is asked about each tile; ``{overview}`` and ``{place}`` are filled in.
78+ #: What the model is asked about each tile, shown as two pictures; ``{overview}``,
79+ #: ``{number}`` and ``{place}`` are filled in.
6780TILE_QUESTION = (
68- "Overview of the whole picture: {overview}\n \n "
69- "The attached picture is a crop of that picture ({place}). Write the prompt for everything "
70- "visible in the attached picture."
81+ "Overview of the whole picture, given for names only: {overview}\n \n "
82+ "The first attached picture is the whole picture with its tiles ruled and numbered; tile "
83+ "{number} ({place}) is outlined in yellow. The second is tile {number} at full detail.\n \n "
84+ "Write the prompt for the second picture only. Start with its largest subject. For "
85+ "everything cut off by an edge of the second picture, say which edge it touches and which "
86+ "part of it shows. Leave out anything outside tile {number}, even where the overview or "
87+ "the prompt names it, and copy no sentence from either."
88+ )
89+
90+ #: What the model is asked about each tile, shown as one picture of two panels.
91+ TILE_QUESTION_PANELS = (
92+ "Overview of the whole picture, given for names only: {overview}\n \n "
93+ "The attached picture has two panels. On the left is the whole picture with its tiles "
94+ "ruled and numbered, tile {number} ({place}) outlined in yellow; on the right is tile "
95+ "{number} at full detail.\n \n "
96+ "Write the prompt for the right panel only. Start with its largest subject. For "
97+ "everything cut off by an edge of the right panel, say which edge it touches and which "
98+ "part of it shows. Leave out anything outside tile {number}, even where the overview or "
99+ "the prompt names it, and copy no sentence from either."
71100)
72101
73102#: Longest side in pixels of the picture the overview is written from.
76105#: Longest side in pixels of the picture a tile caption is written from.
77106TILE_SIDE = 768
78107
108+ #: Longest side in pixels of the whole picture shown beside each tile.
109+ CONTEXT_SIDE = 768
110+
79111#: Most tokens an overview may run to.
80112OVERVIEW_TOKENS = 200
81113
@@ -117,6 +149,22 @@ def require_captioner(captioner, clip, node: str) -> None:
117149 )
118150
119151
152+ def _flat (tokens ) -> list :
153+ """Every entry of a tokenizer's answer, in order."""
154+ batches = tokens .values () if isinstance (tokens , dict ) else [tokens ]
155+ return [item for batch in batches for row in batch for item in row ]
156+
157+
158+ def _pictures_in (tokens ) -> int :
159+ """How many entries of a tokenizer's answer carry a picture rather than a token id."""
160+ count = 0
161+ for item in _flat (tokens ):
162+ head = item [0 ] if isinstance (item , tuple ) and item else item
163+ if isinstance (head , dict ):
164+ count += 1
165+ return count
166+
167+
120168def takes_pictures (clip ) -> bool :
121169 """Whether a CLIP's tokenizer turns a picture into tokens.
122170
@@ -126,16 +174,38 @@ def takes_pictures(clip) -> bool:
126174 Returns:
127175 True where tokenizing with a picture answers different tokens than without one.
128176 """
129- def flat (tokens ):
130- batches = tokens .values () if isinstance (tokens , dict ) else [tokens ]
131- return [item for batch in batches for row in batch for item in row ]
177+ try :
178+ plain = clip .tokenize ("probe" , min_length = 1 )
179+ seen = clip .tokenize ("probe" , image = torch .zeros (1 , 64 , 64 , 3 ), min_length = 1 )
180+ except Exception :
181+ return False
182+ return len (_flat (seen )) != len (_flat (plain )) or _pictures_in (seen ) > 0
183+
184+
185+ def takes_several_pictures (clip ) -> bool :
186+ """Whether a CLIP's tokenizer takes two pictures into one question.
187+
188+ Args:
189+ clip: A ComfyUI ``CLIP``.
132190
191+ Returns:
192+ True where two pictures handed together both arrive in the tokens.
193+ """
133194 try :
134- plain = flat ( clip . tokenize ( "probe" , min_length = 1 ) )
135- seen = flat (clip .tokenize ("probe" , image = torch . zeros ( 1 , 64 , 64 , 3 ), min_length = 1 ))
195+ probe = torch . zeros ( 1 , 64 , 64 , 3 )
196+ return _pictures_in (clip .tokenize ("probe" , images = [ probe , probe ], min_length = 1 )) == 2
136197 except Exception :
137198 return False
138- return len (seen ) != len (plain ) or any (not isinstance (item , (int , tuple )) for item in seen )
199+
200+
201+ def _panels (context : torch .Tensor , crop : torch .Tensor ) -> torch .Tensor :
202+ """The context picture and the crop side by side at one height, a dark gap between them."""
203+ height = int (context .shape [1 ])
204+ width = max (1 , round (crop .shape [2 ] * height / max (1 , crop .shape [1 ])))
205+ resized = torch .nn .functional .interpolate (crop .movedim (- 1 , 1 ), size = (height , width ), mode = "bicubic" ,
206+ align_corners = False , antialias = True ).movedim (1 , - 1 )
207+ gap = torch .zeros (1 , height , max (4 , height // 64 ), 3 )
208+ return torch .cat ([context , gap , resized .clamp (0.0 , 1.0 )], dim = 2 )
139209
140210
141211def _fit (picture : torch .Tensor , side : int ) -> torch .Tensor :
@@ -181,14 +251,19 @@ def _tidy(text: str) -> str:
181251 return reply
182252
183253
184- def _ask (write , store : OrderedDict , picture : torch . Tensor , question : str , system : str ,
254+ def _ask (write , store : OrderedDict , pictures : list , question : str , system : str ,
185255 budget : int , ruled : bool ) -> str :
186- """One answer about a picture, read from the cache where the same question was asked before."""
187- key = (_digest (picture ), question , system if ruled else "" , int (budget ))
256+ """One answer about pictures, read from the cache where the same question was asked before."""
257+ key = (tuple (_digest (picture ) for picture in pictures ), question , system if ruled else "" ,
258+ int (budget ))
188259 if key in store :
189260 store .move_to_end (key )
190261 return store [key ]
191- answer = _tidy (write (question , image = picture , max_length = budget , ruled = ruled ))
262+ if len (pictures ) > 1 :
263+ reply = write (question , images = pictures , max_length = budget , ruled = ruled )
264+ else :
265+ reply = write (question , image = pictures [0 ], max_length = budget , ruled = ruled )
266+ answer = _tidy (reply )
192267 store [key ] = answer
193268 while len (store ) > MOST_CACHED :
194269 store .popitem (last = False )
@@ -223,25 +298,30 @@ def caption_tiles(captioner, image: torch.Tensor, grid: tiles.Grid, system_promp
223298 write = h3_writer .make_writer (captioner , 0 , SAMPLING , system )
224299 store = _store (captioner )
225300 lead = OVERVIEW_CONTEXT .format (context = context ) if context else ""
301+ tile_lead = TILE_CONTEXT .format (context = context ) if context else ""
226302 if announce is not None :
227303 announce ("overview" )
228- overview = _ask (write , store , _fit (image , OVERVIEW_SIDE ), lead + OVERVIEW_QUESTION , system ,
304+ overview = _ask (write , store , [ _fit (image , OVERVIEW_SIDE )] , lead + OVERVIEW_QUESTION , system ,
229305 OVERVIEW_TOKENS , ruled = False )
230306 if progress is not None :
231307 progress .update (1 )
232308 height , width = int (image .shape [1 ]), int (image .shape [2 ])
309+ several = takes_several_pictures (captioner )
310+ whole = _fit (image , CONTEXT_SIDE )
233311 captions = []
234312 for index , (h0 , h1 , w0 , w1 ) in enumerate (grid .boxes ()):
235313 comfy .model_management .throw_exception_if_processing_interrupted ()
236314 if announce is not None :
237315 announce ("caption" , [index ])
238316 top , bottom = int (round (h0 * grid .scale )), min (height , int (round (h1 * grid .scale )))
239317 left , right = int (round (w0 * grid .scale )), min (width , int (round (w1 * grid .scale )))
240- crop = image [:, top :max (bottom , top + 1 ), left :max (right , left + 1 )]
241- question = lead + TILE_QUESTION .format (overview = overview or "not available" ,
242- place = tiles .position (grid , index ))
243- captions .append (_ask (write , store , _fit (crop , TILE_SIDE ), question , system , TILE_TOKENS ,
244- ruled = True ))
318+ crop = _fit (image [:, top :max (bottom , top + 1 ), left :max (right , left + 1 )], TILE_SIDE )
319+ context_picture = tiles .numbered (whole , grid , index , CONTEXT_SIDE )
320+ template = TILE_QUESTION if several else TILE_QUESTION_PANELS
321+ pictures = [context_picture , crop ] if several else [_panels (context_picture , crop )]
322+ question = tile_lead + template .format (overview = overview or "not available" ,
323+ number = index + 1 , place = tiles .position (grid , index ))
324+ captions .append (_ask (write , store , pictures , question , system , TILE_TOKENS , ruled = True ))
245325 if progress is not None :
246326 progress .update (1 )
247327 return overview , captions
0 commit comments