Fix unseeded generation when seed_base is unset; make the critic's request params configurable

#17
agentic_upsampling/clients.py CHANGED
@@ -477,8 +477,9 @@ class VLMQualityJudge:
477
  api_token: str,
478
  endpoint_url: str = DEFAULT_CRITIC_ENDPOINT_URL,
479
  model: str = DEFAULT_CRITIC_MODEL,
480
- max_tokens: int = 8192,
481
  image_jpeg_quality: int | None = DEFAULT_JPEG_QUALITY,
 
482
  ) -> None:
483
  self.chat_client = OpenAIChatClient(
484
  ChatClientConfig(
@@ -487,6 +488,7 @@ class VLMQualityJudge:
487
  api_token=api_token,
488
  max_tokens=max_tokens,
489
  max_retries=3,
 
490
  )
491
  )
492
  self.image_jpeg_quality = image_jpeg_quality
 
477
  api_token: str,
478
  endpoint_url: str = DEFAULT_CRITIC_ENDPOINT_URL,
479
  model: str = DEFAULT_CRITIC_MODEL,
480
+ max_tokens: int = 16384,
481
  image_jpeg_quality: int | None = DEFAULT_JPEG_QUALITY,
482
+ extra_body: dict[str, Any] | None = None,
483
  ) -> None:
484
  self.chat_client = OpenAIChatClient(
485
  ChatClientConfig(
 
488
  api_token=api_token,
489
  max_tokens=max_tokens,
490
  max_retries=3,
491
+ extra_body=extra_body,
492
  )
493
  )
494
  self.image_jpeg_quality = image_jpeg_quality
agentic_upsampling/runner.py CHANGED
@@ -3,6 +3,7 @@
3
  from __future__ import annotations
4
 
5
  import json
 
6
  import traceback
7
  from concurrent.futures import ThreadPoolExecutor, as_completed
8
  from dataclasses import dataclass
@@ -112,6 +113,12 @@ class AgenticUpsamplerRunner:
112
  (item_dir / "failure.json").unlink(missing_ok=True)
113
  (item_dir / "incomplete.json").unlink(missing_ok=True)
114
  self._log(f"[prompt {item.prompt_id}] start")
 
 
 
 
 
 
115
  candidates: list[dict[str, Any]] = []
116
  previous_prompt: dict[str, Any] | None = None
117
  previous_analysis: dict[str, Any] | None = None
@@ -131,6 +138,7 @@ class AgenticUpsamplerRunner:
131
  previous_analysis,
132
  previous_negative_prompt,
133
  candidates,
 
134
  )
135
  except Exception as exc:
136
  if not candidates:
@@ -152,7 +160,9 @@ class AgenticUpsamplerRunner:
152
  self._log(f"[prompt {item.prompt_id}] early stop at iter={iteration}")
153
  break
154
 
155
- return self.finalize_item(item, candidates, incomplete_error=incomplete_error)
 
 
156
 
157
  def run_item_safely(self, item: PromptItem) -> dict[str, Any]:
158
  """Run one item and convert failures into structured records."""
@@ -179,6 +189,7 @@ class AgenticUpsamplerRunner:
179
  previous_analysis: dict[str, Any] | None,
180
  previous_negative_prompt: str,
181
  candidates: list[dict[str, Any]],
 
182
  ) -> dict[str, Any]:
183
  prepared = self.prepare_iteration_prompt(
184
  item,
@@ -195,6 +206,7 @@ class AgenticUpsamplerRunner:
195
  iteration,
196
  prepared.prompt_json,
197
  prepared.negative_prompt,
 
198
  )
199
  return self.finalize_iteration(item, iteration_dir, iteration, sample_candidates, sample_errors)
200
 
@@ -205,6 +217,7 @@ class AgenticUpsamplerRunner:
205
  iteration: int,
206
  prompt_json: dict[str, Any],
207
  negative_prompt: str,
 
208
  ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
209
  """Generate seed samples concurrently, then judge successful images in sample order."""
210
  generation_outputs: dict[int, GenerationOutput] = {}
@@ -218,6 +231,7 @@ class AgenticUpsamplerRunner:
218
  sample_index,
219
  prompt_json,
220
  negative_prompt,
 
221
  ): sample_index
222
  for sample_index in range(self.config.samples_per_iteration)
223
  }
@@ -300,8 +314,11 @@ class AgenticUpsamplerRunner:
300
  sample_index: int,
301
  prompt_json: dict[str, Any],
302
  negative_prompt: str,
 
