polyptych 0.2.0__tar.gz → 0.3.0__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 (130) hide show
  1. {polyptych-0.2.0 → polyptych-0.3.0}/CHANGELOG.md +28 -0
  2. {polyptych-0.2.0 → polyptych-0.3.0}/PKG-INFO +1 -1
  3. {polyptych-0.2.0 → polyptych-0.3.0}/image-presets.yaml +1 -0
  4. {polyptych-0.2.0 → polyptych-0.3.0}/pyproject.toml +1 -1
  5. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/image_batch.py +2 -2
  6. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/pipeline_base.py +7 -2
  7. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_pipeline_base.py +43 -0
  8. {polyptych-0.2.0 → polyptych-0.3.0}/.gitignore +0 -0
  9. {polyptych-0.2.0 → polyptych-0.3.0}/CONTRIBUTING.md +0 -0
  10. {polyptych-0.2.0 → polyptych-0.3.0}/LICENSE +0 -0
  11. {polyptych-0.2.0 → polyptych-0.3.0}/NOTICE +0 -0
  12. {polyptych-0.2.0 → polyptych-0.3.0}/README.md +0 -0
  13. {polyptych-0.2.0 → polyptych-0.3.0}/SECURITY.md +0 -0
  14. {polyptych-0.2.0 → polyptych-0.3.0}/image_model_config.yaml +0 -0
  15. {polyptych-0.2.0 → polyptych-0.3.0}/model_config.yaml +0 -0
  16. {polyptych-0.2.0 → polyptych-0.3.0}/pipeline-presets.yaml +0 -0
  17. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/providers/gemini-best-practices.md +0 -0
  18. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/providers/openai-best-practices.md +0 -0
  19. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/providers/xai-best-practices.md +0 -0
  20. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/style-transfer/anime/anime-dark.md +0 -0
  21. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/style-transfer/editorial/scholarly-monograph.md +0 -0
  22. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/style-transfer/infographic/clean-minimalist.md +0 -0
  23. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/style-transfer/infographic/semi-flat-vector.md +0 -0
  24. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/style-transfer/noir/cinematic-illustrative-noir.md +0 -0
  25. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/style-transfer/period-art/attic-vase-pastoral.md +0 -0
  26. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-01-genre-classification.md +0 -0
  27. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-02-source-analysis.md +0 -0
  28. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-03-structure-planning.md +0 -0
  29. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-04-content-allocation.md +0 -0
  30. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-05-visual-design.md +0 -0
  31. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-06-slide-specification-fiction.md +0 -0
  32. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-06-slide-specification.md +0 -0
  33. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-07-image-generation.md +0 -0
  34. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-critique.md +0 -0
  35. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-i0-analysis.md +0 -0
  36. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-i1-design.md +0 -0
  37. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-i2-critique.md +0 -0
  38. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-i2-prompts.md +0 -0
  39. {polyptych-0.2.0 → polyptych-0.3.0}/prompts/tasks/task-i2-refine.md +0 -0
  40. {polyptych-0.2.0 → polyptych-0.3.0}/src/common/__init__.py +0 -0
  41. {polyptych-0.2.0 → polyptych-0.3.0}/src/common/compat.py +0 -0
  42. {polyptych-0.2.0 → polyptych-0.3.0}/src/common/logging_setup.py +0 -0
  43. {polyptych-0.2.0 → polyptych-0.3.0}/src/common/usage_log.py +0 -0
  44. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/__init__.py +0 -0
  45. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/_datafiles.py +0 -0
  46. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/batch_utils.py +0 -0
  47. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/clean_source.py +0 -0
  48. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/cli.py +0 -0
  49. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/client.py +0 -0
  50. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/concurrent_engine.py +0 -0
  51. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/cross_validate.py +0 -0
  52. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/ext.py +0 -0
  53. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/logging_setup.py +0 -0
  54. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/model_config.py +0 -0
  55. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/models/__init__.py +0 -0
  56. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/models/infographic.py +0 -0
  57. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/models/slide.py +0 -0
  58. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/pipeline.py +0 -0
  59. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/pipeline_config.py +0 -0
  60. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/pipeline_infographic.py +0 -0
  61. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/pipeline_task.py +0 -0
  62. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/presets.py +0 -0
  63. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/prompt_loader.py +0 -0
  64. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/providers/__init__.py +0 -0
  65. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/providers/anthropic.py +0 -0
  66. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/providers/base.py +0 -0
  67. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/providers/gemini.py +0 -0
  68. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/providers/openai.py +0 -0
  69. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/providers/vertex.py +0 -0
  70. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/providers/xai.py +0 -0
  71. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/ref_utils.py +0 -0
  72. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/run_config.py +0 -0
  73. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/task_registry.py +0 -0
  74. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/__init__.py +0 -0
  75. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_01_genre.py +0 -0
  76. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_02_analysis.py +0 -0
  77. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_03_structure.py +0 -0
  78. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_04_content.py +0 -0
  79. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_05_design.py +0 -0
  80. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_06_slides.py +0 -0
  81. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_07_prompts.py +0 -0
  82. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_i0_analysis.py +0 -0
  83. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_i1_design.py +0 -0
  84. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_i2_critique.py +0 -0
  85. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/tasks/task_i2_prompts.py +0 -0
  86. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/text_utils.py +0 -0
  87. {polyptych-0.2.0 → polyptych-0.3.0}/src/polyptych/usage_log.py +0 -0
  88. {polyptych-0.2.0 → polyptych-0.3.0}/tests/__init__.py +0 -0
  89. {polyptych-0.2.0 → polyptych-0.3.0}/tests/common/__init__.py +0 -0
  90. {polyptych-0.2.0 → polyptych-0.3.0}/tests/common/test_compat.py +0 -0
  91. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/__init__.py +0 -0
  92. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/conftest.py +0 -0
  93. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/mock_client.py +0 -0
  94. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/providers/__init__.py +0 -0
  95. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/providers/test_base.py +0 -0
  96. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/providers/test_helpers.py +0 -0
  97. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/__init__.py +0 -0
  98. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/conftest.py +0 -0
  99. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/factories.py +0 -0
  100. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/test_character_canon.py +0 -0
  101. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/test_cross_validate.py +0 -0
  102. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/test_local_manifest.py +0 -0
  103. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/test_prompt_provider.py +0 -0
  104. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/test_schema_ergonomics.py +0 -0
  105. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/test_slide_image_paths.py +0 -0
  106. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/test_slide_pipeline_flow.py +0 -0
  107. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/slide/test_task7_assembly.py +0 -0
  108. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/tasks/__init__.py +0 -0
  109. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/tasks/test_task_05_design.py +0 -0
  110. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_clean_source.py +0 -0
  111. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_cli.py +0 -0
  112. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_client.py +0 -0
  113. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_dependency_resolver.py +0 -0
  114. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_image_batch.py +0 -0
  115. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_infographic_critique.py +0 -0
  116. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_logging_setup.py +0 -0
  117. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_pipeline_config.py +0 -0
  118. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_pipeline_infographic.py +0 -0
  119. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_pipeline_init.py +0 -0
  120. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_pipeline_resume.py +0 -0
  121. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_ref_utils.py +0 -0
  122. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_run_config.py +0 -0
  123. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_task_registry.py +0 -0
  124. {polyptych-0.2.0 → polyptych-0.3.0}/tests/polyptych/test_text_utils.py +0 -0
  125. {polyptych-0.2.0 → polyptych-0.3.0}/tests/test_concurrent_engine.py +0 -0
  126. {polyptych-0.2.0 → polyptych-0.3.0}/tests/test_concurrent_engine_ordering.py +0 -0
  127. {polyptych-0.2.0 → polyptych-0.3.0}/tests/test_datafiles.py +0 -0
  128. {polyptych-0.2.0 → polyptych-0.3.0}/tests/test_ext.py +0 -0
  129. {polyptych-0.2.0 → polyptych-0.3.0}/tests/test_model_config.py +0 -0
  130. {polyptych-0.2.0 → polyptych-0.3.0}/tests/test_presets.py +0 -0
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-06-30
11
+
12
+ ### Added
13
+
14
+ - **`gem-lite` image preset** — an opt-in bundle targeting Google's Nano Banana 2
15
+ Lite (`gemini-3.1-flash-lite-image`), the cheapest/fastest member of the Nano
16
+ Banana 2 family. Resolves to `provider: gemini`, `size: 1K`, `aspect-ratio:
17
+ 16:9` with the Lite model id carried as an `image-model` override, so it works
18
+ without touching `image_model_config.yaml` or the provider defaults. Pinned to
19
+ 1K because Lite is 1K-only. Use it via `--image-preset gem-lite` when cheap,
20
+ high-volume image generation is needed; all defaults are unchanged.
21
+
22
+ ## [0.2.1] - 2026-06-20
23
+
24
+ ### Fixed
25
+
26
+ - **Image-generation failures no longer print a stack trace.** A failed
27
+ per-image request (e.g. an OpenAI output-moderation block) already logs a
28
+ one-line warning carrying the error; the redundant `exc_info=True` traceback
29
+ on the `polyptych.image_batch` logger has been dropped and the error message
30
+ folded into that warning, so the log stays a clean one-liner.
31
+ - **Resuming a run no longer crashes when `source_file` is the output dir's own
32
+ `source.md`.** `PipelineBase.__init__` copied the source into the output
33
+ directory unconditionally, so a resume command that passed
34
+ `<output_dir>/source.md` (the path the first run wrote) raised
35
+ `shutil.SameFileError` before any work ran. The copy is now skipped when the
36
+ source and destination resolve to the same file.
37
+
10
38
  ## [0.2.0] - 2026-06-14
