liquidrandom 0.3.2__tar.gz → 0.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. liquidrandom-0.3.4/IMAGE_GEN.md +189 -0
  2. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/PKG-INFO +1 -1
  3. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/pyproject.toml +1 -1
  4. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/categories.py +40 -0
  5. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/generate.py +215 -2
  6. liquidrandom-0.3.4/seed_generation/image_categories.py +242 -0
  7. liquidrandom-0.3.4/seed_generation/image_config.py +15 -0
  8. liquidrandom-0.3.4/seed_generation/image_grounding.py +38 -0
  9. liquidrandom-0.3.4/seed_generation/image_sampler.py +420 -0
  10. liquidrandom-0.3.4/seed_generation/image_test_gen.py +243 -0
  11. liquidrandom-0.3.4/seed_generation/image_viewer.py +285 -0
  12. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/llm.py +2 -1
  13. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/pyproject.toml +1 -0
  14. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/uploader.py +85 -3
  15. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/uv.lock +17 -0
  16. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/validator.py +2 -1
  17. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/__init__.py +48 -2
  18. liquidrandom-0.3.4/src/liquidrandom/_loader.py +92 -0
  19. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/_registry.py +11 -0
  20. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/__init__.py +10 -0
  21. liquidrandom-0.3.4/src/liquidrandom/models/aerial.py +61 -0
  22. liquidrandom-0.3.4/src/liquidrandom/models/indoor.py +60 -0
  23. liquidrandom-0.3.4/src/liquidrandom/models/navigation.py +58 -0
  24. liquidrandom-0.3.4/src/liquidrandom/models/people_scene.py +60 -0
  25. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/tool_group.py +4 -2
  26. liquidrandom-0.3.4/src/liquidrandom/models/warehouse.py +59 -0
  27. liquidrandom-0.3.4/tests/test_image_models.py +248 -0
  28. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/tests/test_loader.py +7 -1
  29. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/uv.lock +1 -1
  30. liquidrandom-0.3.2/src/liquidrandom/_loader.py +0 -48
  31. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/.gitignore +0 -0
  32. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/.python-version +0 -0
  33. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/CLAUDE.md +0 -0
  34. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/ISSUES_2.md +0 -0
  35. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/ISSUES_AND_FIXES.md +0 -0
  36. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/README.md +0 -0
  37. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/STEP2.md +0 -0
  38. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/README.md +0 -0
  39. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/config.py +0 -0
  40. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/dedup.py +0 -0
  41. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/generate_hardcoded_parquet.py +0 -0
  42. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/hardcoded_instruction_complexities.py +0 -0
  43. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/hardcoded_reasoning_patterns.py +0 -0
  44. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/inspect_samples.py +0 -0
  45. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/run.sh +0 -0
  46. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/sampler.py +0 -0
  47. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/state.py +0 -0
  48. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/taxonomy.py +0 -0
  49. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/tool_sampler.py +0 -0
  50. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/seed_generation/tool_validator.py +0 -0
  51. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/_detail.py +0 -0
  52. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/coding_task.py +0 -0
  53. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/domain.py +0 -0
  54. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/emotional_state.py +0 -0
  55. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/instruction_complexity.py +0 -0
  56. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/job.py +0 -0
  57. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/language.py +0 -0
  58. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/math_category.py +0 -0
  59. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/persona.py +0 -0
  60. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/reasoning_pattern.py +0 -0
  61. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/scenario.py +0 -0
  62. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/science_topic.py +0 -0
  63. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/models/writing_style.py +0 -0
  64. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/src/liquidrandom/py.typed +0 -0
  65. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/tests/__init__.py +0 -0
  66. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/tests/test_e2e.py +0 -0
  67. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/tests/test_models.py +0 -0
  68. {liquidrandom-0.3.2 → liquidrandom-0.3.4}/tests/test_tool_group_models.py +0 -0
@@ -0,0 +1,189 @@
1
+ # liquidrandom - Image as seed data type project
2
+
3
+ In this project we are using Replicate AI API to generate images as additional seed data type for liquidrandom.
4
+ eg. each sample has an image and text attributes that we can use for the prompting/data generation part,
5
+ eg. an image and text attributes such as "number_of_people": int, "setting": str,
6
+
7
+ Instead of having just one big "image" type we want to actually differentiate between different images groups or categories.
8
+ For instance, "humans" as a category of images that include people in natural everyday situations and settings.
9
+ Another category would be areal images of landscapes, cities, etc, eg. from an airplane, drone or satellite.
10
+
11
+ Your task is to come up with about 5 different categories of images that could be useful for typical on-device vision-language tasks, eg. "animals" is probably not a good category.
12
+ Use the ask-user tool to give me a few options that you think are suitable.
13
+
14
+
15
+ Your second task is then to use these categoreis to generate test images using the Replicate API (REPLICATE_API_KEY is defined, not "TOKEN" but "KEY") with the different models shown below.
16
+ This is important since we want to manually check "human-in-the-loop" is the quality for each pair of category X model is acceptable for our use case. We want to make sure that the images generated are of good quality and relevant to the category they belong to.
17
+
18
+ ## Feedback from user about task 2:
19
+
20
+ - Image generation models product per default "too sterile" images with too much focus on aesthetics, which is not ideal for our use case. We want to have more "natural" or "real" images that are more representative of real-world scenarios. Thus, we need to adjust the prompting to include more instructions about this. Two things: it should be "randomized" such that each "real grounding" applied to the image prompt is not the same for each image, and 2, specific imperfections and other types of making it real. Eg. each image has a "depth of field" with blur for aesthetics but we don't want this, i.e. less cinematic.
21
+ - The dashboards and screenshot images are not include enough instructions to make the image an actual screenshot, not a photo of a dashbaord. Make sure in each prompt in this category we include insturction that this is a screenshot.
22
+ - for the "documents" category, the example is good but the images are too "zoomed in". Include explicit instructions in the prompt to make sure the generated images are more "zoomed out" and include the surrounding environment.
23
+ - The warehouse category looks very "bland" (probably since these models are not trained on a lot of back warehouse images). Thus include explictly the instructions on what should be there, eg. objects or people.
24
+
25
+ ## Feedback from user about task 2 - Part 2:
26
+ We will keep the following categories:
27
+ - Aerial images
28
+ - Indoor (but also cover partially outdoor and outdoor scenes, eg. entrance, etc)
29
+ - People scenes
30
+ - Warehouse
31
+ - Street, but make it not only "urban" but the theme should be "navigation path", eg. also suburban, rual, even a hiking path in the woods or bike path in a park, etc.), indoor navigation as well, eg. a hallway with a navigation path, etc. In essence, it should "Navigation"
32
+
33
+ We will drop the "documents" and the "dashboards/screenshots" due to high halluciation rate
34
+
35
+ Your third task is then to adjust the seed data generation process to produce around 10k images in each of the categories using the taxonomy tree approach we used in the text-only seed data generation process.
36
+
37
+ Your fourth tasks is to visualize these data, again for human-in-the-loop checking.
38
+
39
+
40
+ Your fifth tasks is then to adjust the `liquidrandom` public package to support the image seed data types.
41
+
42
+ ```
43
+ import replicate
44
+
45
+ input = {
46
+ "prompt": "A striking fashion editorial photograph of a model wearing an elaborate haute couture gown made entirely of living flowers — white peonies, pale pink roses, and trailing jasmine. She stands in a misty English garden at dawn. The dress cascades to the ground and merges with the garden floor. Dewdrops on every petal catch the first light. Her hair is pulled back severely, face serene, skin porcelain. A single monarch butterfly rests on her shoulder. The background is a soft impressionist blur of green hedgerows and pale sky. Shot on Phase One IQ4 150MP, every stamen and pistil razor sharp.",
47
+ "aspect_ratio": "2:3"
48
+ }
49
+
50
+ output = replicate.run(
51
+ "bytedance/seedream-5-lite",
52
+ input=input
53
+ )
54
+
55
+ # To access the file URLs:
56
+ print(output[0].url)
57
+ #=> "https://replicate.delivery/.../output_0.png"
58
+
59
+ # To write the files to disk:
60
+ for index, item in enumerate(output):
61
+ with open(f"output_{index}.png", "wb") as file:
62
+ file.write(item.read())
63
+ #=> output_0.png written to disk
64
+ ```
65
+
66
+ ```
67
+ import replicate
68
+
69
+ input = {
70
+ "prompt": "Bookstore window display. A sign displays “New Arrivals This Week”. Below, a shelf tag with the text “Best-Selling Novels Here”. To the side, a colorful poster advertises “Author Meet And Greet on Saturday” with a central portrait of the author. There are four books on the bookshelf, namely “The light between worlds” “When stars are scattered” “The slient patient” “The night circus”",
71
+ "guidance": 4,
72
+ "num_inference_steps": 50
73
+ }
74
+
75
+ output = replicate.run(
76
+ "qwen/qwen-image",
77
+ input=input
78
+ )
79
+
80
+ # To access the file URLs:
81
+ print(output[0].url)
82
+ #=> "https://replicate.delivery/.../output_0.webp"
83
+
84
+ # To write the files to disk:
85
+ for index, item in enumerate(output):
86
+ with open(f"output_{index}.webp", "wb") as file:
87
+ file.write(item.read())
88
+ #=> output_0.webp written to disk
89
+ ```
90
+
91
+
92
+ ```
93
+ import replicate
94
+
95
+ input = {
96
+ "size": "4K",
97
+ "prompt": "A warm, nostalgic film-style interior of a cozy café, shot on 35mm-inspired digital photography with soft afternoon sunlight filtering through the front windows. Wooden shelves display neatly arranged ceramics, pastries, and coffee beans. Hand-painted signage on the main interior window reads ‘Seedream 4.5’ in clean, classic lettering, similar to boutique branding. A vintage bicycle with a wicker basket is visible outside the entrance, casting soft shadows on the floor. Rich textures, natural light, warm tones, subtle grain, and calm neighborhood-café ambiance.",
98
+ "aspect_ratio": "16:9"
99
+ }
100
+
101
+ output = replicate.run(
102
+ "bytedance/seedream-4.5",
103
+ input=input
104
+ )
105
+
106
+ # To access the file URLs:
107
+ print(output[0].url)
108
+ #=> "https://replicate.delivery/.../output_0.jpg"
109
+
110
+ # To write the files to disk:
111
+ for index, item in enumerate(output):
112
+ with open(f"output_{index}.jpg", "wb") as file:
113
+ file.write(item.read())
114
+ #=> output_0.jpg written to disk
115
+ ```
116
+
117
+ ```
118
+ import replicate
119
+
120
+ input = {
121
+ "prompt": "A close-up portrait of a leopard with distinctive spotted pattern and intense eyes",
122
+ "aspect_ratio": "3:4"
123
+ }
124
+
125
+ output = replicate.run(
126
+ "minimax/image-01",
127
+ input=input
128
+ )
129
+
130
+ # To access the file URLs:
131
+ print(output[0].url)
132
+ #=> "https://replicate.delivery/.../output_0.jpeg"
133
+
134
+ # To write the files to disk:
135
+ for index, item in enumerate(output):
136
+ with open(f"output_{index}.jpeg", "wb") as file:
137
+ file.write(item.read())
138
+ #=> output_0.jpeg written to disk
139
+ ```
140
+
141
+
142
+ ```
143
+ import replicate
144
+
145
+ input = {
146
+ "prompt": "An illustration of a pink and brown butterfly on a pink background, with the black text “Hunyuan Image 3 on Replicate” below"
147
+ }
148
+
149
+ output = replicate.run(
150
+ "tencent/hunyuan-image-3",
151
+ input=input
152
+ )
153
+
154
+ # To access the file URLs:
155
+ print(output[0].url)
156
+ #=> "https://replicate.delivery/.../output_0.webp"
157
+
158
+ # To write the files to disk:
159
+ for index, item in enumerate(output):
160
+ with open(f"output_{index}.webp", "wb") as file:
161
+ file.write(item.read())
162
+ #=> output_0.webp written to disk
163
+ ```
164
+
165
+
166
+ ```
167
+ import replicate
168
+
169
+ input = {
170
+ "prompt": "Replace the color of the car to blue",
171
+ "aspect_ratio": "match_input_image",
172
+ "input_images": ["https://replicate.delivery/pbxt/O7kbtH7wgLIItlCyeWjm0fgPpx7OpGsT9VbYyxBXnfieVxQe/woman-by-car.jpg"],
173
+ "output_format": "jpg"
174
+ }
175
+
176
+ output = replicate.run(
177
+ "black-forest-labs/flux-2-dev",
178
+ input=input
179
+ )
180
+
181
+ # To access the file URL:
182
+ print(output.url)
183
+ #=> "https://replicate.delivery/.../output.jpeg"
184
+
185
+ # To write the file to disk:
186
+ with open("output.jpeg", "wb") as file:
187
+ file.write(output.read())
188
+ #=> output.jpeg written to disk
189
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: liquidrandom
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Pseudo-random seed data generation for ML/LLM training diversity
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "liquidrandom"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "Pseudo-random seed data generation for ML/LLM training diversity"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -503,4 +503,44 @@ CATEGORY_CONFIGS: dict[str, CategoryConfig] = {
503
503
  generation_prompt_template="", # Not used; tool_sampler has its own prompts
504
504
  specificity_guidance="Each tool group should represent a coherent set of related API functions with inter-tool dependencies.",
505
505
  ),
506
+ "physical_tool_group": CategoryConfig(
507
+ name="physical_tool_group",
508
+ display_name="Physical-AI Tool Groups",
509
+ fields=[
510
+ FieldSpec("domain", "string", "Tool group domain (e.g. 'Smart Home Lighting Control')"),
511
+ FieldSpec("description", "string", "What this tool group does"),
512
+ FieldSpec("taxonomy_path", "string", "Taxonomy path this group belongs to"),
513
+ FieldSpec("tools_json", "string", "JSON-serialized list of ToolFunction dicts"),
514
+ ],
515
+ taxonomy_seed_prompt=(
516
+ "Generate a taxonomy of tool/function calling domains for PHYSICAL and EMBODIED "
517
+ "AI systems (not SaaS or cloud APIs). These represent groups of related tools that "
518
+ "an embodied agent — a voice assistant, robot, vehicle, or wearable AI — would use "
519
+ "together to sense, act on, and reason about the physical world.\n\n"
520
+ "Root categories:\n"
521
+ "- Smart Home Voice Assistants (lighting, climate, security, appliances, media & entertainment)\n"
522
+ "- Mobile Service Robots (indoor delivery, floor cleaning, facility inspection)\n"
523
+ "- Manipulator Robots (arm control, grasping, assembly, pick-and-place, sorting)\n"
524
+ "- Humanoid Robots (bipedal locomotion, whole-body manipulation, human interaction)\n"
525
+ "- Autonomous Vehicles (perception, motion planning, vehicle control, V2X communication)\n"
526
+ "- Sidewalk & Last-Mile Delivery Robots (navigation, obstacle handling, handoff)\n"
527
+ "- Aerial Drones (mission planning, inspection, cinematography, precision agriculture)\n"
528
+ "- Underwater & Marine Robots (ROV/AUV operations, bathymetric mapping)\n"
529
+ "- Industrial & Factory Automation (robotic cells, AMRs, conveyor & PLC control)\n"
530
+ "- Agricultural Robotics (field navigation, harvesting, targeted spraying)\n"
531
+ "- Medical & Surgical Robots (teleoperation, imaging-guided intervention)\n"
532
+ "- Wearable & Personal AI (AR glasses assistants, smart earbuds, fitness devices)\n\n"
533
+ "Each leaf should be a specific embodied tool domain (e.g. 'Quadruped locomotion "
534
+ "gait switching on uneven terrain', not just 'robotics'). Focus on real physical "
535
+ "capabilities: sensing, actuation, state estimation, planning, safety — NOT cloud "
536
+ "services, databases, or web APIs."
537
+ ),
538
+ generation_prompt_template="", # Not used; tool_sampler has its own prompts
539
+ specificity_guidance=(
540
+ "Each tool group should represent a coherent set of physical-world capabilities "
541
+ "(sensing + actuation + state) that an embodied AI agent would use together, with "
542
+ "inter-tool state/data dependencies (e.g. a pose returned by a localization tool is "
543
+ "consumed by a navigation tool, or a detected object handle flows into a grasp planner)."
544
+ ),
545
+ ),
506
546
  }
@@ -26,6 +26,16 @@ from config import (
26
26
  DEFAULT_TOOL_TAXONOMY_DEPTH,
27
27
  STATE_FILE,
28
28
  )
29
+ from image_categories import IMAGE_CATEGORY_CONFIGS
30
+ from image_config import (
31
+ DEFAULT_IMAGE_BATCH_SIZE,
32
+ DEFAULT_IMAGE_CONCURRENCY,
33
+ DEFAULT_IMAGE_DEDUP_THRESHOLD,
34
+ DEFAULT_IMAGE_K,
35
+ DEFAULT_IMAGE_N,
36
+ DEFAULT_IMAGE_SAMPLES_PER_LEAF,
37
+ DEFAULT_IMAGE_TAXONOMY_DEPTH,
38
+ )
29
39
  from llm import create_client
30
40
  from sampler import generate_samples
31
41
  from state import RunState
@@ -349,7 +359,7 @@ def generate_tools(
349
359
  output_dir: str = typer.Option("output", "--output-dir", help="Output directory"),
350
360
  ) -> None:
351
361
  """Generate tool group seed data with parameter variations."""
352
- resolved = categories if categories else ["tool_group"]
362
+ resolved = categories if categories else ["tool_group", "physical_tool_group"]
353
363
  for cat in resolved:
354
364
  if cat not in CATEGORY_CONFIGS:
355
365
  console.print(f"[red]Unknown category: {cat}[/red]")
@@ -370,15 +380,218 @@ def generate_tools(
370
380
  )
371
381
 
372
382
 
383
+ async def _run_generate_images(
384
+ categories: list[str],
385
+ n: int,
386
+ k: int,
387
+ batch_size: int,
388
+ image_concurrency: int,
389
+ taxonomy_depth: int,
390
+ samples_per_leaf: int,
391
+ dedup_threshold: float,
392
+ output_dir: str,
393
+ resume: bool,
394
+ ) -> None:
395
+ from image_sampler import generate_image_samples
396
+
397
+ client = create_client()
398
+ state_path = str(Path(output_dir) / "state.json")
399
+ # Always load existing state to preserve progress from previous runs
400
+ state = RunState.load(state_path)
401
+
402
+ results: dict[str, dict[str, int | float]] = {}
403
+ total_start = time.time()
404
+
405
+ console.print(f"\n[bold]Generating image seed data for {len(categories)} categories[/bold]")
406
+ console.print(f" Target: {n} images per category, {k} prompts per LLM call")
407
+ console.print(f" Taxonomy: depth {taxonomy_depth}, {samples_per_leaf} images/leaf")
408
+ console.print(f" Image concurrency: {image_concurrency}, dedup threshold: {dedup_threshold}")
409
+ console.print()
410
+
411
+ for i, cat_name in enumerate(categories, 1):
412
+ cat_config = IMAGE_CATEGORY_CONFIGS[cat_name]
413
+ cat_state = state.get_category(cat_name)
414
+ cat_start = time.time()
415
+
416
+ console.print(
417
+ f"[bold cyan][{i}/{len(categories)}] {cat_config.display_name}[/bold cyan]"
418
+ )
419
+
420
+ # Phase 1: Taxonomy (reuses existing taxonomy.py)
421
+ if not cat_state.taxonomy_done or not resume:
422
+ try:
423
+ root = await generate_taxonomy(
424
+ client,
425
+ cat_config,
426
+ target_samples=n,
427
+ samples_per_leaf=samples_per_leaf,
428
+ max_depth=taxonomy_depth,
429
+ batch_size=batch_size,
430
+ output_dir=output_dir,
431
+ )
432
+ cat_state.taxonomy_done = True
433
+ state.save(state_path)
434
+ except Exception as e:
435
+ console.print(f" [red]Taxonomy generation failed: {e}[/red]")
436
+ continue
437
+ else:
438
+ from taxonomy import load_taxonomy
439
+ root = load_taxonomy(output_dir, cat_name)
440
+ if root is None:
441
+ console.print(f" [red]No saved taxonomy found, regenerating[/red]")
442
+ cat_state.taxonomy_done = False
443
+ continue
444
+
445
+ # Phase 2+3: Image prompt generation + Replicate image generation
446
+ if not cat_state.generation_done or not resume:
447
+ try:
448
+ total = await generate_image_samples(
449
+ client,
450
+ root,
451
+ cat_config,
452
+ target_samples=n,
453
+ k=k,
454
+ batch_size=batch_size,
455
+ image_concurrency=image_concurrency,
456
+ dedup_threshold=dedup_threshold,
457
+ output_dir=output_dir,
458
+ )
459
+ # Only mark done if we reached the target
460
+ cat_state.generation_done = total >= n
461
+ cat_state.total_samples = total
462
+ state.save(state_path)
463
+ except KeyboardInterrupt:
464
+ console.print("\n [yellow]Interrupted! Saving state...[/yellow]")
465
+ state.save(state_path)
466
+ console.print(
467
+ f" [yellow]Resume with: python generate.py generate-images --resume "
468
+ f"--output-dir {output_dir}[/yellow]"
469
+ )
470
+ raise
471
+ except Exception as e:
472
+ console.print(f" [red]Image generation failed: {e}[/red]")
473
+ state.save(state_path)
474
+ continue
475
+ else:
476
+ total = cat_state.total_samples
477
+ console.print(
478
+ f" [green]Already complete ({total} images)[/green]"
479
+ )
480
+
481
+ elapsed = time.time() - cat_start
482
+ results[cat_name] = {"total": total, "time": elapsed}
483
+ console.print()
484
+
485
+ # Summary table
486
+ total_elapsed = time.time() - total_start
487
+ table = Table(title="Image Generation Summary")
488
+ table.add_column("Category", style="cyan")
489
+ table.add_column("Target", justify="right")
490
+ table.add_column("Generated", justify="right")
491
+ table.add_column("Time", justify="right")
492
+
493
+ for cat_name in categories:
494
+ if cat_name in results:
495
+ r = results[cat_name]
496
+ table.add_row(
497
+ IMAGE_CATEGORY_CONFIGS[cat_name].display_name,
498
+ str(n),
499
+ str(r["total"]),
500
+ f"{r['time']:.1f}s",
501
+ )
502
+
503
+ console.print(table)
504
+ console.print(f"\n[bold]Total time: {total_elapsed:.1f}s[/bold]")
505
+
506
+
507
+ @app.command(name="generate-images")
508
+ def generate_images(
509
+ n: int = typer.Option(DEFAULT_IMAGE_N, "--n", help="Target images per category"),
510
+ k: int = typer.Option(DEFAULT_IMAGE_K, "--k", help="Prompts per LLM call"),
511
+ batch_size: int = typer.Option(
512
+ DEFAULT_IMAGE_BATCH_SIZE, "--batch-size", help="Concurrent LLM calls"
513
+ ),
514
+ image_concurrency: int = typer.Option(
515
+ DEFAULT_IMAGE_CONCURRENCY, "--image-concurrency", help="Concurrent Replicate API calls"
516
+ ),
517
+ taxonomy_depth: int = typer.Option(
518
+ DEFAULT_IMAGE_TAXONOMY_DEPTH, "--taxonomy-depth", help="Max taxonomy tree depth"
519
+ ),
520
+ samples_per_leaf: int = typer.Option(
521
+ DEFAULT_IMAGE_SAMPLES_PER_LEAF, "--samples-per-leaf", help="Target images per leaf"
522
+ ),
523
+ dedup_threshold: float = typer.Option(
524
+ DEFAULT_IMAGE_DEDUP_THRESHOLD, "--dedup-threshold", help="Jaccard similarity threshold"
525
+ ),
526
+ categories: Optional[list[str]] = typer.Option(
527
+ None, "--categories", help="Image categories to generate (default: all)"
528
+ ),
529
+ resume: bool = typer.Option(False, "--resume", help="Resume from checkpoint"),
530
+ output_dir: str = typer.Option("output", "--output-dir", help="Output directory"),
531
+ ) -> None:
532
+ """Generate image seed data for all or selected image categories."""
533
+ if not categories:
534
+ resolved = list(IMAGE_CATEGORY_CONFIGS.keys())
535
+ else:
536
+ for cat in categories:
537
+ if cat not in IMAGE_CATEGORY_CONFIGS:
538
+ console.print(f"[red]Unknown image category: {cat}[/red]")
539
+ console.print(
540
+ f"Available: {', '.join(sorted(IMAGE_CATEGORY_CONFIGS.keys()))}"
541
+ )
542
+ raise typer.Exit(1)
543
+ resolved = categories
544
+ asyncio.run(
545
+ _run_generate_images(
546
+ categories=resolved,
547
+ n=n,
548
+ k=k,
549
+ batch_size=batch_size,
550
+ image_concurrency=image_concurrency,
551
+ taxonomy_depth=taxonomy_depth,
552
+ samples_per_leaf=samples_per_leaf,
553
+ dedup_threshold=dedup_threshold,
554
+ output_dir=output_dir,
555
+ resume=resume,
556
+ )
557
+ )
558
+
559
+
560
+ @app.command(name="review-images")
561
+ def review_images(
562
+ category: str = typer.Argument(help="Image category to review"),
563
+ max_images: int = typer.Option(500, "--max-images", help="Max images to include in gallery"),
564
+ output_dir: str = typer.Option("output", "--output-dir", help="Output directory"),
565
+ ) -> None:
566
+ """Generate an HTML gallery page for reviewing generated images."""
567
+ from image_viewer import generate_review_html
568
+
569
+ if category not in IMAGE_CATEGORY_CONFIGS:
570
+ console.print(f"[red]Unknown image category: {category}[/red]")
571
+ console.print(
572
+ f"Available: {', '.join(sorted(IMAGE_CATEGORY_CONFIGS.keys()))}"
573
+ )
574
+ raise typer.Exit(1)
575
+ path = generate_review_html(output_dir, category, max_images=max_images)
576
+ console.print(f"\n[bold]Open in browser: file://{path.resolve()}[/bold]")
577
+
578
+
373
579
  @app.command(name="upload-only")
374
580
  def upload_only(
375
581
  output_dir: str = typer.Option("output", "--output-dir", help="Output directory"),
376
582
  repo_id: str = typer.Option(
377
583
  "mlech26l/liquidrandom-data", "--repo-id", help="HuggingFace repo ID"
378
584
  ),
585
+ skip_images: bool = typer.Option(
586
+ False,
587
+ "--skip-images",
588
+ help="Skip image category consolidation/upload (any existing remote image parquets stay untouched; README will only list text categories)",
589
+ ),
379
590
  ) -> None:
380
591
  """Consolidate samples and upload to HuggingFace."""
381
- consolidate_and_upload(output_dir=output_dir, repo_id=repo_id)
592
+ consolidate_and_upload(
593
+ output_dir=output_dir, repo_id=repo_id, skip_images=skip_images
594
+ )
382
595
 
383
596
 
384
597
  if __name__ == "__main__":