@@ -137,7 +137,9 @@ async def _async_export_diffusion(config: DiffusionExportConfig) -> dict[str, st
137137 _save_tokenizer (config .hf_model_id , output_path , hf_pipe , overwrite = config .overwrite )
138138
139139 # 4. Write pipeline.json
140- _write_pipeline_json (hf_pipe , config .hf_model_id , pipeline_type , output_path )
140+ _write_metadata_json (
141+ hf_pipe , config .hf_model_id , pipeline_type , output_path , config .compression , results
142+ )
141143
142144 # Summary
143145 logger .info ("=== Export Summary ===" )
@@ -296,33 +298,63 @@ def _save_tokenizer(model_id: str, output_path: Path, hf_pipe: Any, overwrite: b
296298
297299
298300# ---------------------------------------------------------------------------
299- # pipeline .json
301+ # metadata .json (v0.2 schema — aligned with LLM and segmenter bundles)
300302# ---------------------------------------------------------------------------
301303
304+ METADATA_VERSION = "0.2"
302305
303- def _write_pipeline_json (
304- hf_pipe : Any , model_id : str , pipeline_type : str , output_path : Path
306+
307+ def _write_metadata_json (
308+ hf_pipe : Any ,
309+ model_id : str ,
310+ pipeline_type : str ,
311+ output_path : Path ,
312+ compression : str ,
313+ exported_assets : dict [str , str ],
305314) -> None :
306- """Write pipeline.json with model metadata for the Swift pipeline."""
315+ """Write metadata.json with the v0.2 bundle schema for diffusion models."""
316+ from datetime import datetime
317+
307318 if pipeline_type == "flux2" :
308- pipeline_json = _build_flux2_pipeline_json (hf_pipe , model_id )
319+ diffusion_config = _build_flux2_config (hf_pipe , model_id )
309320 else :
310- pipeline_json = _build_sd_pipeline_json (hf_pipe , model_id , pipeline_type )
321+ diffusion_config = _build_sd_config (hf_pipe , model_id , pipeline_type )
322+
323+ # Build assets map from exported component paths
324+ assets : dict [str , str ] = {}
325+ for name , path_str in exported_assets .items ():
326+ assets [name ] = Path (path_str ).name
327+
328+ metadata = {
329+ "metadata_version" : METADATA_VERSION ,
330+ "kind" : "diffusion" ,
331+ "name" : output_path .name ,
332+ "assets" : assets ,
333+ "diffusion" : diffusion_config ,
334+ "source" : {
335+ "model_definition" : "torch" ,
336+ "hf_model_id" : model_id ,
337+ },
338+ "compression" : compression if compression != "none" else None ,
339+ "compilation" : {
340+ "date" : datetime .now ().astimezone ().isoformat (),
341+ "targets" : [],
342+ },
343+ }
311344
312- json_path = output_path / "pipeline .json"
345+ json_path = output_path / "metadata .json"
313346 with open (json_path , "w" ) as f :
314- json .dump (pipeline_json , f , indent = 2 )
315- logger .info (f"Saved pipeline .json to { json_path } " )
347+ json .dump (metadata , f , indent = 2 )
348+ logger .info (f"Saved metadata .json to { json_path } " )
316349
317350
318- def _build_flux2_pipeline_json (hf_pipe : Any , model_id : str ) -> dict :
351+ def _build_flux2_config (hf_pipe : Any , model_id : str ) -> dict :
319352 vae_config = hf_pipe .vae .config
320353 transformer_config = hf_pipe .transformer .config
321354
322355 vae_scale_power = len (vae_config .block_out_channels ) - 1
323356 vae_spatial_scale = 2 ** vae_scale_power
324357 default_sample_size = getattr (transformer_config , "default_sample_size" , 64 )
325- # FLUX.2 uses 2x2 patchification, so actual image size is doubled
326358 image_size = default_sample_size * vae_spatial_scale * 2
327359
328360 scaling_factor = getattr (vae_config , "scaling_factor" , 1.0 )
@@ -333,7 +365,6 @@ def _build_flux2_pipeline_json(hf_pipe: Any, model_id: str) -> dict:
333365 rope_theta = getattr (transformer_config , "rope_theta" , 2000.0 )
334366
335367 return {
336- "model_id" : model_id ,
337368 "type" : "flux2" ,
338369 "prediction_type" : "flow_matching" ,
339370 "encoder_scale_factor" : scaling_factor ,
@@ -349,7 +380,7 @@ def _build_flux2_pipeline_json(hf_pipe: Any, model_id: str) -> dict:
349380 }
350381
351382
352- def _build_sd_pipeline_json (hf_pipe : Any , model_id : str , pipeline_type : str = "sd" ) -> dict :
383+ def _build_sd_config (hf_pipe : Any , model_id : str , pipeline_type : str = "sd" ) -> dict :
353384 scheduler_config = hf_pipe .scheduler .config
354385 vae_config = hf_pipe .vae .config
355386
@@ -364,8 +395,7 @@ def _build_sd_pipeline_json(hf_pipe: Any, model_id: str, pipeline_type: str = "s
364395 vae_spatial_scale = 2 ** vae_scale_power
365396 image_size = denoiser_config .sample_size * vae_spatial_scale
366397
367- pipeline_json : dict [str , Any ] = {
368- "model_id" : model_id ,
398+ config : dict [str , Any ] = {
369399 "type" : "stable-diffusion-3" if is_sd3 else "stable-diffusion" ,
370400 "prediction_type" : "flow" if is_sd3 else prediction_type ,
371401 "encoder_scale_factor" : scaling_factor ,
@@ -376,18 +406,15 @@ def _build_sd_pipeline_json(hf_pipe: Any, model_id: str, pipeline_type: str = "s
376406 "default_steps" : 28 if is_sd3 else 50 ,
377407 }
378408
379- if is_sd3 :
380- # Autodetect also works for SD3 (recognizes MMDiT / text_encoder_2
381- # substrings); emitting explicit paths keeps pipeline.json self-
382- # documenting and guards against future detect() changes.
383- pipeline_json ["components" ] = {
384- "text_encoder" : "TextEncoder.aimodel" ,
385- "text_encoder_2" : "TextEncoder2.aimodel" ,
386- "unet" : "MMDiT.aimodel" ,
387- "vae_decoder" : "VAEDecoder.aimodel" ,
388- }
389-
390- return pipeline_json
409+ # Include scheduler defaults for reproducibility
410+ config ["scheduler" ] = {
411+ "training_steps" : getattr (scheduler_config , "num_train_timesteps" , 1000 ),
412+ "beta_start" : getattr (scheduler_config , "beta_start" , 0.00085 ),
413+ "beta_end" : getattr (scheduler_config , "beta_end" , 0.012 ),
414+ "beta_schedule" : getattr (scheduler_config , "beta_schedule" , "scaled_linear" ),
415+ }
416+
417+ return config
391418
392419
393420# ---------------------------------------------------------------------------
0 commit comments