# External publishing

A scheduled ChatGPT editor researches AI news, writes one JSON file per story
into `content/inbox/`, and a GitHub Action posts those files to AI Radar.

```
ChatGPT schedule
  ↓ English primary-source research
  ↓ canonical English briefing
  ↓ faithful Arabic translation
  ↓ image_prompt + image_alt
  ↓ commit JSON to content/inbox/
GitHub Action
  ↓ preflight GET /publish/schema
  ↓ POST /publish
NestJS
  ↓ dedupe + validate + daily cap
  ↓ generate/store story image when image_prompt is present
  ↓ write English post + Arabic translation atomically
MySQL + local media storage
```

The Action is transport only. Editorial judgement and translation happen
before the file is written. The server owns publishing safety and image
storage.

## Story contract

English is always the source copy stored in `posts`. Arabic is stored in
`post_translations` with locale `ar`.

```json
{
  "title": "Anthropic ships Claude Opus 5.5",
  "summary": "A new frontier model with a larger context window.",
  "what_changed": "Context window doubled to 2M tokens.",
  "why_it_matters": "Long-document work that needed chunking now fits in one call.",
  "who_should_care": ["developers", "founders"],
  "worth_trying": "yes",
  "worth_trying_note": "Worth testing on long-document workflows.",
  "category": "model",
  "source_url": "https://www.anthropic.com/news/claude-opus-5-5",
  "source_name": "Anthropic",
  "company": "anthropic",
  "importance": 88,
  "tags": ["claude", "anthropic", "models"],

  "translations": {
    "ar": {
      "title": "أنثروبيك تطلق Claude Opus 5.5",
      "summary": "نموذج متقدم جديد بسياق أكبر.",
      "what_changed": "تضاعفت نافذة السياق إلى مليوني توكن.",
      "why_it_matters": "يمكن تنفيذ مهام المستندات الطويلة في طلب واحد بدلاً من التقسيم.",
      "worth_trying_note": "يستحق التجربة في مهام المستندات الطويلة."
    }
  },

  "image_prompt": "A conceptual editorial illustration of one very long document flowing into a single AI context window",
  "image_alt": "Conceptual illustration of a long document being processed by an AI model",
  "status": "published"
}
```

The transport minimum remains `title`, `summary`, `why_it_matters`,
`source_url`, and `category` so legacy append-only files can still be rerun.
For a **new** story requested as `published`, the v2 API additionally requires:

- a complete `translations.ar` containing `title`, `summary`, `what_changed`, and `why_it_matters`;
- `image_alt`;
- either `image_prompt` or an existing `image_url`.

Duplicate detection runs before these v2 completeness checks, preserving safe
reruns of older files.

## Primary sources and translation

Prefer English-language primary sources: the company announcement, API or
product changelog, official GitHub release, or original research publication.
The English briefing is the fact-bearing canonical copy. Arabic must be a
faithful translation of the same claims, not a separately researched rewrite.

## Generated images

For scheduled publishing, prefer `image_prompt` rather than an external image
URL. The prompt should describe the visual concept only. Do not request
headline text, captions, watermarks, fake product screenshots, or invented
metrics.

The API calls the configured OpenAI image model, adds the shared editorial
style constraints, writes the generated PNG to `PUBLISH_MEDIA_DIR`, and returns
a public URL under `/api/media/news/...`. The resulting URL, alt text, and image
credit are saved on the post.

Relevant server environment values:

```text
OPENAI_API_KEY=...
PUBLISH_IMAGE_MODEL=gpt-image-2.5-flare
PUBLISH_IMAGE_SIZE=1536x1024
PUBLISH_IMAGE_QUALITY=low
PUBLISH_MEDIA_DIR=storage/news-images
```

`AI_ENABLED=false` also disables paid story-image generation.

## GitHub Action safety

Before publishing, `scripts/publish-content.ts` fetches `GET /publish/schema`
and requires schema v2 with both bilingual external publishing and generated
story-image capabilities. This prevents a new content file from being sent to
an old server that would silently ignore `translations` or `image_prompt`.

The publish request timeout is three minutes because image generation can take
longer than an ordinary API request.

## Configuration

GitHub Actions needs:

| Kind | Name | Value |
|---|---|---|
| Secret | `PUBLISH_API_KEY` | Same key as production `PUBLISH_API_KEY` |
| Variable | `PUBLISH_API_BASE_URL` | e.g. `https://onairadar.com/api` |

The scheduled editor does not receive the publishing key, OpenAI key, database
credentials, or server access. It only commits story JSON.

## Outcomes

`PUBLISHED` means English, Arabic, and the image are ready and the story is
live. `HELD` means the server intentionally held the complete row, for example
because the daily cap was reached. `DUPLICATE` is a successful terminal result.
`REJECTED` or `FAILED` makes the Action fail visibly so the file can be fixed or
rerun.

## Schedule

The intended editor schedule is 09:00, 15:00, and 21:00 Africa/Cairo. The
schedule lives with the editor; the GitHub workflow reacts when story files are
committed.
