Phase B: AI press-release rewrite pipeline

Pluggable rewrite client (Anthropic default, ollama/openrouter stubs)
using claude-opus-4-7 with prompt caching on system prompt + style guide.
Routes the raw submission through:
  1. bb.ai_original_submissions  — raw never edited
  2. bb.ai_rewrite_jobs          — every attempt logged with token counts
  3. quality gates               — AI-tell banned-word check + heuristic
                                   AI-detector score (threshold 0.65)
  4. regenerate up to 3x         — on banned words or detector trip
  5. bb.ai_rewritten_articles    — status ai_rewritten_pending_review
  6. /admin/review-queue          — list + detail page with approve / reject /
                                   needs-human actions; on approve, the
                                   original submission is FK-linked to the
                                   published rewrite

New files only — no existing routes or contributor flow modified.
Persona auto-selection routes by beat keywords; 6 personas seeded
(marcus-halverson, elena-vasquez, darren-okafor, sarah-quinn, mike-trayton,
priya-rao) with SVG monogram avatars in public/assets/images/personas/.

Smoke test (Matrox / ST 2110 sample press release):
- persona auto-routed to darren-okafor (protocols-specs)
- anthropic 22.3s, in=2616 / out=1372 tokens
- gates passed first try, score=0.000, no banned words
- review_queue row created

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-05-14 18:38:27 +00:00
parent 8a2ca37134
commit 3bd9a716b2
17 changed files with 1220 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Darren Okafor">
<defs>
<linearGradient id="do-bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#064e3b"/>
<stop offset="1" stop-color="#047857"/>
</linearGradient>
<linearGradient id="do-ring" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#6ee7b7"/>
<stop offset="1" stop-color="#059669"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="128" fill="url(#do-bg)"/>
<circle cx="128" cy="128" r="118" fill="none" stroke="url(#do-ring)" stroke-width="3" opacity="0.45"/>
<text x="128" y="148" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="92" font-weight="700" fill="#d1fae5" letter-spacing="-2">DO</text>
<text x="128" y="200" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="14" font-weight="600" fill="#6ee7b7" letter-spacing="3">PROTOCOLS / SPECS</text>
</svg>

After

Width:  |  Height:  |  Size: 996 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Elena Vasquez">
<defs>
<linearGradient id="ev-bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#3b0764"/>
<stop offset="1" stop-color="#9333ea"/>
</linearGradient>
<linearGradient id="ev-ring" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f0abfc"/>
<stop offset="1" stop-color="#a21caf"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="128" fill="url(#ev-bg)"/>
<circle cx="128" cy="128" r="118" fill="none" stroke="url(#ev-ring)" stroke-width="3" opacity="0.45"/>
<text x="128" y="148" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="92" font-weight="700" fill="#fae8ff" letter-spacing="-2">EV</text>
<text x="128" y="200" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="14" font-weight="600" fill="#d8b4fe" letter-spacing="3">STREAMING / OTT</text>
</svg>

After

Width:  |  Height:  |  Size: 994 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Marcus Halverson">
<defs>
<linearGradient id="mh-bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0f172a"/>
<stop offset="1" stop-color="#1e3a5f"/>
</linearGradient>
<linearGradient id="mh-ring" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#60a5fa"/>
<stop offset="1" stop-color="#1d4ed8"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="128" fill="url(#mh-bg)"/>
<circle cx="128" cy="128" r="118" fill="none" stroke="url(#mh-ring)" stroke-width="3" opacity="0.45"/>
<text x="128" y="148" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="92" font-weight="700" fill="#e2e8f0" letter-spacing="-2">MH</text>
<text x="128" y="200" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="14" font-weight="600" fill="#94a3b8" letter-spacing="3">INDUSTRY VETERAN</text>
</svg>

After

Width:  |  Height:  |  Size: 998 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Mike Trayton">
<defs>
<linearGradient id="mt-bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#7f1d1d"/>
<stop offset="1" stop-color="#dc2626"/>
</linearGradient>
<linearGradient id="mt-ring" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fca5a5"/>
<stop offset="1" stop-color="#b91c1c"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="128" fill="url(#mt-bg)"/>
<circle cx="128" cy="128" r="118" fill="none" stroke="url(#mt-ring)" stroke-width="3" opacity="0.45"/>
<text x="128" y="148" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="92" font-weight="700" fill="#fee2e2" letter-spacing="-2">MT</text>
<text x="128" y="200" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="14" font-weight="600" fill="#fca5a5" letter-spacing="3">LIVE / SPORTS</text>
</svg>

After

Width:  |  Height:  |  Size: 991 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Priya Rao">
<defs>
<linearGradient id="pr-bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0c4a6e"/>
<stop offset="1" stop-color="#0891b2"/>
</linearGradient>
<linearGradient id="pr-ring" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#67e8f9"/>
<stop offset="1" stop-color="#0e7490"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="128" fill="url(#pr-bg)"/>
<circle cx="128" cy="128" r="118" fill="none" stroke="url(#pr-ring)" stroke-width="3" opacity="0.45"/>
<text x="128" y="148" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="92" font-weight="700" fill="#cffafe" letter-spacing="-2">PR</text>
<text x="128" y="200" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="14" font-weight="600" fill="#67e8f9" letter-spacing="3">POST-PRODUCTION</text>
</svg>

After

Width:  |  Height:  |  Size: 990 B

View File

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Sarah Quinn">
<defs>
<linearGradient id="sq-bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#451a03"/>
<stop offset="1" stop-color="#b45309"/>
</linearGradient>
<linearGradient id="sq-ring" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fcd34d"/>
<stop offset="1" stop-color="#d97706"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="128" fill="url(#sq-bg)"/>
<circle cx="128" cy="128" r="118" fill="none" stroke="url(#sq-ring)" stroke-width="3" opacity="0.45"/>
<text x="128" y="148" text-anchor="middle" font-family="Georgia, 'Times New Roman', serif" font-size="92" font-weight="700" fill="#fef3c7" letter-spacing="-2">SQ</text>
<text x="128" y="200" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="14" font-weight="600" fill="#fcd34d" letter-spacing="3">BUSINESS / M&amp;A</text>
</svg>

After

Width:  |  Height:  |  Size: 995 B