303
  ) -> dict[str, Any]:
304
- generation = self.run_generation_sample(item, iteration_dir, sample_index, prompt_json, negative_prompt)
 
 
305
  return self.judge_iteration_sample(
306
  item,
307
  iteration_dir,
@@ -319,6 +336,7 @@ class AgenticUpsamplerRunner:
319
  sample_index: int,
320
  prompt_json: dict[str, Any],
321
  negative_prompt: str,
 
322
  ) -> GenerationOutput:
323
  """Generate one sample image for an iteration."""
324
  sample_dir = self._sample_dir(iteration_dir, sample_index)
@@ -328,7 +346,7 @@ class AgenticUpsamplerRunner:
328
  prompt_json=prompt_json,
329
  prompt_id=item.prompt_id,
330
  output_dir=sample_dir,
331
- seed=self._sample_seed(sample_index),
332
  negative_prompt=negative_prompt,
333
  jpeg_quality=self.config.jpeg_quality,
334
  )
@@ -422,11 +440,6 @@ class AgenticUpsamplerRunner:
422
  if self.config.verbose:
423
  print(message, flush=True)
424
 
425
- def _sample_seed(self, sample_index: int) -> int | None:
426
- if self.config.seed_base is None:
427
- return None
428
- return self.config.seed_base + sample_index
429
-
430
  def _sample_dir(self, iteration_dir: Path, sample_index: int) -> Path:
431
  if self.config.samples_per_iteration == 1:
432
  return iteration_dir
 
3
  from __future__ import annotations
4
 
5
  import json
6
+ import random
7
  import traceback
8
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
  from dataclasses import dataclass
 
113
  (item_dir / "failure.json").unlink(missing_ok=True)
114
  (item_dir / "incomplete.json").unlink(missing_ok=True)
115
  self._log(f"[prompt {item.prompt_id}] start")
116
+ # One base per item, reused by every iteration (sample slot j gets base + j), so a
117
+ # rewrite is compared against the same noise. A resumed run draws a new base.
118
+ seed_base = self.config.seed_base
119
+ if seed_base is None:
120
+ seed_base = random.randint(0, 2**31 - 1 - self.config.samples_per_iteration)
121
+ self._log(f"[prompt {item.prompt_id}] seed_base drawn: {seed_base}")
122
  candidates: list[dict[str, Any]] = []
123
  previous_prompt: dict[str, Any] | None = None
124
  previous_analysis: dict[str, Any] | None = None
 
138
  previous_analysis,
139
  previous_negative_prompt,
140
  candidates,
141
+ seed_base,
142
  )
143
  except Exception as exc:
144
  if not candidates:
 
160
  self._log(f"[prompt {item.prompt_id}] early stop at iter={iteration}")
161
  break
162
 
163
+ result = self.finalize_item(item, candidates, incomplete_error=incomplete_error)
164
+ result["seed_base"] = seed_base
165
+ return result
166
 
167
  def run_item_safely(self, item: PromptItem) -> dict[str, Any]:
168
  """Run one item and convert failures into structured records."""
 
189
  previous_analysis: dict[str, Any] | None,
190
  previous_negative_prompt: str,
191
  candidates: list[dict[str, Any]],
192
+ seed_base: int,
193
  ) -> dict[str, Any]:
194
  prepared = self.prepare_iteration_prompt(
195
  item,
 
206
  iteration,
207
  prepared.prompt_json,
208
  prepared.negative_prompt,
209
+ seed_base,
210
  )
211
  return self.finalize_iteration(item, iteration_dir, iteration, sample_candidates, sample_errors)
212
 
 
217
  iteration: int,
218
  prompt_json: dict[str, Any],
219
  negative_prompt: str,
220
+ seed_base: int,
221
  ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
222
  """Generate seed samples concurrently, then judge successful images in sample order."""
223
  generation_outputs: dict[int, GenerationOutput] = {}
 
231
  sample_index,
232
  prompt_json,
233
  negative_prompt,
234
+ seed_base,
235
  ): sample_index
236
  for sample_index in range(self.config.samples_per_iteration)
237
  }
 
314
  sample_index: int,
315
  prompt_json: dict[str, Any],
316
  negative_prompt: str,
317
+ seed_base: int,
318
  ) -> dict[str, Any]:
319
+ generation = self.run_generation_sample(
320
+ item, iteration_dir, sample_index, prompt_json, negative_prompt, seed_base
321
+ )
322
  return self.judge_iteration_sample(
323
  item,
324
  iteration_dir,
 
336
  sample_index: int,
337
  prompt_json: dict[str, Any],
338
  negative_prompt: str,
339
+ seed_base: int,
340
  ) -> GenerationOutput:
341
  """Generate one sample image for an iteration."""
342
  sample_dir = self._sample_dir(iteration_dir, sample_index)
 
346
  prompt_json=prompt_json,
347
  prompt_id=item.prompt_id,
348
  output_dir=sample_dir,
349
+ seed=seed_base + sample_index,
350
  negative_prompt=negative_prompt,
351
  jpeg_quality=self.config.jpeg_quality,
352
  )
 
440
  if self.config.verbose:
441
  print(message, flush=True)
442
 
 
 
 
 
 
443
  def _sample_dir(self, iteration_dir: Path, sample_index: int) -> Path:
444
  if self.config.samples_per_iteration == 1:
445
  return iteration_dir
tests/test_agentic_upsampling.py CHANGED
@@ -10,7 +10,7 @@ from typing import Any
10
 
11
  from PIL import Image
12
 
13
- from agentic_upsampling.clients import ImageGenerationClient, PromptRewriterClient
14
  from agentic_upsampling.constants import (
15
  DEFAULT_CRITIC_ENDPOINT_URL,
16
  DEFAULT_CRITIC_MODEL,
@@ -413,7 +413,32 @@ def test_runner_early_stops_by_default(tmp_path: Path) -> None:
413
  assert result["best_iteration"] == 0
414
  assert rewriter.initial_calls == 1
415
  assert rewriter.joint_rewrite_calls == 0
416
- assert generator.seeds == [None]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
 
419
  def test_runner_can_disable_early_stop_and_select_best_sample(tmp_path: Path) -> None:
@@ -496,3 +521,14 @@ def test_extract_best_images_copies_images_and_writes_manifests(tmp_path: Path)
496
  assert (tmp_path / "export" / "best_generations.jsonl").exists()
497
  assert (tmp_path / "export" / "best_generations.csv").exists()
498
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  from PIL import Image
12
 
13
+ from agentic_upsampling.clients import ImageGenerationClient, PromptRewriterClient, VLMQualityJudge
14
  from agentic_upsampling.constants import (
15
  DEFAULT_CRITIC_ENDPOINT_URL,
16
  DEFAULT_CRITIC_MODEL,
 
413
  assert result["best_iteration"] == 0
414
  assert rewriter.initial_calls == 1
415
  assert rewriter.joint_rewrite_calls == 0
416
+ assert generator.seeds == [result["seed_base"]]
417
+
418
+
419
+ def test_runner_draws_one_seed_base_when_unset(tmp_path: Path) -> None:
420
+ """An unset seed_base is drawn once and reused, so slot j gets base + j every iteration."""
421
+ rewriter = FakeRewriter()
422
+ generator = FakeGenerator()
423
+ runner = AgenticUpsamplerRunner(
424
+ rewriter=rewriter,
425
+ generator=generator, # type: ignore[arg-type]
426
+ judge=FakeJudge([5.0, 6.0, 5.5, 6.5]),
427
+ config=RunnerConfig(
428
+ output_dir=tmp_path,
429
+ max_iterations=2,
430
+ samples_per_iteration=2,
431
+ early_stop=False,
432
+ ),
433
+ )
434
+
435
+ result = runner.run_item(_item())
436
+
437
+ seed_base = result["seed_base"]
438
+ assert seed_base is not None
439
+ # Samples are generated concurrently, so compare each iteration's seeds as a set.
440
+ assert sorted(generator.seeds[:2]) == [seed_base, seed_base + 1]
441
+ assert sorted(generator.seeds[2:]) == [seed_base, seed_base + 1]
442
 
443
 
444
  def test_runner_can_disable_early_stop_and_select_best_sample(tmp_path: Path) -> None:
 
521
  assert (tmp_path / "export" / "best_generations.jsonl").exists()
522
  assert (tmp_path / "export" / "best_generations.csv").exists()
523
 
524
+
525
+ def test_vlm_quality_judge_sends_no_extra_body_by_default() -> None:
526
+ judge = VLMQualityJudge(api_token="token")
527
+
528
+ assert judge.chat_client.config.extra_body is None
529
+
530
+
531
+ def test_vlm_quality_judge_forwards_extra_body() -> None:
532
+ judge = VLMQualityJudge(api_token="token", extra_body={"reasoning_effort": "high"})
533
+
534
+ assert judge.chat_client.config.extra_body == {"reasoning_effort": "high"}