11
39
 
12
40
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyptych
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Turn a source text into visual media — slide decks and single-page infographics, with multi-provider text and image generation
5
5
  Project-URL: Homepage, https://github.com/jdinkla/polyptych-studio
6
6
  Project-URL: Repository, https://github.com/jdinkla/polyptych-studio
@@ -21,6 +21,7 @@
21
21
 
22
22
  gem: { provider: "gemini", size: "1K", aspect-ratio: "16:9" }
23
23
  gem-2k: { provider: "gemini", size: "2K", aspect-ratio: "16:9" } # infographic-grade legibility
24
+ gem-lite: { provider: "gemini", size: "1K", aspect-ratio: "16:9", image-model: "gemini-3.1-flash-lite-image" } # Nano Banana 2 Lite: cheapest/fastest, 1K only
24
25
 
25
26
  # --- OpenAI gpt-image-2 -------------------------------------------------------
26
27
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "polyptych"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Turn a source text into visual media — slide decks and single-page infographics, with multi-provider text and image generation"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -208,10 +208,10 @@ class ImageBatchGenerator:
208
208
  f"{item.index}: {error_msg}"
209
209
  )
210
210
  logger.warning(
211
- "Failed to generate image for %s %s",
211
+ "Failed to generate image for %s %s: %s",
212
212
  self.label,
213
213
  item.index,
214
- exc_info=True,
214
+ error_msg,
215
215
  )
