Parse-Then-Place generates layouts through a two-stage transformers pipeline: a semantic parser first extracts layout intent from text, then a placement model predicts element positions. Converted checkpoints include parser and placement subfolders for RICO and Web variants. Public outputs use normalized center xywh boxes in [0, 1], dataset-local or request-local integer labels, a valid-element mask, and id2label.
Developed by: Jiawei Lin et al.
Shared by: creative-graphic-design.
Model type: content-agnostic; task: single-task; conditioning: text.
Use this package for research inference, conversion checks, and agreement checks against the original implementation for generated layouts.
The pipeline owns the two-stage text-to-layout workflow: semantic parser -> placement constraint -> placement model -> common LayoutGenerationOutput. Full checkpoint inference requires converted semantic_parser/ and placement/ subfolders containing standard AutoModelForSeq2SeqLM weights.
layout_text is an offline parser path for pre-generated placement strings; when it is supplied, the pipeline does not use prompt, the semantic parser, or the placement model. Use prompt without layout_text for the full two-stage checkpoint path.
Do not treat generated layouts as production accessibility annotations, OCR output, semantic scene understanding, or license-cleared design assets without separate review.
The converted behavior follows the upstream checkpoints, prompt fixtures, and datasets. Dataset coverage, label vocabularies, and layout quality inherit the limits of those sources.
Clone this repository, install the workspace member, and run the download and conversion steps in REPRODUCING.md. Those steps create .cache/parse-then-place/converted/rico-finetune.
The layout_text token format is label left top width height in discrete ltwh coordinates on the dataset canvas. RICO uses the checkpoint-local label mapping and returns normalized center xywh boxes publicly.
fromparse_then_placeimportParseThenPlacePipelinepath=".cache/parse-then-place/converted/rico-finetune"# After Hub publication: from_pretrained("creative-graphic-design/parse-then-place-rico-finetune")pipe=ParseThenPlacePipeline.from_pretrained(path,local_files_only=True)out=pipe(prompt="create a screen with text")print(out.bbox.shape,out.labels.tolist())
Datasets are RICO text-to-layout and WebUI as distributed by the original repository. WebUI is not yet mirrored under the creative-graphic-design org, so conversion scripts accept the original asset tree as the interim source.
Inputs and outputs are normalized to the public layout schema at package boundaries. Boxes, tokens, prompts, or analog bits that follow the original implementation stay inside package adapters and fixtures used for agreement checks.
Agreement checks use local-only generated fixtures and converted checkpoint directories. Large generated tensors, images, weights, and downloaded artifacts are not committed.
The current agreement test loads the original stage-2 placement checkpoint through the converted package wrapper and compares it with references produced by a direct transformers generation call using hand-copied original generation settings; it does not execute the original trainer path or the converted checkpoint artifact.
See REPRODUCING.md for the commands that download original-implementation assets, generate reference outputs, run agreement checks, convert checkpoints, and smoke-test local loading.
No new model training is performed by these conversion packages. Conversion and agreement-check costs depend on the selected checkpoint and local hardware.
Parse-Then-Place runs two seq2seq stages: semantic_parser/ turns text into layout intent, then placement/ predicts element positions. Converted directories keep those subfolders separate so parser and placement weights can be loaded and smoke-tested independently.
Use uv run --package parse-then-place ... from the repository root so workspace dependency sources and extras resolve correctly.
The original adapter/deepspeed training stack uses legacy pins (adapter_transformers==3.0.1, deepspeed==0.5.10, wandb==0.12.10) that conflict with this workspace's modern Hugging Face stack. Keep those pins in a separate original-implementation environment when flattening stage-1 adapter weights; the published runtime artifact should be standard transformers weights.
@InProceedings{lin2023parse,title={A Parse-Then-Place Approach for Generating Graphic Layouts from Textual Descriptions},author={Lin, Jiawei and Guo, Jiaqi and Sun, Shizhao and Xu, Weijiang and Liu, Ting and Lou, Jian-Guang and Zhang, Dongmei},booktitle={Proceedings of the IEEE/CVF International Conference on Computer Vision (ICCV)},year={2023}}
classParseThenPlaceConfig(PretrainedConfig):"""Stores Parse-Then-Place dataset and generation defaults."""model_type="parse-then-place"def__init__(self,dataset_name:str="rico",stage2_mode:Stage2Mode|str=Stage2Mode.finetune,parser_model_name:str="google/t5-v1_1-base",parser_generation_max_length:int=600,placement_generation_max_length:int=500,temperature:float=0.7,num_return_sequences:int=5,canvas_size:tuple[int,int]|list[int]|None=None,id2label:dict[int|str,str]|None=None,parser_subfolder:str="semantic_parser",placement_subfolder:str="placement",pad_token_id:int=0,eos_token_id:int=1,decoder_start_token_id:int=0,is_encoder_decoder:bool=True,transformers_version:str|None=None,architectures:list[str]|None=None,output_hidden_states:bool|None=False,return_dict:bool|None=True,dtype:str|None=None,torch_dtype:str|None=None,chunk_size_feed_forward:int=0,problem_type:Literal["regression","single_label_classification","multi_label_classification"]|None=None,name_or_path:str="",_commit_hash:str|None=None,attn_implementation:str|None=None,**kwargs:str|int|float|bool|None,)->None:"""Initialize the composite checkpoint configuration."""dataset=normalize_dataset_name(dataset_name)mode=normalize_stage2_mode(stage2_mode)self.dataset_name=str(dataset)self.stage2_mode=str(mode)self.parser_model_name=parser_model_nameself.parser_generation_max_length=parser_generation_max_lengthself.placement_generation_max_length=placement_generation_max_lengthself.temperature=temperatureself.num_return_sequences=num_return_sequencesself.canvas_size=tuple(canvas_sizeorcanvas_size_for_dataset(dataset))self.parser_subfolder=parser_subfolderself.placement_subfolder=placement_subfolderlabel_map=id2labelorid2label_for_dataset(dataset)normalized_id2label={int(key):str(value)forkey,valueinlabel_map.items()}label2id={value:keyforkey,valueinnormalized_id2label.items()}_=kwargs.pop("id2label",None)_=kwargs.pop("label2id",None)super().__init__(transformers_version=transformers_version,architectures=architectures,output_hidden_states=output_hidden_states,return_dict=return_dict,dtype=dtypeortorch_dtype,chunk_size_feed_forward=chunk_size_feed_forward,is_encoder_decoder=is_encoder_decoder,id2label=normalized_id2label,label2id=label2id,problem_type=problem_type,)# Transformers v5 keeps model-specific token fields on the subclass;# only common configuration fields belong in the base call.self.pad_token_id=pad_token_idself.eos_token_id=eos_token_idself.decoder_start_token_id=decoder_start_token_idself.name_or_path=name_or_pathself._commit_hash=_commit_hashself._attn_implementation=attn_implementationforkey,valueinkwargs.items():setattr(self,key,value)
def__init__(self,dataset_name:str="rico",stage2_mode:Stage2Mode|str=Stage2Mode.finetune,parser_model_name:str="google/t5-v1_1-base",parser_generation_max_length:int=600,placement_generation_max_length:int=500,temperature:float=0.7,num_return_sequences:int=5,canvas_size:tuple[int,int]|list[int]|None=None,id2label:dict[int|str,str]|None=None,parser_subfolder:str="semantic_parser",placement_subfolder:str="placement",pad_token_id:int=0,eos_token_id:int=1,decoder_start_token_id:int=0,is_encoder_decoder:bool=True,transformers_version:str|None=None,architectures:list[str]|None=None,output_hidden_states:bool|None=False,return_dict:bool|None=True,dtype:str|None=None,torch_dtype:str|None=None,chunk_size_feed_forward:int=0,problem_type:Literal["regression","single_label_classification","multi_label_classification"]|None=None,name_or_path:str="",_commit_hash:str|None=None,attn_implementation:str|None=None,**kwargs:str|int|float|bool|None,)->None:"""Initialize the composite checkpoint configuration."""dataset=normalize_dataset_name(dataset_name)mode=normalize_stage2_mode(stage2_mode)self.dataset_name=str(dataset)self.stage2_mode=str(mode)self.parser_model_name=parser_model_nameself.parser_generation_max_length=parser_generation_max_lengthself.placement_generation_max_length=placement_generation_max_lengthself.temperature=temperatureself.num_return_sequences=num_return_sequencesself.canvas_size=tuple(canvas_sizeorcanvas_size_for_dataset(dataset))self.parser_subfolder=parser_subfolderself.placement_subfolder=placement_subfolderlabel_map=id2labelorid2label_for_dataset(dataset)normalized_id2label={int(key):str(value)forkey,valueinlabel_map.items()}label2id={value:keyforkey,valueinnormalized_id2label.items()}_=kwargs.pop("id2label",None)_=kwargs.pop("label2id",None)super().__init__(transformers_version=transformers_version,architectures=architectures,output_hidden_states=output_hidden_states,return_dict=return_dict,dtype=dtypeortorch_dtype,chunk_size_feed_forward=chunk_size_feed_forward,is_encoder_decoder=is_encoder_decoder,id2label=normalized_id2label,label2id=label2id,problem_type=problem_type,)# Transformers v5 keeps model-specific token fields on the subclass;# only common configuration fields belong in the base call.self.pad_token_id=pad_token_idself.eos_token_id=eos_token_idself.decoder_start_token_id=decoder_start_token_idself.name_or_path=name_or_pathself._commit_hash=_commit_hashself._attn_implementation=attn_implementationforkey,valueinkwargs.items():setattr(self,key,value)
classParseThenPlacePipeline(LayoutGenerationPipeline):"""Compose standard seq2seq parser and placement models."""config_class:ClassVar[type[PretrainedConfig]]=ParseThenPlaceConfigcomponent_specs:ClassVar[dict[str,PipelineComponentSpec]]={"parser":PipelineComponentSpec(attribute_name="parser",loader=_load_seq2seq_component,config_subfolder_attribute="parser_subfolder",required=False,),"placement":PipelineComponentSpec(attribute_name="placement",loader=_load_seq2seq_component,config_subfolder_attribute="placement_subfolder",),"processor":PipelineComponentSpec(attribute_name="processor",loader=_load_processor_component,marker_file="processor_config.json",save_with_is_main_process=False,),}config:ParseThenPlaceConfigparser:PreTrainedModel|Noneplacement:PreTrainedModel|Noneprocessor:ParseThenPlaceProcessordef__init__(self,config:ParseThenPlaceConfig,processor:ParseThenPlaceProcessor,*,parser:PreTrainedModel|None=None,placement:PreTrainedModel|None=None,)->None:"""Initialize the composite pipeline."""super().__init__(config)self.config=configself.processor=processorself.parser=parserself.placement=placement@classmethoddeffrom_pretrained(cls,pretrained_model_name_or_path:str|Path,*,parser:PreTrainedModel|None=None,placement:PreTrainedModel|None=None,processor:ParseThenPlaceProcessor|None=None,local_files_only:bool=False,config:ParseThenPlaceConfig|PretrainedConfig|None=None,)->ParseThenPlacePipeline:# ty: ignore[invalid-method-override]"""Load a composite pipeline from a root directory."""components:dict[str,PreTrainedModel|ParseThenPlaceProcessor]={}ifparserisnotNone:components["parser"]=parserifplacementisnotNone:components["placement"]=placementifprocessorisNone:loaded=super().from_pretrained(pretrained_model_name_or_path,local_files_only=local_files_only,config=config,components=components,)returncast(ParseThenPlacePipeline,loaded)components["processor"]=processorloaded=super().from_pretrained(pretrained_model_name_or_path,local_files_only=local_files_only,config=config,components=components,)returncast(ParseThenPlacePipeline,loaded)@classmethoddef_from_pretrained_components(cls,*,config:PretrainedConfig,components:Mapping[str,PreTrainedModel|ParseThenPlaceProcessor|None],)->ParseThenPlacePipeline:"""Build a pipeline from loaded config and components."""returncls(config=cast(ParseThenPlaceConfig,config),processor=cast(ParseThenPlaceProcessor,components["processor"]),parser=cast(PreTrainedModel|None,components.get("parser")),placement=cast(PreTrainedModel|None,components["placement"]),)@torch.no_grad()defparse(self,input_ids:Int[torch.Tensor,"batch tokens"],attention_mask:Bool[torch.Tensor,"batch tokens"]|None=None,*,generation_max_length:int|None=None,**generate_kwargs:str|int|float|bool|torch.Generator|None,)->Int[torch.Tensor,"batch tokens"]:"""Generate logical-form token ids with the parser stage."""ifself.parserisNone:raiseValueError("Parser stage is not loaded")generated=cast(_GenerationModel,self.parser).generate(input_ids=input_ids,attention_mask=attention_mask,max_length=generation_max_lengthorself.config.parser_generation_max_length,**generate_kwargs,)returngenerated@torch.no_grad()defplace(self,input_ids:Int[torch.Tensor,"batch tokens"],attention_mask:Bool[torch.Tensor,"batch tokens"]|None=None,*,generation_max_length:int|None=None,num_return_sequences:int|None=None,temperature:float|None=None,do_sample:bool=True,generator:torch.Generator|None=None,**generate_kwargs:str|float|bool|torch.Generator|None,)->Int[torch.Tensor,"batch tokens"]:"""Generate layout token ids with the placement stage."""ifself.placementisNone:raiseValueError("Placement stage is not loaded")ifgeneratorisnotNone:generate_kwargs["generator"]=generatorgenerated=cast(_GenerationModel,self.placement).generate(input_ids=input_ids,attention_mask=attention_mask,max_length=generation_max_lengthorself.config.placement_generation_max_length,num_return_sequences=num_return_sequencesorself.config.num_return_sequences,temperature=temperatureorself.config.temperature,do_sample=do_sample,**generate_kwargs,)returngenerateddef__call__(self,*,prompt:str|Sequence[str]|None=None,batch_size:int=1,seed:int|None=None,generator:torch.Generator|None=None,condition_type:ConditionType|str=ConditionType.text,labels:Int[torch.Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,bbox:Float[torch.Tensor,"batch elements 4"]|list[ArrayLikeInput]|None=None,mask:Bool[torch.Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,num_elements:int|list[int]|Int[torch.Tensor,"batch"]|None=None,box_format:BoxFormat|str=BoxFormat.xywh,normalized:bool=True,canvas_size:tuple[int,int]|None=None,num_inference_steps:int|None=None,num_return_sequences:int|None=None,temperature:float|None=None,output_candidate:Literal["first","all","best"]="first",output_type:Literal["dataclass","dict"]="dataclass",return_intermediates:bool=False,layout_text:str|list[str]|list[list[str]]|None=None,)->LayoutGenerationOutput|ParseThenPlaceOutputDict:# ty: ignore[invalid-method-override]"""Generate a layout from natural-language text."""_=(batch_size,labels,bbox,mask,num_elements,box_format,normalized,canvas_size,num_inference_steps,)condition=normalize_condition_type(condition_type)ifconditionisnotConditionType.text:raiseNotImplementedError("Parse-Then-Place only supports condition_type='text'")iflayout_textisnotNone:layout_items=([layout_text]ifisinstance(layout_text,str)elselayout_text)returnself.processor.layout_text_to_output(layout_items,output_candidate=output_candidate,output_type=output_type,return_intermediates=return_intermediates,)ifpromptisNone:raiseValueError("prompt is required for Parse-Then-Place generation")generation_generator=self.prepare_generator(generator=generator,seed=seed,)prompts=[prompt]ifisinstance(prompt,str)elselist(prompt)parser_inputs=self.processor(prompts)if"input_ids"notinparser_inputs:raiseValueError("processor requires parser_tokenizer for model inference")parser_ids=self.parse(parser_inputs["input_ids"],attention_mask=parser_inputs.get("attention_mask"),generation_max_length=self.config.parser_generation_max_length,)value_maps=cast(list[dict[str,str]|None],parser_inputs.get("value_maps"))logical_forms=self.processor.postprocess_ir(parser_ids,value_maps=value_maps,)placement_inputs=self.processor.ir_to_placement_inputs(logical_forms)placement_encoded=self.processor.encode_placement_inputs(placement_inputs)if"input_ids"notinplacement_encoded:raiseValueError("processor requires placement_tokenizer for model inference")return_sequences=num_return_sequencesorself.config.num_return_sequencesplacement_ids=self.place(placement_encoded["input_ids"],attention_mask=placement_encoded.get("attention_mask"),num_return_sequences=return_sequences,temperature=temperature,generator=generation_generator,)grouped=self.processor.decode_layout_sequences(placement_ids,batch_size=len(prompts),num_return_sequences=return_sequences,)output=self.processor.layout_text_to_output(grouped,output_candidate=output_candidate,output_type="dataclass",return_intermediates=True,)ifisinstance(output,LayoutGenerationOutput):intermediates=(dict(output.intermediates)ifisinstance(output.intermediates,dict)else{})ifreturn_intermediates:intermediates.update({"prompt":prompts,"logical_forms":logical_forms,"placement_inputs":placement_inputs,})output.intermediates=intermediatesifreturn_intermediateselseNoneifoutput_type=="dict":returndict(output)returnoutputgenerate=__call__
Source code in models/parse-then-place/src/parse_then_place/pipeline_parse_then_place.py
111112113114115116117118119120121122123124
def__init__(self,config:ParseThenPlaceConfig,processor:ParseThenPlaceProcessor,*,parser:PreTrainedModel|None=None,placement:PreTrainedModel|None=None,)->None:"""Initialize the composite pipeline."""super().__init__(config)self.config=configself.processor=processorself.parser=parserself.placement=placement
@classmethoddeffrom_pretrained(cls,pretrained_model_name_or_path:str|Path,*,parser:PreTrainedModel|None=None,placement:PreTrainedModel|None=None,processor:ParseThenPlaceProcessor|None=None,local_files_only:bool=False,config:ParseThenPlaceConfig|PretrainedConfig|None=None,)->ParseThenPlacePipeline:# ty: ignore[invalid-method-override]"""Load a composite pipeline from a root directory."""components:dict[str,PreTrainedModel|ParseThenPlaceProcessor]={}ifparserisnotNone:components["parser"]=parserifplacementisnotNone:components["placement"]=placementifprocessorisNone:loaded=super().from_pretrained(pretrained_model_name_or_path,local_files_only=local_files_only,config=config,components=components,)returncast(ParseThenPlacePipeline,loaded)components["processor"]=processorloaded=super().from_pretrained(pretrained_model_name_or_path,local_files_only=local_files_only,config=config,components=components,)returncast(ParseThenPlacePipeline,loaded)
@torch.no_grad()defparse(self,input_ids:Int[torch.Tensor,"batch tokens"],attention_mask:Bool[torch.Tensor,"batch tokens"]|None=None,*,generation_max_length:int|None=None,**generate_kwargs:str|int|float|bool|torch.Generator|None,)->Int[torch.Tensor,"batch tokens"]:"""Generate logical-form token ids with the parser stage."""ifself.parserisNone:raiseValueError("Parser stage is not loaded")generated=cast(_GenerationModel,self.parser).generate(input_ids=input_ids,attention_mask=attention_mask,max_length=generation_max_lengthorself.config.parser_generation_max_length,**generate_kwargs,)returngenerated
@torch.no_grad()defplace(self,input_ids:Int[torch.Tensor,"batch tokens"],attention_mask:Bool[torch.Tensor,"batch tokens"]|None=None,*,generation_max_length:int|None=None,num_return_sequences:int|None=None,temperature:float|None=None,do_sample:bool=True,generator:torch.Generator|None=None,**generate_kwargs:str|float|bool|torch.Generator|None,)->Int[torch.Tensor,"batch tokens"]:"""Generate layout token ids with the placement stage."""ifself.placementisNone:raiseValueError("Placement stage is not loaded")ifgeneratorisnotNone:generate_kwargs["generator"]=generatorgenerated=cast(_GenerationModel,self.placement).generate(input_ids=input_ids,attention_mask=attention_mask,max_length=generation_max_lengthorself.config.placement_generation_max_length,num_return_sequences=num_return_sequencesorself.config.num_return_sequences,temperature=temperatureorself.config.temperature,do_sample=do_sample,**generate_kwargs,)returngenerated
__call__(*,prompt:str|Sequence[str]|None=None,batch_size:int=1,seed:int|None=None,generator:Generator|None=None,condition_type:ConditionType|str=ConditionType.text,labels:Int[Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,bbox:Float[Tensor,"batch elements 4"]|list[ArrayLikeInput]|None=None,mask:Bool[Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,num_elements:int|list[int]|Int[Tensor,"batch"]|None=None,box_format:BoxFormat|str=BoxFormat.xywh,normalized:bool=True,canvas_size:tuple[int,int]|None=None,num_inference_steps:int|None=None,num_return_sequences:int|None=None,temperature:float|None=None,output_candidate:Literal["first","all","best"]="first",output_type:Literal["dataclass","dict"]="dataclass",return_intermediates:bool=False,layout_text:str|list[str]|list[list[str]]|None=None,)->LayoutGenerationOutput|ParseThenPlaceOutputDict
Generate a layout from natural-language text.
Source code in models/parse-then-place/src/parse_then_place/pipeline_parse_then_place.py
def__call__(self,*,prompt:str|Sequence[str]|None=None,batch_size:int=1,seed:int|None=None,generator:torch.Generator|None=None,condition_type:ConditionType|str=ConditionType.text,labels:Int[torch.Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,bbox:Float[torch.Tensor,"batch elements 4"]|list[ArrayLikeInput]|None=None,mask:Bool[torch.Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,num_elements:int|list[int]|Int[torch.Tensor,"batch"]|None=None,box_format:BoxFormat|str=BoxFormat.xywh,normalized:bool=True,canvas_size:tuple[int,int]|None=None,num_inference_steps:int|None=None,num_return_sequences:int|None=None,temperature:float|None=None,output_candidate:Literal["first","all","best"]="first",output_type:Literal["dataclass","dict"]="dataclass",return_intermediates:bool=False,layout_text:str|list[str]|list[list[str]]|None=None,)->LayoutGenerationOutput|ParseThenPlaceOutputDict:# ty: ignore[invalid-method-override]"""Generate a layout from natural-language text."""_=(batch_size,labels,bbox,mask,num_elements,box_format,normalized,canvas_size,num_inference_steps,)condition=normalize_condition_type(condition_type)ifconditionisnotConditionType.text:raiseNotImplementedError("Parse-Then-Place only supports condition_type='text'")iflayout_textisnotNone:layout_items=([layout_text]ifisinstance(layout_text,str)elselayout_text)returnself.processor.layout_text_to_output(layout_items,output_candidate=output_candidate,output_type=output_type,return_intermediates=return_intermediates,)ifpromptisNone:raiseValueError("prompt is required for Parse-Then-Place generation")generation_generator=self.prepare_generator(generator=generator,seed=seed,)prompts=[prompt]ifisinstance(prompt,str)elselist(prompt)parser_inputs=self.processor(prompts)if"input_ids"notinparser_inputs:raiseValueError("processor requires parser_tokenizer for model inference")parser_ids=self.parse(parser_inputs["input_ids"],attention_mask=parser_inputs.get("attention_mask"),generation_max_length=self.config.parser_generation_max_length,)value_maps=cast(list[dict[str,str]|None],parser_inputs.get("value_maps"))logical_forms=self.processor.postprocess_ir(parser_ids,value_maps=value_maps,)placement_inputs=self.processor.ir_to_placement_inputs(logical_forms)placement_encoded=self.processor.encode_placement_inputs(placement_inputs)if"input_ids"notinplacement_encoded:raiseValueError("processor requires placement_tokenizer for model inference")return_sequences=num_return_sequencesorself.config.num_return_sequencesplacement_ids=self.place(placement_encoded["input_ids"],attention_mask=placement_encoded.get("attention_mask"),num_return_sequences=return_sequences,temperature=temperature,generator=generation_generator,)grouped=self.processor.decode_layout_sequences(placement_ids,batch_size=len(prompts),num_return_sequences=return_sequences,)output=self.processor.layout_text_to_output(grouped,output_candidate=output_candidate,output_type="dataclass",return_intermediates=True,)ifisinstance(output,LayoutGenerationOutput):intermediates=(dict(output.intermediates)ifisinstance(output.intermediates,dict)else{})ifreturn_intermediates:intermediates.update({"prompt":prompts,"logical_forms":logical_forms,"placement_inputs":placement_inputs,})output.intermediates=intermediatesifreturn_intermediateselseNoneifoutput_type=="dict":returndict(output)returnoutput
classParseThenPlaceProcessor(ProcessorMixin):"""Build stage inputs and parse placement-model output text."""attributes=["parser_tokenizer","placement_tokenizer"]parser_tokenizer_class="AutoTokenizer"placement_tokenizer_class="T5Tokenizer"def__init__(self,parser_tokenizer:PreTrainedTokenizerBase|None=None,placement_tokenizer:PreTrainedTokenizerBase|None=None,dataset_name:ParseThenPlaceDatasetName|str=ParseThenPlaceDatasetName.rico,canvas_size:tuple[int,int]|None=None,id2label:dict[int,str]|None=None,)->None:"""Initialize tokenizer handles and dataset metadata."""dataset=normalize_dataset_name(dataset_name)self.parser_tokenizer=parser_tokenizerself.placement_tokenizer=placement_tokenizerself.dataset_name=str(dataset)self.canvas_size=canvas_sizeorcanvas_size_for_dataset(dataset)self.id2label=({int(key):str(value)forkey,valueinid2label.items()}ifid2labelisnotNoneelseid2label_for_dataset(dataset))self.label2id={label.lower():idxforidx,labelinself.id2label.items()}# Keep released spellings as aliases because RICO uses lower-case labels.self.label2id.update(label2id_for_dataset(dataset))ifparser_tokenizerisnotNoneandplacement_tokenizerisnotNone:super().__init__(parser_tokenizer=parser_tokenizer,placement_tokenizer=placement_tokenizer,)@classmethoddeffrom_config(cls,dataset_name:ParseThenPlaceDatasetName|str=ParseThenPlaceDatasetName.rico,*,canvas_size:tuple[int,int]|None=None,id2label:dict[int,str]|None=None,)->ParseThenPlaceProcessor:"""Construct metadata-only processor for tests and local smoke checks."""returncls(parser_tokenizer=None,placement_tokenizer=None,dataset_name=dataset_name,canvas_size=canvas_size,id2label=id2label,)defpreprocess_prompt(self,prompt:str,*,replace_explicit_value:bool=True,)->PromptEncoding:"""Apply the released text normalization used before stage-1 parsing. Args: prompt: Natural-language text prompt. replace_explicit_value: Whether quoted values should be replaced by deterministic ``value_N`` placeholders. Returns: Normalized prompt and the placeholder recovery map. """result=prompt.replace("#","").strip().lower()result=(result.replace("“",'"').replace("”",'"').replace("‘","'").replace("’","'"))result=_WHITESPACE_RE.sub(" ",result)ifnotreplace_explicit_value:return{"prompt":result,"value_map":None}returnself._extract_explicit_values(result)def_extract_explicit_values(self,prompt:str)->PromptEncoding:result=re.sub(r"(\w)'s\s+",r"\g<1>`s ",prompt)values=_DOUBLE_QUOTED_RE.findall(result)single_values=_SINGLE_QUOTED_RE.findall(result)iflen(single_values)==1andany(punctinsingle_values[0]forpunctin(",",".")):single_values=[]values.extend(single_values)value_map:dict[str,str]={}forvalue_idx,valueinenumerate(values):placeholder=f"value_{value_idx}"value_map[placeholder]=value.strip('"').strip("'").strip()result=result.replace(value,f'"{placeholder}"',1)result=re.sub(r"(\w)`s\s+",r"\g<1>'s ",result)result=_WHITESPACE_RE.sub(" ",result)return{"prompt":result,"value_map":value_map}def__call__(self,prompt:str|Sequence[str],*,replace_explicit_value:bool=True,return_tensors:Literal["pt"]="pt",)->BatchEncoding:"""Tokenize prompt text for the semantic parser stage."""prompts=[prompt]ifisinstance(prompt,str)elselist(prompt)encodings=[self.preprocess_prompt(item,replace_explicit_value=replace_explicit_value)foriteminprompts]texts=[item["prompt"]foriteminencodings]ifself.parser_tokenizerisNone:returnBatchEncoding({"prompt_text":texts,"value_maps":[item["value_map"]foriteminencodings],})parser_tokenizer=self.parser_tokenizertokenized=parser_tokenizer(texts,return_tensors=return_tensors,padding=True)tokenized["value_maps"]=[item["value_map"]foriteminencodings]tokenized["prompt_text"]=textsreturncast(BatchEncoding,tokenized)defpostprocess_ir(self,generated_ids:Int[torch.Tensor,"batch tokens"]|Sequence[str],*,value_maps:list[dict[str,str]|None]|None=None,)->list[str]:"""Decode and lightly normalize stage-1 logical forms."""ifisinstance(generated_ids,torch.Tensor):ifself.parser_tokenizerisNone:raiseValueError("parser_tokenizer is required to decode generated ids")parser_tokenizer=self.parser_tokenizerlogical_forms=parser_tokenizer.batch_decode(generated_ids,skip_special_tokens=True)else:logical_forms=list(generated_ids)ifvalue_mapsisNone:return[self._normalize_ir(item)foriteminlogical_forms]return[self._recover_ir_values(self._normalize_ir(item),value_map)foritem,value_mapinzip(logical_forms,value_maps,strict=True)]def_normalize_ir(self,logical_form:str)->str:result=logical_form.replace("["," [ ").replace("]"," ] ").strip().lower()return_WHITESPACE_RE.sub(" ",result)def_recover_ir_values(self,logical_form:str,value_map:dict[str,str]|None,)->str:ifnotvalue_map:returnlogical_formresult=logical_formforplaceholder,valueinvalue_map.items():recovered=value.replace("'","")result=result.replace(f"'{placeholder}'",f"'{recovered}'")result=result.replace(f" {placeholder},",f" {value},")result=result.replace(f" {placeholder}'",f" {value}'")returnresult.replace("&"," and ")defir_to_placement_inputs(self,logical_forms:Sequence[str])->list[str]:"""Convert logical forms to placement-constraint strings. Runtime keeps this method deterministic and accepts already-linearized constraints, which is also the artifact stored in stage-1 prediction JSON files. The current parity scripts do not execute the released grammar executor. """return[self._logical_form_to_constraint(item)foriteminlogical_forms]def_logical_form_to_constraint(self,logical_form:str)->str:text=_WHITESPACE_RE.sub(" ",logical_form.strip())if":"intextand"|"intext:returntextreturntextdefencode_placement_inputs(self,placement_inputs:Sequence[str],*,return_tensors:Literal["pt"]="pt",)->BatchEncoding:"""Tokenize stage-2 placement constraints."""ifself.placement_tokenizerisNone:returnBatchEncoding({"placement_text":list(placement_inputs)})placement_tokenizer=self.placement_tokenizertokenized=placement_tokenizer(list(placement_inputs),return_tensors=return_tensors,padding=True)tokenized["placement_text"]=list(placement_inputs)returncast(BatchEncoding,tokenized)defdecode_layout_sequences(self,generated_ids:Int[torch.Tensor,"batch tokens"]|Sequence[str],*,batch_size:int,num_return_sequences:int,)->list[list[str]]:"""Decode stage-2 generated ids into grouped layout strings."""ifisinstance(generated_ids,torch.Tensor):ifself.placement_tokenizerisNone:raiseValueError("placement_tokenizer is required to decode generated ids")placement_tokenizer=self.placement_tokenizerflat=placement_tokenizer.batch_decode(generated_ids,skip_special_tokens=True)else:flat=list(generated_ids)expected=batch_size*num_return_sequencesiflen(flat)!=expected:raiseValueError("Generated layout count does not match batch_size * num_return_sequences: "f"{len(flat)} != {expected}")return[flat[idx*num_return_sequences:(idx+1)*num_return_sequences]foridxinrange(batch_size)]deflayout_text_to_output(self,layout_text:Sequence[str]|Sequence[Sequence[str]],*,output_candidate:Literal["first","all","best"]="first",output_type:Literal["dataclass","dict"]="dataclass",return_intermediates:bool=False,)->LayoutGenerationOutput|ParseThenPlaceOutputDict:"""Parse generated ``label left top width height`` text into schema."""candidate_groups=self._normalize_layout_text_groups(layout_text)selected=self._select_candidates(candidate_groups,output_candidate)parsed_groups=[self._parse_layout_text(item)foriteminselected]max_len=max((len(item)foriteminparsed_groups),default=0)or1bbox_rows:list[Float[torch.Tensor,"elements 4"]]=[]label_rows:list[Int[torch.Tensor,...]]=[]mask_rows:list[Bool[torch.Tensor,...]]=[]forparsedinparsed_groups:labels=torch.tensor([item["label"]foriteminparsed],dtype=torch.long)boxes=torch.tensor([item["bbox"]foriteminparsed],dtype=torch.float32)mask=torch.ones(len(parsed),dtype=torch.bool)iflen(parsed)==0:labels=torch.zeros(max_len,dtype=torch.long)boxes=torch.zeros(max_len,4,dtype=torch.float32)mask=torch.zeros(max_len,dtype=torch.bool)eliflen(parsed)<max_len:pad=max_len-len(parsed)labels=torch.nn.functional.pad(labels,(0,pad))boxes=torch.nn.functional.pad(boxes,(0,0,0,pad))mask=torch.nn.functional.pad(mask,(0,pad))label_rows.append(labels)bbox_rows.append(boxes)mask_rows.append(mask)raw_bbox=torch.stack(bbox_rows)bbox=normalize_boxes(raw_bbox,canvas_size=self.canvas_size,box_format="ltwh",)output=LayoutGenerationOutput(bbox=bbox.float(),labels=torch.stack(label_rows).long(),mask=torch.stack(mask_rows).bool(),id2label=dict(self.id2label),intermediates={"layout_text":selected,"layout_text_candidates":candidate_groups,"dataset_name":self.dataset_name,"canvas_size":self.canvas_size,}ifreturn_intermediateselseNone,)ifoutput_type=="dict":returndict(output)ifoutput_type!="dataclass":raiseValueError(f"Unsupported output_type: {output_type}")returnoutputdef_normalize_layout_text_groups(self,layout_text:Sequence[str]|Sequence[Sequence[str]],)->list[list[str]]:ifnotlayout_text:return[]first=layout_text[0]ifisinstance(first,str):return[[item]foritemincast(Sequence[str],layout_text)]return[list(item)foritemincast(Sequence[Sequence[str]],layout_text)]def_select_candidates(self,candidate_groups:list[list[str]],output_candidate:Literal["first","all","best"],)->list[str]:ifoutput_candidate=="first":return[group[0]ifgroupelse""forgroupincandidate_groups]ifoutput_candidate=="best":return[max(group,key=lambdaitem:len(self._parse_layout_text(item)))ifgroupelse""forgroupincandidate_groups]ifoutput_candidate=="all":return["\n".join(group)forgroupincandidate_groups]raiseValueError(f"Unsupported output_candidate: {output_candidate}")def_parse_layout_text(self,layout_text:str)->list[ParsedElement]:elements:list[ParsedElement]=[]formatchin_LAYOUT_PATTERN.finditer(layout_text.lower()):label=_WHITESPACE_RE.sub(" ",match.group("label")).strip()label_id=self.label2id.get(label)iflabel_idisNone:continueelements.append({"label":label_id,"bbox":[float(match.group("left")),float(match.group("top")),float(match.group("width")),float(match.group("height")),],})returnelements
def__init__(self,parser_tokenizer:PreTrainedTokenizerBase|None=None,placement_tokenizer:PreTrainedTokenizerBase|None=None,dataset_name:ParseThenPlaceDatasetName|str=ParseThenPlaceDatasetName.rico,canvas_size:tuple[int,int]|None=None,id2label:dict[int,str]|None=None,)->None:"""Initialize tokenizer handles and dataset metadata."""dataset=normalize_dataset_name(dataset_name)self.parser_tokenizer=parser_tokenizerself.placement_tokenizer=placement_tokenizerself.dataset_name=str(dataset)self.canvas_size=canvas_sizeorcanvas_size_for_dataset(dataset)self.id2label=({int(key):str(value)forkey,valueinid2label.items()}ifid2labelisnotNoneelseid2label_for_dataset(dataset))self.label2id={label.lower():idxforidx,labelinself.id2label.items()}# Keep released spellings as aliases because RICO uses lower-case labels.self.label2id.update(label2id_for_dataset(dataset))ifparser_tokenizerisnotNoneandplacement_tokenizerisnotNone:super().__init__(parser_tokenizer=parser_tokenizer,placement_tokenizer=placement_tokenizer,)
Construct metadata-only processor for tests and local smoke checks.
Source code in models/parse-then-place/src/parse_then_place/processing_parse_then_place.py
104105106107108109110111112113114115116117118119
@classmethoddeffrom_config(cls,dataset_name:ParseThenPlaceDatasetName|str=ParseThenPlaceDatasetName.rico,*,canvas_size:tuple[int,int]|None=None,id2label:dict[int,str]|None=None,)->ParseThenPlaceProcessor:"""Construct metadata-only processor for tests and local smoke checks."""returncls(parser_tokenizer=None,placement_tokenizer=None,dataset_name=dataset_name,canvas_size=canvas_size,id2label=id2label,)
defpreprocess_prompt(self,prompt:str,*,replace_explicit_value:bool=True,)->PromptEncoding:"""Apply the released text normalization used before stage-1 parsing. Args: prompt: Natural-language text prompt. replace_explicit_value: Whether quoted values should be replaced by deterministic ``value_N`` placeholders. Returns: Normalized prompt and the placeholder recovery map. """result=prompt.replace("#","").strip().lower()result=(result.replace("“",'"').replace("”",'"').replace("‘","'").replace("’","'"))result=_WHITESPACE_RE.sub(" ",result)ifnotreplace_explicit_value:return{"prompt":result,"value_map":None}returnself._extract_explicit_values(result)
def__call__(self,prompt:str|Sequence[str],*,replace_explicit_value:bool=True,return_tensors:Literal["pt"]="pt",)->BatchEncoding:"""Tokenize prompt text for the semantic parser stage."""prompts=[prompt]ifisinstance(prompt,str)elselist(prompt)encodings=[self.preprocess_prompt(item,replace_explicit_value=replace_explicit_value)foriteminprompts]texts=[item["prompt"]foriteminencodings]ifself.parser_tokenizerisNone:returnBatchEncoding({"prompt_text":texts,"value_maps":[item["value_map"]foriteminencodings],})parser_tokenizer=self.parser_tokenizertokenized=parser_tokenizer(texts,return_tensors=return_tensors,padding=True)tokenized["value_maps"]=[item["value_map"]foriteminencodings]tokenized["prompt_text"]=textsreturncast(BatchEncoding,tokenized)
defpostprocess_ir(self,generated_ids:Int[torch.Tensor,"batch tokens"]|Sequence[str],*,value_maps:list[dict[str,str]|None]|None=None,)->list[str]:"""Decode and lightly normalize stage-1 logical forms."""ifisinstance(generated_ids,torch.Tensor):ifself.parser_tokenizerisNone:raiseValueError("parser_tokenizer is required to decode generated ids")parser_tokenizer=self.parser_tokenizerlogical_forms=parser_tokenizer.batch_decode(generated_ids,skip_special_tokens=True)else:logical_forms=list(generated_ids)ifvalue_mapsisNone:return[self._normalize_ir(item)foriteminlogical_forms]return[self._recover_ir_values(self._normalize_ir(item),value_map)foritem,value_mapinzip(logical_forms,value_maps,strict=True)]
Convert logical forms to placement-constraint strings.
Runtime keeps this method deterministic and accepts already-linearized
constraints, which is also the artifact stored in stage-1 prediction JSON
files. The current parity scripts do not execute the released grammar
executor.
Source code in models/parse-then-place/src/parse_then_place/processing_parse_then_place.py
237238239240241242243244245
defir_to_placement_inputs(self,logical_forms:Sequence[str])->list[str]:"""Convert logical forms to placement-constraint strings. Runtime keeps this method deterministic and accepts already-linearized constraints, which is also the artifact stored in stage-1 prediction JSON files. The current parity scripts do not execute the released grammar executor. """return[self._logical_form_to_constraint(item)foriteminlogical_forms]
defdecode_layout_sequences(self,generated_ids:Int[torch.Tensor,"batch tokens"]|Sequence[str],*,batch_size:int,num_return_sequences:int,)->list[list[str]]:"""Decode stage-2 generated ids into grouped layout strings."""ifisinstance(generated_ids,torch.Tensor):ifself.placement_tokenizerisNone:raiseValueError("placement_tokenizer is required to decode generated ids")placement_tokenizer=self.placement_tokenizerflat=placement_tokenizer.batch_decode(generated_ids,skip_special_tokens=True)else:flat=list(generated_ids)expected=batch_size*num_return_sequencesiflen(flat)!=expected:raiseValueError("Generated layout count does not match batch_size * num_return_sequences: "f"{len(flat)} != {expected}")return[flat[idx*num_return_sequences:(idx+1)*num_return_sequences]foridxinrange(batch_size)]
deflayout_text_to_output(self,layout_text:Sequence[str]|Sequence[Sequence[str]],*,output_candidate:Literal["first","all","best"]="first",output_type:Literal["dataclass","dict"]="dataclass",return_intermediates:bool=False,)->LayoutGenerationOutput|ParseThenPlaceOutputDict:"""Parse generated ``label left top width height`` text into schema."""candidate_groups=self._normalize_layout_text_groups(layout_text)selected=self._select_candidates(candidate_groups,output_candidate)parsed_groups=[self._parse_layout_text(item)foriteminselected]max_len=max((len(item)foriteminparsed_groups),default=0)or1bbox_rows:list[Float[torch.Tensor,"elements 4"]]=[]label_rows:list[Int[torch.Tensor,...]]=[]mask_rows:list[Bool[torch.Tensor,...]]=[]forparsedinparsed_groups:labels=torch.tensor([item["label"]foriteminparsed],dtype=torch.long)boxes=torch.tensor([item["bbox"]foriteminparsed],dtype=torch.float32)mask=torch.ones(len(parsed),dtype=torch.bool)iflen(parsed)==0:labels=torch.zeros(max_len,dtype=torch.long)boxes=torch.zeros(max_len,4,dtype=torch.float32)mask=torch.zeros(max_len,dtype=torch.bool)eliflen(parsed)<max_len:pad=max_len-len(parsed)labels=torch.nn.functional.pad(labels,(0,pad))boxes=torch.nn.functional.pad(boxes,(0,0,0,pad))mask=torch.nn.functional.pad(mask,(0,pad))label_rows.append(labels)bbox_rows.append(boxes)mask_rows.append(mask)raw_bbox=torch.stack(bbox_rows)bbox=normalize_boxes(raw_bbox,canvas_size=self.canvas_size,box_format="ltwh",)output=LayoutGenerationOutput(bbox=bbox.float(),labels=torch.stack(label_rows).long(),mask=torch.stack(mask_rows).bool(),id2label=dict(self.id2label),intermediates={"layout_text":selected,"layout_text_candidates":candidate_groups,"dataset_name":self.dataset_name,"canvas_size":self.canvas_size,}ifreturn_intermediateselseNone,)ifoutput_type=="dict":returndict(output)ifoutput_type!="dataclass":raiseValueError(f"Unsupported output_type: {output_type}")returnoutput
Return the dataset canvas size as (width, height).
Source code in models/parse-then-place/src/parse_then_place/labels.py
127128129130131
defcanvas_size_for_dataset(dataset_name:ParseThenPlaceDatasetName|str,)->tuple[int,int]:"""Return the dataset canvas size as ``(width, height)``."""returndataset_metadata(dataset_name)["canvas_size"]
Source code in models/parse-then-place/src/parse_then_place/labels.py
94 95 96 97 98 99100101
defnormalize_stage2_mode(stage2_mode:Stage2Mode|str)->Stage2Mode:"""Normalize a released stage-2 checkpoint mode."""ifisinstance(stage2_mode,Stage2Mode):returnstage2_modetry:returnStage2Mode(stage2_mode.lower().replace("-","_"))exceptValueErrorasexc:raiseValueError(f"Unsupported stage2_mode: {stage2_mode}")fromexc
classParseThenPlaceConfig(PretrainedConfig):"""Stores Parse-Then-Place dataset and generation defaults."""model_type="parse-then-place"def__init__(self,dataset_name:str="rico",stage2_mode:Stage2Mode|str=Stage2Mode.finetune,parser_model_name:str="google/t5-v1_1-base",parser_generation_max_length:int=600,placement_generation_max_length:int=500,temperature:float=0.7,num_return_sequences:int=5,canvas_size:tuple[int,int]|list[int]|None=None,id2label:dict[int|str,str]|None=None,parser_subfolder:str="semantic_parser",placement_subfolder:str="placement",pad_token_id:int=0,eos_token_id:int=1,decoder_start_token_id:int=0,is_encoder_decoder:bool=True,transformers_version:str|None=None,architectures:list[str]|None=None,output_hidden_states:bool|None=False,return_dict:bool|None=True,dtype:str|None=None,torch_dtype:str|None=None,chunk_size_feed_forward:int=0,problem_type:Literal["regression","single_label_classification","multi_label_classification"]|None=None,name_or_path:str="",_commit_hash:str|None=None,attn_implementation:str|None=None,**kwargs:str|int|float|bool|None,)->None:"""Initialize the composite checkpoint configuration."""dataset=normalize_dataset_name(dataset_name)mode=normalize_stage2_mode(stage2_mode)self.dataset_name=str(dataset)self.stage2_mode=str(mode)self.parser_model_name=parser_model_nameself.parser_generation_max_length=parser_generation_max_lengthself.placement_generation_max_length=placement_generation_max_lengthself.temperature=temperatureself.num_return_sequences=num_return_sequencesself.canvas_size=tuple(canvas_sizeorcanvas_size_for_dataset(dataset))self.parser_subfolder=parser_subfolderself.placement_subfolder=placement_subfolderlabel_map=id2labelorid2label_for_dataset(dataset)normalized_id2label={int(key):str(value)forkey,valueinlabel_map.items()}label2id={value:keyforkey,valueinnormalized_id2label.items()}_=kwargs.pop("id2label",None)_=kwargs.pop("label2id",None)super().__init__(transformers_version=transformers_version,architectures=architectures,output_hidden_states=output_hidden_states,return_dict=return_dict,dtype=dtypeortorch_dtype,chunk_size_feed_forward=chunk_size_feed_forward,is_encoder_decoder=is_encoder_decoder,id2label=normalized_id2label,label2id=label2id,problem_type=problem_type,)# Transformers v5 keeps model-specific token fields on the subclass;# only common configuration fields belong in the base call.self.pad_token_id=pad_token_idself.eos_token_id=eos_token_idself.decoder_start_token_id=decoder_start_token_idself.name_or_path=name_or_pathself._commit_hash=_commit_hashself._attn_implementation=attn_implementationforkey,valueinkwargs.items():setattr(self,key,value)
def__init__(self,dataset_name:str="rico",stage2_mode:Stage2Mode|str=Stage2Mode.finetune,parser_model_name:str="google/t5-v1_1-base",parser_generation_max_length:int=600,placement_generation_max_length:int=500,temperature:float=0.7,num_return_sequences:int=5,canvas_size:tuple[int,int]|list[int]|None=None,id2label:dict[int|str,str]|None=None,parser_subfolder:str="semantic_parser",placement_subfolder:str="placement",pad_token_id:int=0,eos_token_id:int=1,decoder_start_token_id:int=0,is_encoder_decoder:bool=True,transformers_version:str|None=None,architectures:list[str]|None=None,output_hidden_states:bool|None=False,return_dict:bool|None=True,dtype:str|None=None,torch_dtype:str|None=None,chunk_size_feed_forward:int=0,problem_type:Literal["regression","single_label_classification","multi_label_classification"]|None=None,name_or_path:str="",_commit_hash:str|None=None,attn_implementation:str|None=None,**kwargs:str|int|float|bool|None,)->None:"""Initialize the composite checkpoint configuration."""dataset=normalize_dataset_name(dataset_name)mode=normalize_stage2_mode(stage2_mode)self.dataset_name=str(dataset)self.stage2_mode=str(mode)self.parser_model_name=parser_model_nameself.parser_generation_max_length=parser_generation_max_lengthself.placement_generation_max_length=placement_generation_max_lengthself.temperature=temperatureself.num_return_sequences=num_return_sequencesself.canvas_size=tuple(canvas_sizeorcanvas_size_for_dataset(dataset))self.parser_subfolder=parser_subfolderself.placement_subfolder=placement_subfolderlabel_map=id2labelorid2label_for_dataset(dataset)normalized_id2label={int(key):str(value)forkey,valueinlabel_map.items()}label2id={value:keyforkey,valueinnormalized_id2label.items()}_=kwargs.pop("id2label",None)_=kwargs.pop("label2id",None)super().__init__(transformers_version=transformers_version,architectures=architectures,output_hidden_states=output_hidden_states,return_dict=return_dict,dtype=dtypeortorch_dtype,chunk_size_feed_forward=chunk_size_feed_forward,is_encoder_decoder=is_encoder_decoder,id2label=normalized_id2label,label2id=label2id,problem_type=problem_type,)# Transformers v5 keeps model-specific token fields on the subclass;# only common configuration fields belong in the base call.self.pad_token_id=pad_token_idself.eos_token_id=eos_token_idself.decoder_start_token_id=decoder_start_token_idself.name_or_path=name_or_pathself._commit_hash=_commit_hashself._attn_implementation=attn_implementationforkey,valueinkwargs.items():setattr(self,key,value)
Source code in models/parse-then-place/src/parse_then_place/labels.py
94 95 96 97 98 99100101
defnormalize_stage2_mode(stage2_mode:Stage2Mode|str)->Stage2Mode:"""Normalize a released stage-2 checkpoint mode."""ifisinstance(stage2_mode,Stage2Mode):returnstage2_modetry:returnStage2Mode(stage2_mode.lower().replace("-","_"))exceptValueErrorasexc:raiseValueError(f"Unsupported stage2_mode: {stage2_mode}")fromexc
Return static metadata for a Parse-Then-Place dataset.
Source code in models/parse-then-place/src/parse_then_place/labels.py
104105106107108
defdataset_metadata(dataset_name:ParseThenPlaceDatasetName|str,)->DatasetMetadata:"""Return static metadata for a Parse-Then-Place dataset."""returnDATASET_METADATA[normalize_dataset_name(dataset_name)]
Return the dataset canvas size as (width, height).
Source code in models/parse-then-place/src/parse_then_place/labels.py
127128129130131
defcanvas_size_for_dataset(dataset_name:ParseThenPlaceDatasetName|str,)->tuple[int,int]:"""Return the dataset canvas size as ``(width, height)``."""returndataset_metadata(dataset_name)["canvas_size"]
classParseThenPlacePipeline(LayoutGenerationPipeline):"""Compose standard seq2seq parser and placement models."""config_class:ClassVar[type[PretrainedConfig]]=ParseThenPlaceConfigcomponent_specs:ClassVar[dict[str,PipelineComponentSpec]]={"parser":PipelineComponentSpec(attribute_name="parser",loader=_load_seq2seq_component,config_subfolder_attribute="parser_subfolder",required=False,),"placement":PipelineComponentSpec(attribute_name="placement",loader=_load_seq2seq_component,config_subfolder_attribute="placement_subfolder",),"processor":PipelineComponentSpec(attribute_name="processor",loader=_load_processor_component,marker_file="processor_config.json",save_with_is_main_process=False,),}config:ParseThenPlaceConfigparser:PreTrainedModel|Noneplacement:PreTrainedModel|Noneprocessor:ParseThenPlaceProcessordef__init__(self,config:ParseThenPlaceConfig,processor:ParseThenPlaceProcessor,*,parser:PreTrainedModel|None=None,placement:PreTrainedModel|None=None,)->None:"""Initialize the composite pipeline."""super().__init__(config)self.config=configself.processor=processorself.parser=parserself.placement=placement@classmethoddeffrom_pretrained(cls,pretrained_model_name_or_path:str|Path,*,parser:PreTrainedModel|None=None,placement:PreTrainedModel|None=None,processor:ParseThenPlaceProcessor|None=None,local_files_only:bool=False,config:ParseThenPlaceConfig|PretrainedConfig|None=None,)->ParseThenPlacePipeline:# ty: ignore[invalid-method-override]"""Load a composite pipeline from a root directory."""components:dict[str,PreTrainedModel|ParseThenPlaceProcessor]={}ifparserisnotNone:components["parser"]=parserifplacementisnotNone:components["placement"]=placementifprocessorisNone:loaded=super().from_pretrained(pretrained_model_name_or_path,local_files_only=local_files_only,config=config,components=components,)returncast(ParseThenPlacePipeline,loaded)components["processor"]=processorloaded=super().from_pretrained(pretrained_model_name_or_path,local_files_only=local_files_only,config=config,components=components,)returncast(ParseThenPlacePipeline,loaded)@classmethoddef_from_pretrained_components(cls,*,config:PretrainedConfig,components:Mapping[str,PreTrainedModel|ParseThenPlaceProcessor|None],)->ParseThenPlacePipeline:"""Build a pipeline from loaded config and components."""returncls(config=cast(ParseThenPlaceConfig,config),processor=cast(ParseThenPlaceProcessor,components["processor"]),parser=cast(PreTrainedModel|None,components.get("parser")),placement=cast(PreTrainedModel|None,components["placement"]),)@torch.no_grad()defparse(self,input_ids:Int[torch.Tensor,"batch tokens"],attention_mask:Bool[torch.Tensor,"batch tokens"]|None=None,*,generation_max_length:int|None=None,**generate_kwargs:str|int|float|bool|torch.Generator|None,)->Int[torch.Tensor,"batch tokens"]:"""Generate logical-form token ids with the parser stage."""ifself.parserisNone:raiseValueError("Parser stage is not loaded")generated=cast(_GenerationModel,self.parser).generate(input_ids=input_ids,attention_mask=attention_mask,max_length=generation_max_lengthorself.config.parser_generation_max_length,**generate_kwargs,)returngenerated@torch.no_grad()defplace(self,input_ids:Int[torch.Tensor,"batch tokens"],attention_mask:Bool[torch.Tensor,"batch tokens"]|None=None,*,generation_max_length:int|None=None,num_return_sequences:int|None=None,temperature:float|None=None,do_sample:bool=True,generator:torch.Generator|None=None,**generate_kwargs:str|float|bool|torch.Generator|None,)->Int[torch.Tensor,"batch tokens"]:"""Generate layout token ids with the placement stage."""ifself.placementisNone:raiseValueError("Placement stage is not loaded")ifgeneratorisnotNone:generate_kwargs["generator"]=generatorgenerated=cast(_GenerationModel,self.placement).generate(input_ids=input_ids,attention_mask=attention_mask,max_length=generation_max_lengthorself.config.placement_generation_max_length,num_return_sequences=num_return_sequencesorself.config.num_return_sequences,temperature=temperatureorself.config.temperature,do_sample=do_sample,**generate_kwargs,)returngenerateddef__call__(self,*,prompt:str|Sequence[str]|None=None,batch_size:int=1,seed:int|None=None,generator:torch.Generator|None=None,condition_type:ConditionType|str=ConditionType.text,labels:Int[torch.Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,bbox:Float[torch.Tensor,"batch elements 4"]|list[ArrayLikeInput]|None=None,mask:Bool[torch.Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,num_elements:int|list[int]|Int[torch.Tensor,"batch"]|None=None,box_format:BoxFormat|str=BoxFormat.xywh,normalized:bool=True,canvas_size:tuple[int,int]|None=None,num_inference_steps:int|None=None,num_return_sequences:int|None=None,temperature:float|None=None,output_candidate:Literal["first","all","best"]="first",output_type:Literal["dataclass","dict"]="dataclass",return_intermediates:bool=False,layout_text:str|list[str]|list[list[str]]|None=None,)->LayoutGenerationOutput|ParseThenPlaceOutputDict:# ty: ignore[invalid-method-override]"""Generate a layout from natural-language text."""_=(batch_size,labels,bbox,mask,num_elements,box_format,normalized,canvas_size,num_inference_steps,)condition=normalize_condition_type(condition_type)ifconditionisnotConditionType.text:raiseNotImplementedError("Parse-Then-Place only supports condition_type='text'")iflayout_textisnotNone:layout_items=([layout_text]ifisinstance(layout_text,str)elselayout_text)returnself.processor.layout_text_to_output(layout_items,output_candidate=output_candidate,output_type=output_type,return_intermediates=return_intermediates,)ifpromptisNone:raiseValueError("prompt is required for Parse-Then-Place generation")generation_generator=self.prepare_generator(generator=generator,seed=seed,)prompts=[prompt]ifisinstance(prompt,str)elselist(prompt)parser_inputs=self.processor(prompts)if"input_ids"notinparser_inputs:raiseValueError("processor requires parser_tokenizer for model inference")parser_ids=self.parse(parser_inputs["input_ids"],attention_mask=parser_inputs.get("attention_mask"),generation_max_length=self.config.parser_generation_max_length,)value_maps=cast(list[dict[str,str]|None],parser_inputs.get("value_maps"))logical_forms=self.processor.postprocess_ir(parser_ids,value_maps=value_maps,)placement_inputs=self.processor.ir_to_placement_inputs(logical_forms)placement_encoded=self.processor.encode_placement_inputs(placement_inputs)if"input_ids"notinplacement_encoded:raiseValueError("processor requires placement_tokenizer for model inference")return_sequences=num_return_sequencesorself.config.num_return_sequencesplacement_ids=self.place(placement_encoded["input_ids"],attention_mask=placement_encoded.get("attention_mask"),num_return_sequences=return_sequences,temperature=temperature,generator=generation_generator,)grouped=self.processor.decode_layout_sequences(placement_ids,batch_size=len(prompts),num_return_sequences=return_sequences,)output=self.processor.layout_text_to_output(grouped,output_candidate=output_candidate,output_type="dataclass",return_intermediates=True,)ifisinstance(output,LayoutGenerationOutput):intermediates=(dict(output.intermediates)ifisinstance(output.intermediates,dict)else{})ifreturn_intermediates:intermediates.update({"prompt":prompts,"logical_forms":logical_forms,"placement_inputs":placement_inputs,})output.intermediates=intermediatesifreturn_intermediateselseNoneifoutput_type=="dict":returndict(output)returnoutputgenerate=__call__
Source code in models/parse-then-place/src/parse_then_place/pipeline_parse_then_place.py
111112113114115116117118119120121122123124
def__init__(self,config:ParseThenPlaceConfig,processor:ParseThenPlaceProcessor,*,parser:PreTrainedModel|None=None,placement:PreTrainedModel|None=None,)->None:"""Initialize the composite pipeline."""super().__init__(config)self.config=configself.processor=processorself.parser=parserself.placement=placement
@classmethoddeffrom_pretrained(cls,pretrained_model_name_or_path:str|Path,*,parser:PreTrainedModel|None=None,placement:PreTrainedModel|None=None,processor:ParseThenPlaceProcessor|None=None,local_files_only:bool=False,config:ParseThenPlaceConfig|PretrainedConfig|None=None,)->ParseThenPlacePipeline:# ty: ignore[invalid-method-override]"""Load a composite pipeline from a root directory."""components:dict[str,PreTrainedModel|ParseThenPlaceProcessor]={}ifparserisnotNone:components["parser"]=parserifplacementisnotNone:components["placement"]=placementifprocessorisNone:loaded=super().from_pretrained(pretrained_model_name_or_path,local_files_only=local_files_only,config=config,components=components,)returncast(ParseThenPlacePipeline,loaded)components["processor"]=processorloaded=super().from_pretrained(pretrained_model_name_or_path,local_files_only=local_files_only,config=config,components=components,)returncast(ParseThenPlacePipeline,loaded)
@torch.no_grad()defparse(self,input_ids:Int[torch.Tensor,"batch tokens"],attention_mask:Bool[torch.Tensor,"batch tokens"]|None=None,*,generation_max_length:int|None=None,**generate_kwargs:str|int|float|bool|torch.Generator|None,)->Int[torch.Tensor,"batch tokens"]:"""Generate logical-form token ids with the parser stage."""ifself.parserisNone:raiseValueError("Parser stage is not loaded")generated=cast(_GenerationModel,self.parser).generate(input_ids=input_ids,attention_mask=attention_mask,max_length=generation_max_lengthorself.config.parser_generation_max_length,**generate_kwargs,)returngenerated
@torch.no_grad()defplace(self,input_ids:Int[torch.Tensor,"batch tokens"],attention_mask:Bool[torch.Tensor,"batch tokens"]|None=None,*,generation_max_length:int|None=None,num_return_sequences:int|None=None,temperature:float|None=None,do_sample:bool=True,generator:torch.Generator|None=None,**generate_kwargs:str|float|bool|torch.Generator|None,)->Int[torch.Tensor,"batch tokens"]:"""Generate layout token ids with the placement stage."""ifself.placementisNone:raiseValueError("Placement stage is not loaded")ifgeneratorisnotNone:generate_kwargs["generator"]=generatorgenerated=cast(_GenerationModel,self.placement).generate(input_ids=input_ids,attention_mask=attention_mask,max_length=generation_max_lengthorself.config.placement_generation_max_length,num_return_sequences=num_return_sequencesorself.config.num_return_sequences,temperature=temperatureorself.config.temperature,do_sample=do_sample,**generate_kwargs,)returngenerated
__call__(*,prompt:str|Sequence[str]|None=None,batch_size:int=1,seed:int|None=None,generator:Generator|None=None,condition_type:ConditionType|str=ConditionType.text,labels:Int[Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,bbox:Float[Tensor,"batch elements 4"]|list[ArrayLikeInput]|None=None,mask:Bool[Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,num_elements:int|list[int]|Int[Tensor,"batch"]|None=None,box_format:BoxFormat|str=BoxFormat.xywh,normalized:bool=True,canvas_size:tuple[int,int]|None=None,num_inference_steps:int|None=None,num_return_sequences:int|None=None,temperature:float|None=None,output_candidate:Literal["first","all","best"]="first",output_type:Literal["dataclass","dict"]="dataclass",return_intermediates:bool=False,layout_text:str|list[str]|list[list[str]]|None=None,)->LayoutGenerationOutput|ParseThenPlaceOutputDict
Generate a layout from natural-language text.
Source code in models/parse-then-place/src/parse_then_place/pipeline_parse_then_place.py
def__call__(self,*,prompt:str|Sequence[str]|None=None,batch_size:int=1,seed:int|None=None,generator:torch.Generator|None=None,condition_type:ConditionType|str=ConditionType.text,labels:Int[torch.Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,bbox:Float[torch.Tensor,"batch elements 4"]|list[ArrayLikeInput]|None=None,mask:Bool[torch.Tensor,"batch elements"]|list[ArrayLikeInput]|None=None,num_elements:int|list[int]|Int[torch.Tensor,"batch"]|None=None,box_format:BoxFormat|str=BoxFormat.xywh,normalized:bool=True,canvas_size:tuple[int,int]|None=None,num_inference_steps:int|None=None,num_return_sequences:int|None=None,temperature:float|None=None,output_candidate:Literal["first","all","best"]="first",output_type:Literal["dataclass","dict"]="dataclass",return_intermediates:bool=False,layout_text:str|list[str]|list[list[str]]|None=None,)->LayoutGenerationOutput|ParseThenPlaceOutputDict:# ty: ignore[invalid-method-override]"""Generate a layout from natural-language text."""_=(batch_size,labels,bbox,mask,num_elements,box_format,normalized,canvas_size,num_inference_steps,)condition=normalize_condition_type(condition_type)ifconditionisnotConditionType.text:raiseNotImplementedError("Parse-Then-Place only supports condition_type='text'")iflayout_textisnotNone:layout_items=([layout_text]ifisinstance(layout_text,str)elselayout_text)returnself.processor.layout_text_to_output(layout_items,output_candidate=output_candidate,output_type=output_type,return_intermediates=return_intermediates,)ifpromptisNone:raiseValueError("prompt is required for Parse-Then-Place generation")generation_generator=self.prepare_generator(generator=generator,seed=seed,)prompts=[prompt]ifisinstance(prompt,str)elselist(prompt)parser_inputs=self.processor(prompts)if"input_ids"notinparser_inputs:raiseValueError("processor requires parser_tokenizer for model inference")parser_ids=self.parse(parser_inputs["input_ids"],attention_mask=parser_inputs.get("attention_mask"),generation_max_length=self.config.parser_generation_max_length,)value_maps=cast(list[dict[str,str]|None],parser_inputs.get("value_maps"))logical_forms=self.processor.postprocess_ir(parser_ids,value_maps=value_maps,)placement_inputs=self.processor.ir_to_placement_inputs(logical_forms)placement_encoded=self.processor.encode_placement_inputs(placement_inputs)if"input_ids"notinplacement_encoded:raiseValueError("processor requires placement_tokenizer for model inference")return_sequences=num_return_sequencesorself.config.num_return_sequencesplacement_ids=self.place(placement_encoded["input_ids"],attention_mask=placement_encoded.get("attention_mask"),num_return_sequences=return_sequences,temperature=temperature,generator=generation_generator,)grouped=self.processor.decode_layout_sequences(placement_ids,batch_size=len(prompts),num_return_sequences=return_sequences,)output=self.processor.layout_text_to_output(grouped,output_candidate=output_candidate,output_type="dataclass",return_intermediates=True,)ifisinstance(output,LayoutGenerationOutput):intermediates=(dict(output.intermediates)ifisinstance(output.intermediates,dict)else{})ifreturn_intermediates:intermediates.update({"prompt":prompts,"logical_forms":logical_forms,"placement_inputs":placement_inputs,})output.intermediates=intermediatesifreturn_intermediateselseNoneifoutput_type=="dict":returndict(output)returnoutput
classParseThenPlaceProcessor(ProcessorMixin):"""Build stage inputs and parse placement-model output text."""attributes=["parser_tokenizer","placement_tokenizer"]parser_tokenizer_class="AutoTokenizer"placement_tokenizer_class="T5Tokenizer"def__init__(self,parser_tokenizer:PreTrainedTokenizerBase|None=None,placement_tokenizer:PreTrainedTokenizerBase|None=None,dataset_name:ParseThenPlaceDatasetName|str=ParseThenPlaceDatasetName.rico,canvas_size:tuple[int,int]|None=None,id2label:dict[int,str]|None=None,)->None:"""Initialize tokenizer handles and dataset metadata."""dataset=normalize_dataset_name(dataset_name)self.parser_tokenizer=parser_tokenizerself.placement_tokenizer=placement_tokenizerself.dataset_name=str(dataset)self.canvas_size=canvas_sizeorcanvas_size_for_dataset(dataset)self.id2label=({int(key):str(value)forkey,valueinid2label.items()}ifid2labelisnotNoneelseid2label_for_dataset(dataset))self.label2id={label.lower():idxforidx,labelinself.id2label.items()}# Keep released spellings as aliases because RICO uses lower-case labels.self.label2id.update(label2id_for_dataset(dataset))ifparser_tokenizerisnotNoneandplacement_tokenizerisnotNone:super().__init__(parser_tokenizer=parser_tokenizer,placement_tokenizer=placement_tokenizer,)@classmethoddeffrom_config(cls,dataset_name:ParseThenPlaceDatasetName|str=ParseThenPlaceDatasetName.rico,*,canvas_size:tuple[int,int]|None=None,id2label:dict[int,str]|None=None,)->ParseThenPlaceProcessor:"""Construct metadata-only processor for tests and local smoke checks."""returncls(parser_tokenizer=None,placement_tokenizer=None,dataset_name=dataset_name,canvas_size=canvas_size,id2label=id2label,)defpreprocess_prompt(self,prompt:str,*,replace_explicit_value:bool=True,)->PromptEncoding:"""Apply the released text normalization used before stage-1 parsing. Args: prompt: Natural-language text prompt. replace_explicit_value: Whether quoted values should be replaced by deterministic ``value_N`` placeholders. Returns: Normalized prompt and the placeholder recovery map. """result=prompt.replace("#","").strip().lower()result=(result.replace("“",'"').replace("”",'"').replace("‘","'").replace("’","'"))result=_WHITESPACE_RE.sub(" ",result)ifnotreplace_explicit_value:return{"prompt":result,"value_map":None}returnself._extract_explicit_values(result)def_extract_explicit_values(self,prompt:str)->PromptEncoding:result=re.sub(r"(\w)'s\s+",r"\g<1>`s ",prompt)values=_DOUBLE_QUOTED_RE.findall(result)single_values=_SINGLE_QUOTED_RE.findall(result)iflen(single_values)==1andany(punctinsingle_values[0]forpunctin(",",".")):single_values=[]values.extend(single_values)value_map:dict[str,str]={}forvalue_idx,valueinenumerate(values):placeholder=f"value_{value_idx}"value_map[placeholder]=value.strip('"').strip("'").strip()result=result.replace(value,f'"{placeholder}"',1)result=re.sub(r"(\w)`s\s+",r"\g<1>'s ",result)result=_WHITESPACE_RE.sub(" ",result)return{"prompt":result,"value_map":value_map}def__call__(self,prompt:str|Sequence[str],*,replace_explicit_value:bool=True,return_tensors:Literal["pt"]="pt",)->BatchEncoding:"""Tokenize prompt text for the semantic parser stage."""prompts=[prompt]ifisinstance(prompt,str)elselist(prompt)encodings=[self.preprocess_prompt(item,replace_explicit_value=replace_explicit_value)foriteminprompts]texts=[item["prompt"]foriteminencodings]ifself.parser_tokenizerisNone:returnBatchEncoding({"prompt_text":texts,"value_maps":[item["value_map"]foriteminencodings],})parser_tokenizer=self.parser_tokenizertokenized=parser_tokenizer(texts,return_tensors=return_tensors,padding=True)tokenized["value_maps"]=[item["value_map"]foriteminencodings]tokenized["prompt_text"]=textsreturncast(BatchEncoding,tokenized)defpostprocess_ir(self,generated_ids:Int[torch.Tensor,"batch tokens"]|Sequence[str],*,value_maps:list[dict[str,str]|None]|None=None,)->list[str]:"""Decode and lightly normalize stage-1 logical forms."""ifisinstance(generated_ids,torch.Tensor):ifself.parser_tokenizerisNone:raiseValueError("parser_tokenizer is required to decode generated ids")parser_tokenizer=self.parser_tokenizerlogical_forms=parser_tokenizer.batch_decode(generated_ids,skip_special_tokens=True)else:logical_forms=list(generated_ids)ifvalue_mapsisNone:return[self._normalize_ir(item)foriteminlogical_forms]return[self._recover_ir_values(self._normalize_ir(item),value_map)foritem,value_mapinzip(logical_forms,value_maps,strict=True)]def_normalize_ir(self,logical_form:str)->str:result=logical_form.replace("["," [ ").replace("]"," ] ").strip().lower()return_WHITESPACE_RE.sub(" ",result)def_recover_ir_values(self,logical_form:str,value_map:dict[str,str]|None,)->str:ifnotvalue_map:returnlogical_formresult=logical_formforplaceholder,valueinvalue_map.items():recovered=value.replace("'","")result=result.replace(f"'{placeholder}'",f"'{recovered}'")result=result.replace(f" {placeholder},",f" {value},")result=result.replace(f" {placeholder}'",f" {value}'")returnresult.replace("&"," and ")defir_to_placement_inputs(self,logical_forms:Sequence[str])->list[str]:"""Convert logical forms to placement-constraint strings. Runtime keeps this method deterministic and accepts already-linearized constraints, which is also the artifact stored in stage-1 prediction JSON files. The current parity scripts do not execute the released grammar executor. """return[self._logical_form_to_constraint(item)foriteminlogical_forms]def_logical_form_to_constraint(self,logical_form:str)->str:text=_WHITESPACE_RE.sub(" ",logical_form.strip())if":"intextand"|"intext:returntextreturntextdefencode_placement_inputs(self,placement_inputs:Sequence[str],*,return_tensors:Literal["pt"]="pt",)->BatchEncoding:"""Tokenize stage-2 placement constraints."""ifself.placement_tokenizerisNone:returnBatchEncoding({"placement_text":list(placement_inputs)})placement_tokenizer=self.placement_tokenizertokenized=placement_tokenizer(list(placement_inputs),return_tensors=return_tensors,padding=True)tokenized["placement_text"]=list(placement_inputs)returncast(BatchEncoding,tokenized)defdecode_layout_sequences(self,generated_ids:Int[torch.Tensor,"batch tokens"]|Sequence[str],*,batch_size:int,num_return_sequences:int,)->list[list[str]]:"""Decode stage-2 generated ids into grouped layout strings."""ifisinstance(generated_ids,torch.Tensor):ifself.placement_tokenizerisNone:raiseValueError("placement_tokenizer is required to decode generated ids")placement_tokenizer=self.placement_tokenizerflat=placement_tokenizer.batch_decode(generated_ids,skip_special_tokens=True)else:flat=list(generated_ids)expected=batch_size*num_return_sequencesiflen(flat)!=expected:raiseValueError("Generated layout count does not match batch_size * num_return_sequences: "f"{len(flat)} != {expected}")return[flat[idx*num_return_sequences:(idx+1)*num_return_sequences]foridxinrange(batch_size)]deflayout_text_to_output(self,layout_text:Sequence[str]|Sequence[Sequence[str]],*,output_candidate:Literal["first","all","best"]="first",output_type:Literal["dataclass","dict"]="dataclass",return_intermediates:bool=False,)->LayoutGenerationOutput|ParseThenPlaceOutputDict:"""Parse generated ``label left top width height`` text into schema."""candidate_groups=self._normalize_layout_text_groups(layout_text)selected=self._select_candidates(candidate_groups,output_candidate)parsed_groups=[self._parse_layout_text(item)foriteminselected]max_len=max((len(item)foriteminparsed_groups),default=0)or1bbox_rows:list[Float[torch.Tensor,"elements 4"]]=[]label_rows:list[Int[torch.Tensor,...]]=[]mask_rows:list[Bool[torch.Tensor,...]]=[]forparsedinparsed_groups:labels=torch.tensor([item["label"]foriteminparsed],dtype=torch.long)boxes=torch.tensor([item["bbox"]foriteminparsed],dtype=torch.float32)mask=torch.ones(len(parsed),dtype=torch.bool)iflen(parsed)==0:labels=torch.zeros(max_len,dtype=torch.long)boxes=torch.zeros(max_len,4,dtype=torch.float32)mask=torch.zeros(max_len,dtype=torch.bool)eliflen(parsed)<max_len:pad=max_len-len(parsed)labels=torch.nn.functional.pad(labels,(0,pad))boxes=torch.nn.functional.pad(boxes,(0,0,0,pad))mask=torch.nn.functional.pad(mask,(0,pad))label_rows.append(labels)bbox_rows.append(boxes)mask_rows.append(mask)raw_bbox=torch.stack(bbox_rows)bbox=normalize_boxes(raw_bbox,canvas_size=self.canvas_size,box_format="ltwh",)output=LayoutGenerationOutput(bbox=bbox.float(),labels=torch.stack(label_rows).long(),mask=torch.stack(mask_rows).bool(),id2label=dict(self.id2label),intermediates={"layout_text":selected,"layout_text_candidates":candidate_groups,"dataset_name":self.dataset_name,"canvas_size":self.canvas_size,}ifreturn_intermediateselseNone,)ifoutput_type=="dict":returndict(output)ifoutput_type!="dataclass":raiseValueError(f"Unsupported output_type: {output_type}")returnoutputdef_normalize_layout_text_groups(self,layout_text:Sequence[str]|Sequence[Sequence[str]],)->list[list[str]]:ifnotlayout_text:return[]first=layout_text[0]ifisinstance(first,str):return[[item]foritemincast(Sequence[str],layout_text)]return[list(item)foritemincast(Sequence[Sequence[str]],layout_text)]def_select_candidates(self,candidate_groups:list[list[str]],output_candidate:Literal["first","all","best"],)->list[str]:ifoutput_candidate=="first":return[group[0]ifgroupelse""forgroupincandidate_groups]ifoutput_candidate=="best":return[max(group,key=lambdaitem:len(self._parse_layout_text(item)))ifgroupelse""forgroupincandidate_groups]ifoutput_candidate=="all":return["\n".join(group)forgroupincandidate_groups]raiseValueError(f"Unsupported output_candidate: {output_candidate}")def_parse_layout_text(self,layout_text:str)->list[ParsedElement]:elements:list[ParsedElement]=[]formatchin_LAYOUT_PATTERN.finditer(layout_text.lower()):label=_WHITESPACE_RE.sub(" ",match.group("label")).strip()label_id=self.label2id.get(label)iflabel_idisNone:continueelements.append({"label":label_id,"bbox":[float(match.group("left")),float(match.group("top")),float(match.group("width")),float(match.group("height")),],})returnelements
def__init__(self,parser_tokenizer:PreTrainedTokenizerBase|None=None,placement_tokenizer:PreTrainedTokenizerBase|None=None,dataset_name:ParseThenPlaceDatasetName|str=ParseThenPlaceDatasetName.rico,canvas_size:tuple[int,int]|None=None,id2label:dict[int,str]|None=None,)->None:"""Initialize tokenizer handles and dataset metadata."""dataset=normalize_dataset_name(dataset_name)self.parser_tokenizer=parser_tokenizerself.placement_tokenizer=placement_tokenizerself.dataset_name=str(dataset)self.canvas_size=canvas_sizeorcanvas_size_for_dataset(dataset)self.id2label=({int(key):str(value)forkey,valueinid2label.items()}ifid2labelisnotNoneelseid2label_for_dataset(dataset))self.label2id={label.lower():idxforidx,labelinself.id2label.items()}# Keep released spellings as aliases because RICO uses lower-case labels.self.label2id.update(label2id_for_dataset(dataset))ifparser_tokenizerisnotNoneandplacement_tokenizerisnotNone:super().__init__(parser_tokenizer=parser_tokenizer,placement_tokenizer=placement_tokenizer,)
Construct metadata-only processor for tests and local smoke checks.
Source code in models/parse-then-place/src/parse_then_place/processing_parse_then_place.py
104105106107108109110111112113114115116117118119
@classmethoddeffrom_config(cls,dataset_name:ParseThenPlaceDatasetName|str=ParseThenPlaceDatasetName.rico,*,canvas_size:tuple[int,int]|None=None,id2label:dict[int,str]|None=None,)->ParseThenPlaceProcessor:"""Construct metadata-only processor for tests and local smoke checks."""returncls(parser_tokenizer=None,placement_tokenizer=None,dataset_name=dataset_name,canvas_size=canvas_size,id2label=id2label,)
defpreprocess_prompt(self,prompt:str,*,replace_explicit_value:bool=True,)->PromptEncoding:"""Apply the released text normalization used before stage-1 parsing. Args: prompt: Natural-language text prompt. replace_explicit_value: Whether quoted values should be replaced by deterministic ``value_N`` placeholders. Returns: Normalized prompt and the placeholder recovery map. """result=prompt.replace("#","").strip().lower()result=(result.replace("“",'"').replace("”",'"').replace("‘","'").replace("’","'"))result=_WHITESPACE_RE.sub(" ",result)ifnotreplace_explicit_value:return{"prompt":result,"value_map":None}returnself._extract_explicit_values(result)
def__call__(self,prompt:str|Sequence[str],*,replace_explicit_value:bool=True,return_tensors:Literal["pt"]="pt",)->BatchEncoding:"""Tokenize prompt text for the semantic parser stage."""prompts=[prompt]ifisinstance(prompt,str)elselist(prompt)encodings=[self.preprocess_prompt(item,replace_explicit_value=replace_explicit_value)foriteminprompts]texts=[item["prompt"]foriteminencodings]ifself.parser_tokenizerisNone:returnBatchEncoding({"prompt_text":texts,"value_maps":[item["value_map"]foriteminencodings],})parser_tokenizer=self.parser_tokenizertokenized=parser_tokenizer(texts,return_tensors=return_tensors,padding=True)tokenized["value_maps"]=[item["value_map"]foriteminencodings]tokenized["prompt_text"]=textsreturncast(BatchEncoding,tokenized)
defpostprocess_ir(self,generated_ids:Int[torch.Tensor,"batch tokens"]|Sequence[str],*,value_maps:list[dict[str,str]|None]|None=None,)->list[str]:"""Decode and lightly normalize stage-1 logical forms."""ifisinstance(generated_ids,torch.Tensor):ifself.parser_tokenizerisNone:raiseValueError("parser_tokenizer is required to decode generated ids")parser_tokenizer=self.parser_tokenizerlogical_forms=parser_tokenizer.batch_decode(generated_ids,skip_special_tokens=True)else:logical_forms=list(generated_ids)ifvalue_mapsisNone:return[self._normalize_ir(item)foriteminlogical_forms]return[self._recover_ir_values(self._normalize_ir(item),value_map)foritem,value_mapinzip(logical_forms,value_maps,strict=True)]
Convert logical forms to placement-constraint strings.
Runtime keeps this method deterministic and accepts already-linearized
constraints, which is also the artifact stored in stage-1 prediction JSON
files. The current parity scripts do not execute the released grammar
executor.
Source code in models/parse-then-place/src/parse_then_place/processing_parse_then_place.py
237238239240241242243244245
defir_to_placement_inputs(self,logical_forms:Sequence[str])->list[str]:"""Convert logical forms to placement-constraint strings. Runtime keeps this method deterministic and accepts already-linearized constraints, which is also the artifact stored in stage-1 prediction JSON files. The current parity scripts do not execute the released grammar executor. """return[self._logical_form_to_constraint(item)foriteminlogical_forms]
defdecode_layout_sequences(self,generated_ids:Int[torch.Tensor,"batch tokens"]|Sequence[str],*,batch_size:int,num_return_sequences:int,)->list[list[str]]:"""Decode stage-2 generated ids into grouped layout strings."""ifisinstance(generated_ids,torch.Tensor):ifself.placement_tokenizerisNone:raiseValueError("placement_tokenizer is required to decode generated ids")placement_tokenizer=self.placement_tokenizerflat=placement_tokenizer.batch_decode(generated_ids,skip_special_tokens=True)else:flat=list(generated_ids)expected=batch_size*num_return_sequencesiflen(flat)!=expected:raiseValueError("Generated layout count does not match batch_size * num_return_sequences: "f"{len(flat)} != {expected}")return[flat[idx*num_return_sequences:(idx+1)*num_return_sequences]foridxinrange(batch_size)]
deflayout_text_to_output(self,layout_text:Sequence[str]|Sequence[Sequence[str]],*,output_candidate:Literal["first","all","best"]="first",output_type:Literal["dataclass","dict"]="dataclass",return_intermediates:bool=False,)->LayoutGenerationOutput|ParseThenPlaceOutputDict:"""Parse generated ``label left top width height`` text into schema."""candidate_groups=self._normalize_layout_text_groups(layout_text)selected=self._select_candidates(candidate_groups,output_candidate)parsed_groups=[self._parse_layout_text(item)foriteminselected]max_len=max((len(item)foriteminparsed_groups),default=0)or1bbox_rows:list[Float[torch.Tensor,"elements 4"]]=[]label_rows:list[Int[torch.Tensor,...]]=[]mask_rows:list[Bool[torch.Tensor,...]]=[]forparsedinparsed_groups:labels=torch.tensor([item["label"]foriteminparsed],dtype=torch.long)boxes=torch.tensor([item["bbox"]foriteminparsed],dtype=torch.float32)mask=torch.ones(len(parsed),dtype=torch.bool)iflen(parsed)==0:labels=torch.zeros(max_len,dtype=torch.long)boxes=torch.zeros(max_len,4,dtype=torch.float32)mask=torch.zeros(max_len,dtype=torch.bool)eliflen(parsed)<max_len:pad=max_len-len(parsed)labels=torch.nn.functional.pad(labels,(0,pad))boxes=torch.nn.functional.pad(boxes,(0,0,0,pad))mask=torch.nn.functional.pad(mask,(0,pad))label_rows.append(labels)bbox_rows.append(boxes)mask_rows.append(mask)raw_bbox=torch.stack(bbox_rows)bbox=normalize_boxes(raw_bbox,canvas_size=self.canvas_size,box_format="ltwh",)output=LayoutGenerationOutput(bbox=bbox.float(),labels=torch.stack(label_rows).long(),mask=torch.stack(mask_rows).bool(),id2label=dict(self.id2label),intermediates={"layout_text":selected,"layout_text_candidates":candidate_groups,"dataset_name":self.dataset_name,"canvas_size":self.canvas_size,}ifreturn_intermediateselseNone,)ifoutput_type=="dict":returndict(output)ifoutput_type!="dataclass":raiseValueError(f"Unsupported output_type: {output_type}")returnoutput