zer0-image-generator 0.3.0 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24975e688896196e8e883c15ed17d09e5856a9b68f18b47b347bc61d701cc265
4
- data.tar.gz: da2d123ed3067f36c2e5962bd3a351a9d1ae48e1fd97bfe6dbcccb23aac53e52
3
+ metadata.gz: 8abc47b88bb6730b91b0358f17328e477db2a16d56a315ac8a7189bc54e6471d
4
+ data.tar.gz: 68d3d6064d3fe7d709a1957baf245dbc12a7c02e6d9e81569f639ea140a5647a
5
5
  SHA512:
6
- metadata.gz: 2add913663a1f13bfebc00746b3ace4ebf5c4e096fd34ee1db89754245762d011e1a2ca8173ed2f0aefbf46abdacb041625f9ac916f9b8e48aa13c6c38c87467
7
- data.tar.gz: e275f913cfeb197728138bc154a6963e31b1cdf0a1b11081a8526ef002667dfa504b1c7a0e55bac799a5977a599995012f2fe0f66aa28a3e5b28d599244c16a1
6
+ metadata.gz: 25b6769e4645e195e6a776790f3735cc608d5d76bc4057510de0a9e5cdeec8c31701840fbe8d893017fc159a932f29c3dca59031cf63a5e6729efcff63b58c54
7
+ data.tar.gz: 00aab1bfadf7cf22f32ca7c4abe3b4f02de118d025c8441e107928e1edd9b91ddfd848bd09218c8bfa445aaa5742f8d43577e6115f5b88346ba45dbb567e0148
data/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-07-15
9
+
10
+ ### Added
11
+
12
+ - `claude_effort` knob (default `low`): the Claude art-brief and vision-review
13
+ calls now send `output_config: {effort: ...}` — short creative tasks don't
14
+ need deep reasoning, and `low` substantially cuts per-image orchestration
15
+ latency. Configurable per site (`preview_images.claude_effort`); `''`
16
+ disables. Gracefully retries without the parameter on models that reject it.
17
+
18
+ ### Changed
19
+
20
+ - README documents the batch-performance contract: one `--collection` run
21
+ parallelizes across files (`--parallel N`, default 4); per-file loops don't.
22
+
8
23
  ## [0.3.0] - 2026-07-14
9
24
 
10
25
  ### Added
data/README.md CHANGED
@@ -87,6 +87,9 @@ preview_images:
87
87
  output_dir: assets/images/previews
88
88
  prompt_engine: claude # claude analyzes each article | template
89
89
  review_engine: claude # claude vision-reviews each render | none
90
+ claude_effort: low # output_config.effort for the brief/review calls
91
+ # (low = fastest, right-sized for short briefs;
92
+ # high = deepest; '' sends no effort parameter)
90
93
  # --- portability knobs (the zer0-isms, now configurable) ---
91
94
  collections: [posts] # list of collections, or 'auto' to discover
92
95
  # them from Jekyll's own `collections:` map
@@ -130,6 +133,21 @@ preview_images:
130
133
  # front_matter_key/preview + /assets prefixing are already the defaults
131
134
  ```
132
135
 
136
+ ### Batch performance
137
+
138
+ The engine parallelizes **within one invocation** (4 workers by default, `-j N`
139
+ to change). For many files, one collection run beats a loop of single-file
140
+ runs — the loop pays process startup per file and never engages the pool:
141
+
142
+ ```bash
143
+ bundle exec jekyll preview-images --collection posts --parallel 6 # ✓ parallel
144
+ for f in ...; do jekyll preview-images -f "$f"; done # ✗ serial
145
+ ```
146
+
147
+ Claude orchestration runs at `claude_effort: low` by default — right-sized for
148
+ a short art brief and a render verdict. Raise it (or set `claude_model`) if you
149
+ want deeper art direction at the cost of latency.
150
+
133
151
  ### Per-collection styles
134
152
 
135
153
  Give each collection its own visual identity — tutorials render differently
@@ -128,6 +128,8 @@ DEFAULTS: Dict[str, Any] = {
128
128
  "prompt_engine": "claude", # claude analyzes the article; falls back to template
129
129
  "review_engine": "claude", # claude vision-reviews the render; `none` disables
130
130
  "claude_model": "", # empty → DEFAULT_CLAUDE_MODEL
131
+ "claude_effort": "low", # output_config.effort for brief/review calls
132
+ # (short creative tasks; "" sends no effort)
131
133
  }
132
134
 
133
135
  # Enhance mode (OpenAI /v1/images/edits — see OpenAIProvider.edit)
@@ -650,6 +652,7 @@ class Settings:
650
652
  prompt_engine: str = DEFAULTS["prompt_engine"]
651
653
  review_engine: str = DEFAULTS["review_engine"]
652
654
  claude_model: str = DEFAULTS["claude_model"]
655
+ claude_effort: str = DEFAULTS["claude_effort"]
653
656
 
654
657
  dry_run: bool = False
655
658
  verbose: bool = False
@@ -752,6 +755,7 @@ def resolve_settings(args: argparse.Namespace, config: Dict[str, Any]) -> Settin
752
755
  prompt_engine=pick(args.prompt_engine, "PROMPT_ENGINE", "prompt_engine"),
753
756
  review_engine=pick(args.review, "REVIEW_ENGINE", "review_engine"),
754
757
  claude_model=str(cfg("claude_model", "") or ""),
758
+ claude_effort=str(cfg("claude_effort", DEFAULTS["claude_effort"]) or ""),
755
759
  dry_run=args.dry_run or _env_flag("DRY_RUN"),
756
760
  verbose=args.verbose or _env_flag("VERBOSE"),
757
761
  force=args.force or _env_flag("FORCE"),
@@ -1120,6 +1124,7 @@ def claude_article_brief(client: "AnthropicClient", cf: ContentFile,
1120
1124
  text = client.complete(
1121
1125
  ART_DIRECTOR_SYSTEM, article,
1122
1126
  model=claude_model_for(settings), max_tokens=2048,
1127
+ effort=settings.claude_effort,
1123
1128
  ).strip()
1124
1129
  if text:
1125
1130
  debug(f"Claude art-direction brief: {text[:300]}...")
@@ -1159,6 +1164,7 @@ def claude_review_image(client: "AnthropicClient", image_path: Path,
1159
1164
  text = client.complete_vision(
1160
1165
  REVIEWER_SYSTEM, context, image_path,
1161
1166
  model=claude_model_for(settings),
1167
+ effort=settings.claude_effort,
1162
1168
  )
1163
1169
  data = _extract_json_object(text)
1164
1170
  if not data:
@@ -1496,6 +1502,7 @@ class AnthropicClient:
1496
1502
  user_text: str,
1497
1503
  model: str = DEFAULT_CLAUDE_MODEL,
1498
1504
  max_tokens: int = CLAUDE_MAX_TOKENS,
1505
+ effort: str = "",
1499
1506
  ) -> str:
1500
1507
  """One Messages-API turn (or CLI run); returns concatenated text blocks.
1501
1508
 
@@ -1520,6 +1527,8 @@ class AnthropicClient:
1520
1527
  "system": system_blocks,
1521
1528
  "messages": [{"role": "user", "content": user_text}],
1522
1529
  }
1530
+ if effort:
1531
+ payload["output_config"] = {"effort": effort}
1523
1532
 
1524
1533
  def call(body: Dict[str, Any]) -> dict:
1525
1534
  return with_retries(
@@ -1530,7 +1539,12 @@ class AnthropicClient:
1530
1539
  try:
1531
1540
  data = call(payload)
1532
1541
  except HttpStatusError as exc:
1533
- if exc.status == 400 and "thinking" in exc.message().lower():
1542
+ message = exc.message().lower() if exc.status == 400 else ""
1543
+ if exc.status == 400 and ("output_config" in message or "effort" in message):
1544
+ debug("Retrying without output_config parameter")
1545
+ payload.pop("output_config", None)
1546
+ data = call(payload)
1547
+ elif exc.status == 400 and "thinking" in message:
1534
1548
  debug("Retrying without thinking parameter")
1535
1549
  payload.pop("thinking", None)
1536
1550
  data = call(payload)
@@ -1564,6 +1578,7 @@ class AnthropicClient:
1564
1578
  image_path: Path,
1565
1579
  model: str = DEFAULT_CLAUDE_MODEL,
1566
1580
  max_tokens: int = 2048,
1581
+ effort: str = "",
1567
1582
  ) -> str:
1568
1583
  """One vision turn over a local PNG (review stage). CLI mode passes the
1569
1584
  file path and lets `claude -p` read it; API modes embed base64."""
@@ -1595,10 +1610,25 @@ class AnthropicClient:
1595
1610
  ],
1596
1611
  }],
1597
1612
  }
1598
- data = with_retries(
1599
- lambda: http_json(ANTHROPIC_API_URL, payload, self.headers(), timeout=900),
1600
- "Anthropic API (review)",
1601
- )
1613
+ if effort:
1614
+ payload["output_config"] = {"effort": effort}
1615
+
1616
+ def call(body: Dict[str, Any]) -> dict:
1617
+ return with_retries(
1618
+ lambda: http_json(ANTHROPIC_API_URL, body, self.headers(), timeout=900),
1619
+ "Anthropic API (review)",
1620
+ )
1621
+
1622
+ try:
1623
+ data = call(payload)
1624
+ except HttpStatusError as exc:
1625
+ message = exc.message().lower() if exc.status == 400 else ""
1626
+ if exc.status == 400 and ("output_config" in message or "effort" in message):
1627
+ debug("Retrying review without output_config parameter")
1628
+ payload.pop("output_config", None)
1629
+ data = call(payload)
1630
+ else:
1631
+ raise
1602
1632
  if data.get("stop_reason") == "refusal":
1603
1633
  details = data.get("stop_details") or {}
1604
1634
  raise ClaudeRefusal(details.get("category") if isinstance(details, dict) else None)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zer0ImageGenerator
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zer0-image-generator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amr Abdel