216
216
  return result
217
217
 
@@ -136,8 +136,13 @@ class SlidePipelineBase:
136
136
  (self.output_dir / "prompts").mkdir(exist_ok=True)
137
137
  (self.output_dir / "images").mkdir(exist_ok=True)
138
138
 
139
- # Copy source material into output directory
140
- shutil.copy2(self.source_path, self.output_dir / "source.md")
139
+ # Copy source material into output directory. Skip when the source is
140
+ # already the in-dir copy (e.g. a resume run that passes
141
+ # <output_dir>/source.md, the path the first run wrote); copying a file
142
+ # onto itself raises shutil.SameFileError.
143
+ source_copy = self.output_dir / "source.md"
144
+ if self.source_path.resolve() != source_copy.resolve():
145
+ shutil.copy2(self.source_path, source_copy)
141
146
 
142
147
  def _model_for(self, task_name: str) -> str:
143
148
  """Resolve the concrete model string for a pipeline step."""
@@ -30,6 +30,49 @@ def _build_pipeline(
30
30
  )
31
31
 
32
32
 
33
+ class TestSourceCopy:
34
+ def test_resume_with_in_dir_source_does_not_raise(
35
+ self, tmp_path, source_text, test_model_config
36
+ ):
37
+ """Resuming a run by pointing source_path at the output dir's own
38
+ source.md must not crash. The first run writes <output_dir>/source.md,
39
+ so a natural resume command passes that exact path back in; copying it
40
+ onto itself used to raise shutil.SameFileError before any work ran."""
41
+ output_dir = tmp_path / "output"
42
+ output_dir.mkdir(parents=True)
43
+ in_dir_source = output_dir / "source.md"
44
+ in_dir_source.write_text(source_text)
45
+
46
+ pipeline = SlidePipeline(
47
+ source_path=in_dir_source,
48
+ output_dir=output_dir,
49
+ model_config=test_model_config,
50
+ text_provider="gemini",
51
+ text_fallback=["none"],
52
+ )
53
+
54
+ # The self-copy is skipped, not attempted: the in-dir source survives
55
+ # untouched and the pipeline still loaded it.
56
+ assert (output_dir / "source.md").read_text() == source_text
57
+ assert pipeline.source_essay == source_text
58
+
59
+ def test_fresh_run_copies_source_into_output_dir(
60
+ self, tmp_path, source_text, test_model_config
61
+ ):
62
+ """A normal run (source outside the output dir) still copies source.md
63
+ into the output directory."""
64
+ source_file = tmp_path / "essay.md"
65
+ source_file.write_text(source_text)
66
+ pipeline = SlidePipeline(
67
+ source_path=source_file,
68
+ output_dir=tmp_path / "output",
69
+ model_config=test_model_config,
70
+ text_provider="gemini",
71
+ text_fallback=["none"],
72
+ )
73
+ assert (pipeline.output_dir / "source.md").read_text() == source_text
74
+
75
+
33
76
  class TestSaveYamlAtomic:
34
77
  def test_save_writes_file_and_leaves_no_temp(
35
78
  self, tmp_path, source_text, test_model_config
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes