Systems · Content automation
Human-approved social publishing pipeline
A Python service for a football content workflow. It finds stories, drafts captions, checks and cuts out real photos, and renders branded posts, carousels and reels. A person picks, shapes and approves every post in Telegram before it goes to Instagram.
At a glance
Role
Designed and built it with Claude Code. I operate it and approve every post.
Tools
- Python
- Telegram Bot API
- Instagram Graph API
- Pillow
- FFmpeg
- Gemini 2.5 Flash
- BiRefNet on fal
Outcomes
- Nothing is drafted until a story is picked, and nothing is published without an explicit approval.
- Real photos pass five checks before they are offered; subjects are cut out, not generated.
- 342 offline tests pass with every network call stubbed.
Links
The problem
Football news moves fast, and an account needs posts within minutes, in a consistent voice and look. Fully automatic posting risks wrong facts and unusable photos; fully manual posting is too slow.
The service does the searching, drafting, checking and rendering. The person keeps the judgement: which story, which words, which photo, and whether it goes out at all.
From story to published post
-
01
Collect stories
Every two hours the service refreshes its story pool. Outside quiet hours (23:00 to 08:00) it sends a menu of up to ten stories, at most three per source.
-
02
Pick one
The operator picks a story, filters the menu, asks for a topic or pastes a link.
Human approval -
03
Draft and search
Three caption options in the house voice, and candidate photos from the article, image search and open libraries.
-
04
Shape it
Choose caption A, B or C, a headline word and photo 1, 2 or 3. Edit in plain language, undo, or send an exact edit.
Human approval -
05
Build the post
The cut-out, headline, grade and wordmark are rendered into a single post, carousel or reel preview.
-
06
Approve
Post as a single image or carousel, or cancel.
Human approval -
07
Publish
The post goes to Instagram and the live link comes back to the chat.
Human approval
Five checks before a photo is offered
- 01
Source filter
Watermarked stock-agency hosts are dropped before anything is downloaded.
- 02
Resolution
The short side must be at least 500 px. No thumbnails.
- 03
Vision check
A vision model confirms the right subject and face, and rejects watermarks and pre-made graphics.
- 04
Cut-out
A segmentation matte isolates the subject, who must fill at least half the frame height.
- 05
Crop
The frame is auto-cropped to 4:5 for the post canvas.
Output formats
| Format | Size | Built from |
|---|---|---|
| Single post | 1080 × 1350 | Fixed headline at the top, cut-out subject with rim light and shadow, grain, wordmark and accent colour |
| Carousel | 1080 × 1350 slides | Photos cropped to 4:5 with a light grade and the wordmark |
| Reel | 1080 × 1920 | A pasted clip with a hook card, highlighted punch word and source credit, composed with FFmpeg |
Limits that keep the operator in control
- Open drafts
- New menus pause while two drafts are waiting for a decision.
- Unpicked stories
- Return to the pool and can be offered again after 90 minutes.
- Failures
- A failed attempt returns the story to the menu instead of marking it used.
- Keys
- API keys live only on the server, never in the repository.
The menu gate
NEWS_INTERVAL_S = 2 * 60 * 60
QUIET_START, QUIET_END = 23, 8
MAX_OPEN_DRAFTS = 2
MENU_SIZE = 10
RE_PITCH_COOLDOWN_MIN = 90
SOURCE_CAP = 3
…
if not _awake():
log.info("cycle: quiet hours — pool refreshed, no menu")
return {"ok": True, "skipped": "quiet hours"}
_expire_stale_drafts()
open_rows = _open_draft_rows()
if len(open_rows) >= MAX_OPEN_DRAFTS:
…
stories = list_story_candidates()
if not stories:
return {"ok": True, "skipped": "nothing new to pitch"}
out = send_story_menu(stories)
The scheduler never drafts on its own. It only offers a menu, and holds it back in quiet hours or while decisions are pending.
Architecture
Operator
- Telegram chat: menus, options, previews, approve and cancel
Python service
- Scheduler and story pool
- Drafts: captions, headline words, edits
- Images: search, checks, cut-outs, crops
- Templates: posts, carousels, reel cards
- Reels: clip fetch and FFmpeg compose
Models
- Caption model: GLM, with Gemini 2.5 Flash as fallback
- Gemini 2.5 Flash for vision checks
- BiRefNet on fal for segmentation
Publishing
- Instagram Graph API: image, carousel and reel
- Cloudflare R2 for image hosting
One plain Python service using httpx, Pillow and boto3. Models are called over plain HTTP, with no agent framework.
Testing
- Tests
- 342 passing on 15 September 2026, run offline with a fake Telegram client and every network call stubbed.
- Screens
- The live chat and the published posts identify the account, so this page shows the flow, checks and formats instead of screenshots.