@@ -467,6 +467,189 @@ async def _critic_run(*a, **kw):
467467 console .print (f" Run ID: [dim]{ result .metadata .get ('run_id' , 'unknown' )} [/dim]" )
468468
469469
470+ @app .command ()
471+ def batch (
472+ manifest : str = typer .Option (
473+ ..., "--manifest" , "-m" , help = "Path to batch manifest (YAML or JSON)"
474+ ),
475+ output_dir : str = typer .Option (
476+ "outputs" ,
477+ "--output-dir" ,
478+ "-o" ,
479+ help = "Parent directory for batch run (batch_<id> will be created here)" ,
480+ ),
481+ config : Optional [str ] = typer .Option (None , "--config" , help = "Path to config YAML file" ),
482+ vlm_provider : Optional [str ] = typer .Option (None , "--vlm-provider" , help = "VLM provider" ),
483+ vlm_model : Optional [str ] = typer .Option (None , "--vlm-model" , help = "VLM model name" ),
484+ image_provider : Optional [str ] = typer .Option (
485+ None , "--image-provider" , help = "Image gen provider"
486+ ),
487+ image_model : Optional [str ] = typer .Option (None , "--image-model" , help = "Image gen model name" ),
488+ iterations : Optional [int ] = typer .Option (
489+ None , "--iterations" , "-n" , help = "Refinement iterations"
490+ ),
491+ auto : bool = typer .Option (
492+ False , "--auto" , help = "Loop until critic satisfied (with safety cap)"
493+ ),
494+ max_iterations : Optional [int ] = typer .Option (
495+ None , "--max-iterations" , help = "Safety cap for --auto"
496+ ),
497+ optimize : bool = typer .Option (
498+ False , "--optimize" , help = "Preprocess inputs for better generation"
499+ ),
500+ format : str = typer .Option (
501+ "png" , "--format" , "-f" , help = "Output image format (png, jpeg, webp)"
502+ ),
503+ save_prompts : Optional [bool ] = typer .Option (
504+ None , "--save-prompts/--no-save-prompts" , help = "Save prompts per run"
505+ ),
506+ auto_download_data : bool = typer .Option (
507+ False , "--auto-download-data" , help = "Auto-download reference set if needed"
508+ ),
509+ verbose : bool = typer .Option (False , "--verbose" , "-v" , help = "Show detailed progress" ),
510+ ):
511+ """Generate multiple methodology diagrams from a manifest file (YAML or JSON)."""
512+ if format not in ("png" , "jpeg" , "webp" ):
513+ console .print (f"[red]Error: Format must be png, jpeg, or webp. Got: { format } [/red]" )
514+ raise typer .Exit (1 )
515+
516+ configure_logging (verbose = verbose )
517+ manifest_path = Path (manifest )
518+ if not manifest_path .exists ():
519+ console .print (f"[red]Error: Manifest not found: { manifest } [/red]" )
520+ raise typer .Exit (1 )
521+
522+ from paperbanana .core .batch import generate_batch_id , load_batch_manifest
523+ from paperbanana .core .utils import ensure_dir , save_json
524+
525+ try :
526+ items = load_batch_manifest (manifest_path )
527+ except (ValueError , FileNotFoundError , RuntimeError ) as e :
528+ console .print (f"[red]Error loading manifest: { e } [/red]" )
529+ raise typer .Exit (1 )
530+
531+ batch_id = generate_batch_id ()
532+ batch_dir = Path (output_dir ) / batch_id
533+ ensure_dir (batch_dir )
534+
535+ overrides = {"output_dir" : str (batch_dir ), "output_format" : format }
536+ if vlm_provider :
537+ overrides ["vlm_provider" ] = vlm_provider
538+ if vlm_model :
539+ overrides ["vlm_model" ] = vlm_model
540+ if image_provider :
541+ overrides ["image_provider" ] = image_provider
542+ if image_model :
543+ overrides ["image_model" ] = image_model
544+ if iterations is not None :
545+ overrides ["refinement_iterations" ] = iterations
546+ if auto :
547+ overrides ["auto_refine" ] = True
548+ if max_iterations is not None :
549+ overrides ["max_iterations" ] = max_iterations
550+ if optimize :
551+ overrides ["optimize_inputs" ] = True
552+ if save_prompts is not None :
553+ overrides ["save_prompts" ] = save_prompts
554+
555+ if config :
556+ settings = Settings .from_yaml (config , ** overrides )
557+ else :
558+ from dotenv import load_dotenv
559+
560+ load_dotenv ()
561+ settings = Settings (** overrides )
562+
563+ if auto_download_data :
564+ from paperbanana .data .manager import DatasetManager
565+
566+ dm = DatasetManager (cache_dir = settings .cache_dir )
567+ if not dm .is_downloaded ():
568+ console .print (" [dim]Downloading expanded reference set...[/dim]" )
569+ try :
570+ dm .download ()
571+ except Exception as e :
572+ console .print (f" [yellow]Download failed: { e } , using built-in set[/yellow]" )
573+
574+ console .print (
575+ Panel .fit (
576+ f"[bold]PaperBanana[/bold] — Batch Generation\n \n "
577+ f"Manifest: { manifest_path .name } \n "
578+ f"Items: { len (items )} \n "
579+ f"Output: { batch_dir } " ,
580+ border_style = "blue" ,
581+ )
582+ )
583+ console .print ()
584+
585+ from paperbanana .core .pipeline import PaperBananaPipeline
586+
587+ report = {"batch_id" : batch_id , "manifest" : str (manifest_path ), "items" : []}
588+ total_start = time .perf_counter ()
589+
590+ for idx , item in enumerate (items ):
591+ item_id = item ["id" ]
592+ input_path = Path (item ["input" ])
593+ if not input_path .exists ():
594+ console .print (f"[red]Skipping item '{ item_id } ': input not found: { input_path } [/red]" )
595+ report ["items" ].append (
596+ {
597+ "id" : item_id ,
598+ "input" : item ["input" ],
599+ "caption" : item ["caption" ],
600+ "run_id" : None ,
601+ "output_path" : None ,
602+ "error" : "input file not found" ,
603+ }
604+ )
605+ continue
606+ source_context = input_path .read_text (encoding = "utf-8" )
607+ gen_input = GenerationInput (
608+ source_context = source_context ,
609+ communicative_intent = item ["caption" ],
610+ diagram_type = DiagramType .METHODOLOGY ,
611+ )
612+ console .print (f"[bold]Item { idx + 1 } /{ len (items )} [/bold] — { item_id } " )
613+ pipeline = PaperBananaPipeline (settings = settings )
614+ try :
615+ result = asyncio .run (pipeline .generate (gen_input ))
616+ report ["items" ].append (
617+ {
618+ "id" : item_id ,
619+ "input" : item ["input" ],
620+ "caption" : item ["caption" ],
621+ "run_id" : result .metadata .get ("run_id" ),
622+ "output_path" : result .image_path ,
623+ "iterations" : len (result .iterations ),
624+ }
625+ )
626+ console .print (f" [green]✓[/green] [dim]{ result .image_path } [/dim]\n " )
627+ except Exception as e :
628+ console .print (f" [red]✗[/red] { e } \n " )
629+ report ["items" ].append (
630+ {
631+ "id" : item_id ,
632+ "input" : item ["input" ],
633+ "caption" : item ["caption" ],
634+ "run_id" : None ,
635+ "output_path" : None ,
636+ "error" : str (e ),
637+ }
638+ )
639+
640+ total_elapsed = time .perf_counter () - total_start
641+ report ["total_seconds" ] = round (total_elapsed , 1 )
642+ report_path = batch_dir / "batch_report.json"
643+ save_json (report , report_path )
644+
645+ succeeded = sum (1 for x in report ["items" ] if x .get ("output_path" ))
646+ console .print (
647+ f"[green]Batch complete.[/green] [dim]{ total_elapsed :.1f} s · "
648+ f"{ succeeded } /{ len (items )} succeeded[/dim]"
649+ )
650+ console .print (f" Report: [bold]{ report_path } [/bold]" )
651+
652+
470653@app .command ()
471654def plot (
472655 data : str = typer .Option (..., "--data" , "-d" , help = "Path to data file (CSV or JSON)" ),
0 commit comments