Reality-first specification (v2.0.0, 2026-06-03). Describes what the image pipeline actually does in code today, reconciled against
ora-project/. The original v1.1 design — a 6-layer per-invocation pipeline that searched public-domain/CC image commons before falling back to AI, with a five-screen visual-suitability gate, tier-asymmetric AI disclosure, and a placeholder+retry queue — is mostly not implemented. The real pipeline is a single AI render behind a deterministic ~22%/day salience budget, finished by a cream-panel post-processor and a frontmatter patch. The original design is preserved in the Design intent appendix and in vault git history (pre-rename, through commit24ad52a44b, filenameFramework — News Image Generator.md).
Display Name
MSI News Image Generator
Display Description
Produce the hero image for the subset of Main Street Independent articles that earn one under a daily salience budget. Runs AI-generation only (no commons search), gated up front by article_salience, executed by msi_image_render.render_news_image, finished by the msi_image_post_process cream-panel keyer, and written into the article’s frontmatter image: field. Editorial-cartoon images (Hector Rentier) are delegated to the cartoon path; their register is governed by MSI Hector Rentier Editorial Cartoon.
Implementation Status (read this first)
| Spec concept (v1.1) | Status today | Where reality lives |
|---|---|---|
Executor at orchestrator/msi_image_render.py | ❌ Wrong path | ora-project/tools/msi_image_render.py |
| Commons search (Unsplash/Pexels/Wikimedia/PD), Layer 2 | ❌ Not implemented — AI-only | render_news_image (commented “Layer 2 intentionally not implemented”) |
| 6-layer structure | ❌ Collapsed to: budget gate → likeness strip → prompt → 1-retry generate → alt → post-process → patch | — |
| Five-screen suitability gate (likeness/PII/defamatory/misframing/register), Layer 4 | ❌ Not implemented as output screens | partial pre-generation analog: negative-prompt vocab bans |
| Likeness gate on real individuals | ⚠️ Exists but effectively a no-op (entity list is always empty in the auto path) | apply_likeness_gate |
| Tier-asymmetric AI disclosure (Tier1 discloses; Tier2/3 don’t) | ❌ Not implemented — every image carries the same disclosure string | render_news_image image_schema |
Placeholder + retry queue (not_before, retry cascade) | ❌ Does not exist | nearest analogs: sweeper self-heal; reconcile_dangling_images (strip) |
imageSchema = 10 fields, schema-validated | ⚠️ Astro imageSchema is now {url, alt, source} only; the rest moved to body | src/content/config.ts:54 |
| Salience budget (which articles get an image) | ✅ Live (not in original spec) | tools/article_salience.py |
| Cream-panel post-processing (key/pad/tint) | ✅ Live (not in original spec) | tools/msi_image_post_process.py |
| Style spec “not yet drafted / v1.0” | ❌ Stale — exists, cited as v1.6+ in code | Reference — MSI Image Style Specification.md |
The Real Pipeline
Who gets an image — the salience budget (tools/article_salience.py)
The original spec assumed every article gets an image (placeholder if not ready). In reality, only ~22% of a day’s articles are imaged. should_render_hero() is the single source of truth, shared by the worker-time gate and the batch sweeper:
- Budget =
daily_image_budget()=ceil(IMAGE_RATE × day_cluster_count)clamped to[MIN_BUDGET, MAX_BUDGET]. Defaults:IMAGE_RATE= 0.22 (MSI_IMAGE_RATE),MIN_BUDGET= 3,MAX_BUDGET= 25. The denominator is the day’s cluster count (stable up front), falling back to the published.mdcount. - Per-article decision is rank-based and order-independent:
_ranked_top_cluster_ids()picks the day’s top-budgetcluster_ids; an article is imaged iff itscluster_idis in that frozenset (cached per(date, budget)). - Salience =
compute_salience(): asource_tierbaseline ++0.40if AP Top News + small multi-outlet-corroboration and AP-section-breadth bonuses (each capped ~+0.08) ++0.05for primary-document sourcing, clamped to[0,1]. (There is no entity-richness term — it was removed as dead code.) - Source tiers (
_DEFAULT_TIERS): a flatWIRE_TIER= 0.50 for wsj/bbc/guardian/npr/upi; AP deliberately low at 0.40 (it earns its score from the AP-Top-News flag, not a prior);DEFAULT_TIER= 0.30. An article must clearSALIENCE_MIN= 0.43. - Diversification (
diversify_mode(), default on): a priority pass images headliners withsalience ≥ PRIORITY_SALIENCE(0.68) regardless of source, then fills the remaining budget round-robin across sources (visit order rotated by apublish_date-derived offset) so no single outlet (historically WSJ) sweeps the budget. - Legacy mode (
MSI_IMAGE_SALIENCE=legacy) restores the pre-2026-06-01 AP-Top-News-only behavior.
The hero-image gate (tools/backfill_orchestrator.py::_maybe_render_hero_image)
This is the real entry point — not orchestrator/msi_image_render.py. It runs as “Step 4.5” inside msi_engine.produce_article (import + call). Gate order:
- Reject if no slug / publish_date.
- Pre-2026 cutoff: if
publish_date < 2026-01-01(PAID_IMAGE_GEN_BOUNDARY_DATE), no paid image at all. - Load the cluster JSON for salience signals.
- Skip-if-exists idempotency: if
public/articles/<slug>.pngalready exists and is non-empty, don’t regenerate / don’t spend budget. - Call
article_salience.should_render_hero(...); bail with the decision reason ifrenderis False. build_request_from_article(slug, fm)(fromarticle_image_sweeper) builds theNewsImageRequest.- Call
render_news_image(request_data).
The render executor (tools/msi_image_render.py::render_news_image)
- Parse the
NewsImageRequest(article_slug, article_headline, article_lede, visual_register, prompt_seeds, primary_entities, style_spec_version, source_path_strategy). - Reject
editorial_cartoon(that routes torender_hector_cartoon). apply_likeness_gate()— an entity may appear in the prompt only ifis_public_figure AND in_public_role; otherwise its name is removed and stripped from the seeds. In the live auto path the entity list is always empty (build_request_from_articlehardcodesprimary_entities: []), so the gate is a defensive default, effectively a no-op unless a request is hand-authored.construct_news_prompt()— composes the locked §6 vocabulary (universal_positive+ per-register vocab fromPHOTOGRAPHIC_VOCAB/ILLUSTRATED_VOCAB/DIAGRAMMATIC_VOCAB) + seeds + negatives, includingFORBIDDEN_CARICATURE_NEGATIVE(the antisemitic-trope / core-disgust ban list). These locked tables are the operationalized style spec.invoke_image_gen()→capability_registry.invoke("image_generates", {prompt, aspect_ratio}); one retry on failure with a simplified seeds-only prompt, then fail (article ships text-only — no placeholder).- Alt text =
trim_alt_text(f"Illustration accompanying article: {headline}"), hard-capped at 125 chars. msi_image_post_process.process_png_bytes()(see below).- Write PNG to
public/articles/<article_slug>.png. - Assemble the
image_schemadict and_patch_article_image_field()rewrites the article frontmatterimage:block.
The emitted news image_schema is fixed/templated: url: /articles/<slug>.png, alt, credit: "Main Street Independent (algorithmic)", source: "ai_generated", a constant disclosure string, ai_model (the provider that answered), ai_prompt (prompt[:200]), license (CC0). Note: the Astro imageSchema (src/content/config.ts:54) only requires/validates { url, alt, source } — credit/caption/license/ai_prompt were moved inline into the body, so the renderer writes them to frontmatter but Astro neither requires nor validates them. The source enum adds press_release_licensed and commissioned.
Post-processing (tools/msi_image_post_process.py::process_png_bytes)
Runs once at generation (explicitly not idempotent): _square_pad() pads (never crops) non-square frames to 1:1 with transparent margins; _apply_transform() keys only near-pure-white desaturated pixels (brightness ≥ PAPER_THRESHOLD = 250) to alpha 0, preserves all tonal detail below that, and tints every pixel toward the page-cream hue #F4ECD8 (PAGE_CREAM). This is the cream-panel approach (a fixed cream tint, not a dark-mode invert — the invert approach in the file’s historical docstring is retired).
Cartoon path (delegated)
render_hector_image_only() / render_hector_cartoon() use a dedicated image_generates_cartoon capability slot, the same process_png_bytes, write to public/cartoons/<slug>.png, and emit a cartoon schema crediting “Hector Rentier (Main Street Independent, algorithmic)”. The register and visual vocabulary are governed by MSI Hector Rentier Editorial Cartoon; this framework only provides the shared executor. (The legacy SVG/potrace vectorization path still exists in the file but is no longer used.)
Other callers / maintenance
tools/article_image_sweeper.py::sweep_once— batch backfill of articles missingimage:, using the same salience budget; newest-first; cost-capped (max_renders/daily_cap, default 20/20; optionalmax_age_days) after a runaway sweep once rendered 1,067 images; self-heals half-completed renders (frontmatter present but PNG missing).tools/reconcile_dangling_images.py— the durable complement: a full-scan, zero-render strip ofimage:blocks whoseurlis dangling (hotlink or missing file) acrosssrc/content/articles+src/content/analyses. Leavesimage: nullalone. Idempotent; dry-run by default.tools/msi_engine.py::render_image(request, register=...)— the universal engine seam; delegates news registers (photographic/illustrated/diagrammatic) torender_news_image,editorial_cartoonto the Hector path.
Code Seams (canonical references)
- Render executor:
ora-project/tools/msi_image_render.py—render_news_image,apply_likeness_gate,construct_news_prompt,invoke_image_gen,trim_alt_text,_patch_article_image_field,_yaml_dump_image; locked vocab +FORBIDDEN_CARICATURE_NEGATIVE. - Hero gate (real entry):
ora-project/tools/backfill_orchestrator.py—_maybe_render_hero_image,_count_images_for_date,_is_ap_top_news,_rehome_prior_cluster_assets,PAID_IMAGE_GEN_BOUNDARY_DATE. - Engine seam:
ora-project/tools/msi_engine.py::render_image(Step-4.5 call inproduce_article). - Salience budget:
ora-project/tools/article_salience.py—should_render_hero,compute_salience,daily_image_budget,_ranked_top_cluster_ids,diversify_mode. - Post-processing:
ora-project/tools/msi_image_post_process.py—process_png_bytes,_apply_transform,_square_pad,PAGE_CREAM,PAPER_THRESHOLD. - Batch + request derivation:
ora-project/tools/article_image_sweeper.py—sweep_once,build_request_from_article,_split_frontmatter. - Dangling-ref strip:
ora-project/tools/reconcile_dangling_images.py::reconcile. - Slash commands:
ora-project/scripts/news_image_render.py,ora-project/scripts/hector_render.py(registered inora-project/ora-project.json). - Data contract:
src/content/config.tsimageSchema({url, alt, source}). - Style spec:
Reference — MSI Image Style Specification.md(cited as v1.6+ in code).
Environment flags
MSI_IMAGE_RATE (0.22) · MSI_IMAGE_MIN_BUDGET (3) · MSI_IMAGE_MAX_BUDGET (25) · MSI_IMAGE_SALIENCE_MIN (0.43) · MSI_IMAGE_DEFAULT_TIER (0.30) · MSI_IMAGE_WIRE_TIER (0.50) · MSI_IMAGE_PRIORITY_SALIENCE (0.68) · MSI_IMAGE_SOURCE_TIERS · MSI_IMAGE_SALIENCE (=legacy switch) · MSI_IMAGE_DIVERSIFY (default on). Live values are environment-dependent — verify on the server.
Editorial intent still in force
The original framework’s editorial commitments remain the publication’s standard even where code doesn’t enforce them: complete and accurate attribution; alt text describing the image (not the article); visual bad-faith parity with textual bad-faith (no manipulated imagery, defamatory composite, or contextual misframing); affirmative authorization before any AI likeness of a real individual; and symmetric visual standards across pen names and political alignment. Today these are partially carried by pre-generation mechanisms — the locked positive/negative prompt vocabularies (notably FORBIDDEN_CARICATURE_NEGATIVE) and the (currently dormant) likeness gate — rather than the spec’s post-generation five-screen suitability gate, which is not implemented.
Appendix — Design intent (original v1.1, NOT implemented)
Retained as design intent; most of it does not run. Full text in vault git history (pre-rename, through commit 24ad52a44b, filename Framework — News Image Generator.md). The unbuilt design comprised:
- A 6-layer per-invocation pipeline: (L1) request validation + visual-register routing via a content_type/section/pen_name decision tree; (L2) commons search across Unsplash → Pexels → Wikimedia Commons → public-domain archives, with license verification against an acceptance table, the preferred path for hard news; (L3) AI generation with a likeness gate; (L4) a five-screen visual-suitability gate (likeness, PII, defamatory-composite, contextual-misframing, style-register) plus alt-text generation; (L5) attribution assembly with tier-asymmetric AI disclosure (Tier 1 newsfeed discloses; Tier 2 Editorial Board and Tier 3 pen-names/Hector do not); (L6) schema validation + article update + retry queue with
not_beforeand a retry-cascade escalation. - A placeholder that ships with the article and is replaced on retry.
refresh/retry/ correction modes invoked by asource-correction-monitor.- A 10-field schema-validated
imageSchemaandlikeness-exclusion-list.json.
The visual-register routing tree (photographic for hard news, illustrated for analytical voices, editorial-cartoon for Hector, diagrammatic for white papers) is the intended target; in code today the news auto-path defaults every register to illustrated, and only the cartoon register is set explicitly (by the Hector runner).
Change Log
- 2026-06-03 (v2.0.0) — Reality-first rewrite + rename to
Framework — MSI News Image Generator.md. Documents the real AI-only pipeline (salience budget → hero gate →render_news_image→ cream-panel post-process → frontmatter patch) as primary content; demotes the unimplemented commons-first 6-layer design (commons search, five-screen suitability, tier-asymmetric disclosure, placeholder/retry) to a design-intent appendix. Corrects the executor path (ora-project/tools/, notorchestrator/), theimageSchemashape ({url, alt, source}), and the style-spec status (exists, v1.6+). Added Obsidian aliases for the prior filename. - 2026-05-06 (v1.1) — Style-spec wiring; tier-asymmetric disclosure design.
- 2026-05-05 (v1.0) — Initial 6-layer commons-first design. See appendix.