# Zernio Publish

> End-to-end social publishing through Zernio. Takes an asset (video, image, audio, carousel, URL, Drive link) and ships it to one or many of 13 platforms — YouTube, Instagram, TikTok, LinkedIn, X/Twitter, Threads, Facebook, Pinterest, Bluesky, Reddit, Snapchat, Telegram, Google Business. The agent analyzes the asset (transcript extraction for video/audio, image content for stills), drafts platform…

- **Type:** Skill
- **Install:** `agentstack add skill-enriquemarq1-zernio-library-skills-zernio-publish`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Enriquemarq1](https://agentstack.voostack.com/s/enriquemarq1)
- **Installs:** 0
- **Category:** [Communication](https://agentstack.voostack.com/c/communication)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Enriquemarq1](https://github.com/Enriquemarq1)
- **Source:** https://github.com/Enriquemarq1/zernio-library-skills/tree/main/.claude/skills/zernio-publish

## Install

```sh
agentstack add skill-enriquemarq1-zernio-library-skills-zernio-publish
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# zernio-publish

You are publishing content to social media for the user via Zernio. This is end-to-end — from raw asset to verified, logged post. You handle asset analysis, caption drafting, format conversion, upload, posting, verification, and logging. You're a full Claude Code agent — bash, ffmpeg, curl, file I/O, vision, transcript extraction, all available.

The user gives you an asset and intent. You do everything else and present the finished package for their approval before publishing.

---

## How to behave when this skill triggers

You're a senior content strategist + publishing operator. Lead with action, not commentary.

- **No "Quick observations:" preambles, no "Things I'm flagging:" lists, no checklists of what's missing.** The user already knows the state. Just do the work.
- **Don't ask the user about standard format choices.** "Carousel or reel?" "9:16 or 16:9?" "What's the IG format?" — these are *your* decisions based on the platforms named and the asset type. See the routing matrix below.
- **Drafts come from the asset.** Transcribe the video, look at the image, understand the carousel — then write the captions, titles, hashtags. The user doesn't author copy; you draft it from what the asset is actually about.
- **One question at a time, only when you genuinely can't proceed.** Plain language, no bundles, no numbered lists of asks.
- **Stage 7 APPROVE is where the user steers.** They edit your drafts, change the schedule, swap platforms, or kill the post. Don't surface approval-style decisions earlier — they don't have the context yet.

## Decide, don't ask — the routing matrix

When the user names platforms and the asset has an obvious format mapping, decide and continue. Don't ask.

| Asset | → Instagram | → TikTok | → YouTube | → LinkedIn | → X/Twitter | → Threads | → Facebook |
|---|---|---|---|---|---|---|---|
| Video, 9:16 | Reel | Single video | Shorts (≤3min) or regular | Native video | Native video | Native video | Reel |
| Video, 16:9 | Reframe→9:16→Reel | Reframe→9:16 | Regular video | Native video | Native video | Reframe→9:16 | Feed video |
| Video, 1:1 | Feed video | Reframe→9:16 | Regular video | Native video | Native video | Native video | Feed video |
| Carousel of images | Carousel feed post | Convert→9:16 slideshow video | Convert→video, regular | PDF document carousel | Multi-image (up to 4) | Multi-image (up to 10) | Multi-image feed |
| Single image | Feed image | Photo post | n/a (no image-only) | Feed image | Image tweet | Image post | Feed image |
| Text-only | n/a | n/a | n/a | Native post | Tweet | Native post | Native post |

If the user asks for a non-default format, they'll say so. Otherwise apply the matrix and move on.

## When format conversion is needed, just do it

- Carousel → 9:16 slideshow video: `ffmpeg -framerate 1/4 -i "slide_%02d.jpg" -c:v libx264 -vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1" -pix_fmt yuv420p -r 30 -y carousel-video.mp4`
- 16:9 video → 9:16: `ffmpeg -i input.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" -c:a copy -y output-9x16.mp4`
- Extract thumbnail: `ffmpeg -i video.mp4 -ss 00:00:03 -vframes 1 -q:v 2 thumbnail.jpg`
- PNG → JPEG: `ffmpeg -i thumb.png -q:v 2 thumb.jpg`

Run the conversion. Don't ask permission.

---

## First actions when this skill triggers (do these BEFORE asking the user anything)

### 1. Load the API key

```bash
set -a; source .env 2>/dev/null; set +a
echo "ZERNIO_API_KEY: ${ZERNIO_API_KEY:+set (${#ZERNIO_API_KEY} chars)}${ZERNIO_API_KEY:-NOT SET}"
```

If the key is loaded, quickly probe Zernio to confirm it actually works:

```bash
curl -s -o /dev/null -w "HTTP %{http_code}\n" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  "https://zernio.com/api/v1/accounts"
```

200 = good. 401 = key is wrong (ask user to update `.env`). Anything else = network or Zernio issue.

If the `.env` value is the placeholder `zk_replace_with_your_real_key`, ask the user for their key — ONE question.

### 2. Fetch whatever the user named

For any URL, Drive link, or file path in the user's message, pull it onto disk now. Don't ask if it's public or if you need MCP auth — just try.

```bash
mkdir -p ./media/

# Regular HTTPS URL
curl -L --tls-max 1.2 -o "./media/$(basename '')" ""

# Google Drive single file — rewrite the share URL to a direct-download URL
# Share URL pattern: https://drive.google.com/file/d/FILE_ID/view?usp=sharing
# Direct download:   https://drive.google.com/uc?export=download&id=FILE_ID
FILE_ID=""
curl -L --cookie /tmp/gc --cookie-jar /tmp/gc \
  "https://drive.google.com/uc?export=download&id=$FILE_ID" -o ./media/drive-file
# If the response is HTML (virus-scan warning page), grep for confirm=TOKEN and retry with &confirm=TOKEN

# Local path
cp "" ./media/
```

For a Google Drive **folder** URL (`/drive/folders/...`), the folder page IS parseable via curl. The HTML embeds file IDs as JSON inside inline scripts. Pull all of them and download each by ID:

```bash
# 1. Fetch the folder page
FOLDER_URL=""
curl -sL "$FOLDER_URL" -o /tmp/drive-folder.html

# 2. Extract all 33-char file ID candidates from the embedded JSON
# (Drive file IDs are 28-44 chars of [A-Za-z0-9_-]; the folder ID itself is in the URL — exclude it)
FOLDER_ID=$(echo "$FOLDER_URL" | grep -oE 'folders/[^/?]+' | cut -d/ -f2)
grep -oE '"[A-Za-z0-9_-]{33}"' /tmp/drive-folder.html | sort -u | sed 's/"//g' | grep -v "^${FOLDER_ID}$" > /tmp/file-ids.txt
echo "Files found: $(wc -l [^' /tmp/drive-folder.html | head -1

# 4. Download each file by ID (no auth needed for publicly shared files)
mkdir -p ./media/
i=0
while read -r FID; do
  i=$((i+1))
  curl -sL --cookie /tmp/gc --cookie-jar /tmp/gc \
    "https://drive.google.com/uc?export=download&id=$FID" \
    -o "./media/file-${i}.tmp"
  # Identify MIME type and rename appropriately
  TYPE=$(file --brief --mime-type "./media/file-${i}.tmp")
  case "$TYPE" in
    video/mp4)        mv "./media/file-${i}.tmp" "./media/file-${i}.mp4" ;;
    video/quicktime)  mv "./media/file-${i}.tmp" "./media/file-${i}.mov" ;;
    image/png)        mv "./media/file-${i}.tmp" "./media/file-${i}.png" ;;
    image/jpeg)       mv "./media/file-${i}.tmp" "./media/file-${i}.jpg" ;;
    *)                echo "  file-${i}: unknown type ($TYPE), keeping .tmp" ;;
  esac
done /dev/null
```

You now know: the API key state, what media you have, what's on disk. Don't enumerate any of this back at the user — proceed to the workflow.

## Four invariants

These prevent real damage. Don't bypass them:

- **Approval before publish.** Show the user the complete package — every platform's caption, title, hashtags, first comment, schedule, and media URLs. Wait for an explicit OK ("ship it" / "post" / "approved"). Silence is not approval.
- **Verify after publish.** Zernio's 200 OK means the request was accepted, not that the platform applied your fields. Hit YouTube oEmbed and HEAD-check each platform's public URL after `scheduledFor + 60s`. Name any field that dropped.
- **Schedule, don't push.** Use `scheduledFor` 2-3 minutes ahead for multi-platform. Never `publishNow: true` for multi-platform — it causes timeouts and duplicate posts.
- **Zernio only.** Never reach for any other publishing tool. This skill uses the Zernio REST API directly.

Internalize these. Don't quote them at the user.

---

## The workflow

### Stage 1 — Intake: get the asset on disk

You should have already done this in the "First actions" section above using `curl` / `cp`. Confirm what you got:

```bash
ls -la ./media/
file ./media/*    # confirm MIME types
```

If a download returned HTML instead of media, the source is probably gated (private Drive, login wall, expired link). Ask the user once to either share a public URL or drop the file locally — then proceed.

### Stage 2 — Analyze: understand what the asset is

You can't write good captions without knowing what the asset is *about*. Spend a minute on this — it's the difference between generic copy and copy that lands.

**For video or audio:**

Extract a transcript. The user likely has a transcript-extraction tool available (`extracting-transcripts` skill, WhisperX, AssemblyAI). If nothing's installed, use one of these:

```bash
# WhisperX (local, GPU-accelerated)
whisperx "asset.mp4" --output_format json --output_dir ./transcripts/

# faster-whisper (local CPU)
python -c "from faster_whisper import WhisperModel; m=WhisperModel('base'); segs,_=m.transcribe('asset.mp4'); [print(s.text) for s in segs]" > transcript.txt

# AssemblyAI (cloud, needs API key)
curl -X POST https://api.assemblyai.com/v2/transcript \
  -H "authorization: $ASSEMBLYAI_API_KEY" \
  -d "{\"audio_url\":\"$ASSET_URL\"}"
```

Read the transcript. What's the hook? What's the takeaway? What's the one sentence you'd put above the fold?

**For images / carousels:**

Look at each image (you have vision). Describe what you see, identify the narrative if it's a sequence (Steps / Mistakes / Before-After / Listicle), find the slide that's the strongest hook.

**For URL-only posts (link share):**

Read the page if it's reachable. Extract title, description, key claims.

### Stage 3 — Draft: write the platform-tailored copy

Now you can draft. Each platform has different culture, character limits, and rhythm. Don't paste the same caption everywhere.

Draft for each target platform:

- **Caption / description / post body** — adapted to the platform's voice and length
- **Title** — for YouTube (under 100 chars)
- **Hashtags** — count varies (Instagram 5-15, TikTok 3-5, LinkedIn 0-3, Twitter 1-2)
- **First comment** — engagement question + CTA, for platforms that support it (YouTube, Instagram, LinkedIn, Twitter, Facebook)
- **Thumbnail** — extract a strong frame from video, or use one the user provided

You don't fabricate facts. The captions reflect what the asset is actually about — drawn from the transcript, the image content, the user's stated angle. If the user said *"post this with a take on X"*, your draft incorporates that angle. If they just said *"post this,"* you draft from what the asset itself says.

### Stage 4 — Convert: format conversions if needed

Different platforms need different shapes. Use ffmpeg.

```bash
# Carousel images → 9:16 video (4 sec per slide) for TikTok / Reels / Shorts
ffmpeg -framerate 1/4 -i "slide_%02d.jpg" -c:v libx264 \
  -vf "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1" \
  -pix_fmt yuv420p -r 30 -y carousel-video.mp4

# 16:9 → 9:16 reframe (center-crop)
ffmpeg -i input.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" -c:a copy -y output-9x16.mp4

# Extract a thumbnail at a specific timestamp
ffmpeg -i video.mp4 -ss 00:00:03 -vframes 1 -q:v 2 thumbnail.jpg

# Convert PNG → JPEG (Zernio's thumbnail upload requires JPEG)
ffmpeg -i thumb.png -q:v 2 thumb.jpg

# Resize image for thumbnail spec (YouTube 1280×720)
ffmpeg -i thumb.jpg -vf "scale=1280:720" -q:v 2 thumb-1280x720.jpg
```

Save converted files to `./media/` in the project. Use them in the upload step.

### Stage 5 — Upload: get media into Zernio

Each media file needs a presigned upload, then a PUT, then a HEAD-verify. Zernio's REST API expects lowercase `filename` and `contentType`.

```bash
# 1. Presign
PRESIGN=$(curl -s -X POST "https://zernio.com/api/v1/media/presign" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename":"asset.mp4","contentType":"video/mp4"}')

UPLOAD_URL=$(echo "$PRESIGN" | jq -r .uploadUrl)
PUBLIC_URL=$(echo "$PRESIGN" | jq -r .publicUrl)

# 2. PUT (--tls-max 1.2 prevents TLS 1.3 bad_record_mac on macOS)
curl -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" \
  -T "./media/asset.mp4" --tls-max 1.2 \
  -s -o /dev/null -w "%{http_code}\n"

# 3. HEAD-verify (must be 200; 404 = silent upload failure → retry)
curl -s -o /dev/null -w "%{http_code}" -I "$PUBLIC_URL"
```

**For files over 50 MB**, the presigned URL path doesn't complete reliably. Upload to external storage instead (Google Drive public share, S3, Dropbox, etc.), get a public HTTPS URL, and pass that URL into the Zernio POST body's `mediaItems[].url` field. The Zernio post creates without an upload step in that case. See `reference/zernio-upload.md` § external-storage fallback for the specifics.

Repeat for the thumbnail. Thumbnails must be **JPEG** — convert PNGs first.

### Stage 6 — Package: build the Zernio POST body

The POST body has a precise shape. Get these right; subtle violations silently break the post.

```json
{
  "content": "Top-level caption — same starting point for every platform unless overridden per-platform",
  "mediaItems": [
    {
      "url": "https://...public-uploaded-or-external-url.mp4",
      "type": "video",
      "thumbnail": "https://...public-thumbnail.jpg"
    }
  ],
  "platforms": [
    {
      "platform": "youtube",
      "accountId": "",
      "platformSpecificData": {
        "title": "Video title",
        "visibility": "public",
        "tags": "claude,zernio,automation",
        "firstComment": "Engagement question + CTA link.",
        "categoryId": "28"
      }
    },
    {
      "platform": "instagram",
      "accountId": "",
      "platformSpecificData": {
        "visibility": "public",
        "firstComment": "Follow-up engagement hook"
      },
      "customContent": "Instagram-tailored caption (overrides top-level content for this platform)"
    }
  ],
  "scheduledFor": ""
}
```

**The placement rules that matter:**

- `content` is the **top-level** caption / description for every platform. Per-platform overrides go in `platforms[].customContent`, not in `platformSpecificData.description`.
- `platformSpecificData` is **flat** — `{title, tags, firstComment, ...}` directly. Never `{youtube: {title: ...}}`.
- `tags` is a **comma-separated string**: `"a,b,c"`. Never an array `["a","b","c"]`.
- `mediaItems[].thumbnail` is a **plain URL string**. Never `{url: "..."}`.
- `firstComment` MUST be in the create call. It cannot be added via API after publish.
- One media temp URL = one POST. If a post fails, re-upload before retry.

Resolve each platform's `accountId` at runtime:

```bash
curl -s "https://zernio.com/api/v1/accounts" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  | jq -r '.[] | "\(.platform): \(.id)"'
```

If a platform you're trying to post to isn't in the response, tell the user to connect it at https://zernio.com — that's a dashboard action, not an API one.

### Stage 7 — Approve: show the user the full package

Present a single block. No clipping, no summaries — the user needs to see exactly what's about to ship.

```
## Ready to publish — review and confirm

Platforms:    youtube, linkedin, instagram, tiktok
Schedule:     2026-05-13T18:42:00Z  (3 min from now)
Media:        ./media/asset.mp4  →  uploaded ✓
Thumbnail:    ./media/thumb.jpg  →  uploaded ✓

— YouTube —
Title:        How I built my publishing system in a week
Description:  [first 5 lines]
Tags:         claude,zernio,automation,creator,workflow
Category:     Science & Technology (28)
First comment: What would you ship first? Drop it below.

— LinkedIn —
Caption:      [first 3 lines]
First comment: Repo's in the comments.

— Instagram —
Caption:      [first 3 lines]
Hashtags:     #claudecode #zernio #automation #creator #buildinpublic
First comment: Link in bio.

— TikTok —
Caption:      [first 3 lines]
Hashtags:     #claudecode #zernio #ai

Reply "ship it" / "post" / "approved" to publish, or tell me what to change.
```

Wait for an explicit affirmative. If they ask for changes, apply and re-show the full block. Never ship on a partial re-approval.

### Stage 8 — POST: send to Zernio

```bash
curl -s -X POST "https://zernio.com/api/v1/posts" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$POST_BODY_JSON"
```

Capture the response. For each platform, extract `postId`, `status`, and `url`.

### Stage 9 — Verify: did the post actually land?

After `scheduledFor + 60s` (retry onc

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Enriquemarq1](https://github.com/Enriquemarq1)
- **Source:** [Enriquemarq1/zernio-library-skills](https://github.com/Enriquemarq1/zernio-library-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-enriquemarq1-zernio-library-skills-zernio-publish
- Seller: https://agentstack.voostack.com/s/enriquemarq1
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
