postkitty docs

Install the plugin, learn how to ask, and look up every tool, layout, limit and API endpoint.

Start

postkitty is a plugin for Hermes Agent. Install it once, open a new chat and ask in plain words.

Install

Run this in a terminal. The plugin comes from the official Hermes plugin catalog.

hermes plugins install postkitty

Answer yes when Hermes asks about the Python dependencies and about turning the plugin on. This form skips both questions:

hermes plugins install postkitty --yes-deps --enable

Then open a new chat. Hermes loads a plugin's tools when the next chat starts.

You can also let Hermes install it. Send this message:

Install the postkitty plugin for me: run `hermes plugins install postkitty --yes-deps --enable` in the terminal. When it finishes, tell me to open a new chat so its tools load.

What you need

You needForHow you get it
Hermes Agent 0.22.7 or newer
Everything
Update Hermes if yours is older.
Five Python packages: OpenCV, NumPy, Pillow, regex and ruamel.yaml
Photos, video and counting
They install with the plugin. Nothing to do.
ffmpeg and ffprobe
Video only
Hermes brings its own ffmpeg. On Windows and most Linux systems that is all video needs. On macOS it comes without ffprobe, so run brew install ffmpeg once.
The YuNet face detection model
Photos and video
postkitty downloads it once from the OpenCV Zoo and verifies its checksum.
An ffmpeg build with libass
Captions only
The ffmpeg that Hermes brings has it. With an ffmpeg of your own, look at the Captions row of /postkitty doctor.
One font on the machine
Captions only
A desktop has plenty. On a bare server or in a container, install one: on Ubuntu, for example, fonts-dejavu-core.

postkitty needs no API key and no account. Text needs none of the media tools: without ffmpeg it still writes and counts your text.

To check all of it at once, type this in a Hermes chat:

/postkitty doctor

It reports on Python, ffmpeg and ffprobe, caption support, OpenCV, Pillow, the face model and the output folder. Each problem comes with one line on how to fix it, and the last line says what works now: text, photos, video, captions.

Your first request

In the new chat, name the platforms and give Hermes your draft or the full path of each file.

Use postkitty to turn this into posts for X, LinkedIn and Instagram and fix my wording: [your draft, or the path to a photo or video]

One job takes one photo or one video, or one of each, plus the text. For several photos Hermes runs postkitty once per photo. postkitty does not build a carousel.

Hermes replies with the path of a job folder. The files that passed every check are in ready/, one folder per platform. Open board.html to see each file next to its text, its alt text and its check results.

postkitty/<job>/
  ready/
    instagram/
    linkedin/
    x/
  board.html
  advice.md

The job folder is created under ./postkitty in the folder your Hermes session works in. Settings lets you change that.

How to ask

You talk to Hermes in plain words and Hermes calls postkitty. A request that names the platforms, the files and the result you want gets it right the first time. Each pair below shows a vague request and a clear one.

Name the platforms

Without names, postkitty makes every platform that fits your files. Naming them keeps the job short.

Vague Make this ready for social media.

Clear

Use postkitty to make [path to the video] ready for TikTok, Instagram Reels and YouTube Shorts.

Say who must stay in frame

postkitty keeps every face it finds. Say so when one person matters more, or when nobody may be cut off. People are counted from the left.

Vague Crop this photo.

Clear

Crop [path to the photo] for the Instagram feed with postkitty. Keep all five people in the picture and put the second person from the left in the middle.

Use the layout words

Four words choose how a wide video becomes a tall one: crop, split, speaker and wide shot, which is the pad layout. Layouts shows each of them.

Vague Make it vertical.

Clear

Two people talk in [path to the video]. Split the screen for Reels, cut to whoever is talking for TikTok, and keep the whole wide shot for Shorts.

Give trim and cover times

Give times in seconds or as minutes and seconds. Without them postkitty cuts a clip only where the platform has a hard length limit, and picks the cover frame itself.

Vague Make it shorter and pick a nice thumbnail.

Clear

Trim the Reel to the first 45 seconds and use the frame at 0:12 as the cover.

Set the tone and the language

The wording comes from the model your Hermes uses. It keeps your voice and your language unless you say otherwise.

Vague Write something good for this.

Clear

Write the LinkedIn post in a formal tone and the X post in a casual one, both in German. Keep my facts as they are.

Say thread or single post

Text that runs over the limit on X, Threads or Bluesky can become a numbered thread. Say which one you want.

Vague Post this on X.

Clear

Turn this into a thread on X with postkitty and keep my tone. Keep the Bluesky version to a single post: [your long post]

Bring captions as a file

postkitty burns captions in from an .srt or .vtt file, at most two lines at a time, inside the platform's caption-safe area. It does not transcribe audio, so bring the file.

Vague Add subtitles to the video.

Clear

Burn the captions from [path to the .srt file] into the TikTok and Reels clips with postkitty.

Ask for a change

Name the platform and what should change. postkitty makes only that text or file again and checks it again.

Vague I don't like it, try again.

Clear

Make the LinkedIn post shorter and more formal, use other hashtags, and put the person on the left in focus in the Instagram photo.

Ask whether a file will upload

postkitty can check a file it did not make against one platform's limits. Name the file and the platform.

Vague Is this video okay?

Clear

Will [path to the file] upload to TikTok as it is? Check it with postkitty.

Tools

Hermes calls five tools, in this order: scan, write, make, then redo for changes. check is for files postkitty did not make. You never call them yourself. This section helps you read what Hermes did and build on the plugin.

postkitty_scan

Reads the inputs and plans the job

Reads the inputs and plans the job. It creates the job folder, finds people, shots and scenes, and plans a framing for each platform target. It renders nothing, and it also works with text alone.

ArgumentTypeWhat it is
inputs
string[]
Path of one photo or one video, or one of each. A relative path starts at the folder your session works in.
text
string
Your draft, if you have one.
platforms
string[]
Platform names (instagram, tiktok, youtube, x, linkedin, threads, bluesky, facebook) or target ids. Left out: every target that fits the inputs.
job
string
An existing job id, to continue that job.

Returns. The job id, the facts about each input (kind, size, length, people, shots), the plan for each target (layout, trim, cover frame), the limits each platform sets, the counts of your text and the path of a preview image.

postkitty_write

Counts each platform's text and writes the text files

Counts each platform's text with that platform's real rule and writes the text files. Text that is over the limit on X, Threads or Bluesky is split into a numbered thread. Hermes calls it again with a rewritten draft until every platform reports ok, and again for every later change to the text.

ArgumentTypeWhat it is
jobrequired
string
The job id that postkitty_scan returned.
advice
object
What Hermes worked out about the post: topic, audience, tone, key_message, a ranking of platforms with a reason each, and tips.
drafts
object
One draft per platform name or target id. A list of strings is a thread.
alt_text
object
Alt text per target id, input file name or ready file path. It describes only what is visible.
split_threads
boolean
Split text that is over the limit into a thread. Default true.

Returns. For each platform: the count, the unit, the limit, ok, the overflow and the part that stays visible before the feed cuts the text off. It writes advice.md into the job folder and the post.txt, thread.txt and alt.txt files into ready/.

postkitty_make

Renders, checks, repairs and builds the review board

Renders every planned target, checks each file against the platform's published limits, repairs what fails and puts only passing files into ready/. It also writes the review board. A job with text only has nothing to render.

ArgumentTypeWhat it is
jobrequired
string
The job id that postkitty_scan returned.
targets
string[]
Only these target ids or platforms. Left out: every planned target.
overrides
object
Per target id, any of layout, focus, trim, cover_at and captions, as described under postkitty_redo.

Returns. A table with one row per target. A file is ready to upload only when its status is ready or repaired. failed rows say what is wrong, skipped rows say which program is missing on your machine.

postkitty_redo

Changes one photo or video output

Renders one photo or video target again after you ask for a change: another layout, another person in focus, a trim, another cover frame or captions. It reuses the scan, so it is quick. When the new render fails, the earlier passing file stays in place.

ArgumentTypeWhat it is
jobrequired
string
The job id that postkitty_scan returned.
targetrequired
string
The target to render again, for example tiktok or instagram_feed.
layout
crop | split | speaker | seam | pad | auto
How the frame is built. See Layouts.
focus
string
Who or what the frame keeps. See Focus.
trim
[number, number]
Start and end in seconds. Video only.
cover_at
number
Time of the cover frame in seconds. Video only.
captions
string
Path of an .srt or .vtt file to burn into the video. none removes captions set earlier. Video only.

Returns. The same table as postkitty_make, for that one target.

postkitty_check

Checks a file postkitty did not make

Checks a file that postkitty did not make against one platform target, without rendering anything. Files from make and redo are already checked.

ArgumentTypeWhat it is
path
string
Path of the file to check.
target
string
A target id such as x_image, or a platform name when it has one target for that kind of file, for example tiktok.
job
string
A job id, to check that job's ready files again.

Returns. Pass or fail, and every check that failed with the limit it broke.

The /postkitty command

Type these in a Hermes chat. They answer at once and do not use the model.

CommandWhat it does
/postkitty
What postkitty does, example requests and every target with its size and main limits.
/postkitty tiktok
One platform's targets and limits. Use any platform name.
/postkitty doctor
Checks your setup and gives one line on how to fix each problem.
/postkitty check <file> [target]
Checks one file against one target and prints the result. Without a target it asks which one.

The same engine runs from a terminal, for scripts and cron jobs:

CommandWhat it does
hermes postkitty doctor
The setup check.
hermes postkitty platforms [name]
Every target and its limits, or one platform's.
hermes postkitty check FILE [--target ID]
Checks one file against one target. The exit code is 1 when the file fails.
hermes postkitty make [FILES] [options]
Makes and checks every file for one post and prints the status table. It counts and splits the text as given and does not rewrite it. Options: --text, --text-file, --platforms, --layout, --focus, --captions, --out. The exit code is 0 only when every file is ready.

Settings

Three settings, all optional. Change them in the plugin's settings in Hermes.

SettingWhat it isDefault
output_dirOutput folder
Where job folders are written.
./postkitty in the folder your session works in
default_platformsDefault platforms
The platforms postkitty makes when you name none.
Every platform that fits the input
loudness_lufsLoudness target (LUFS)
The loudness that video audio is levelled to.
-14

Layouts

A layout decides how a wide picture becomes the shape a platform wants. postkitty picks one for each target. Ask for another whenever you like.

crop

crop, follow the person

Cuts the frame to the new shape and keeps the subject in it. In video the frame follows the person as they move.

A potter standing at the left edge of a wide photo. The center crop shows an empty window; the postkitty crop keeps her in frame. Enlarge image

split

split screen, stack them

Stacks two people in one tall frame, each in their own half. Good when two people sit far apart.

Two podcast hosts sitting far apart in a wide shot. The center crop shows the empty table between them; postkitty stacks the two hosts in one tall frame. Enlarge image

speaker

cut to whoever is talking

Shows one person at a time and cuts to whoever is talking. Video only.

seam

squeeze the background

Removes background between two people so both fit, without cutting either of them. Photos only.

pad

the whole wide shot, keep everything

Keeps the whole picture and fills the rest of the frame with a blurred copy of it. Nothing is cut off.

Five people in a wide team photo. The center crop cuts off the two at the edges; postkitty keeps all five and fills the frame above and below with a blurred band. Enlarge image

Focus

Focus says who or what the frame is built around. In plain words: “put the woman on the left in the middle”. Left alone, postkitty cuts nobody off. A focus you set is kept even when someone then falls outside the frame, and the result says who.

ValueWhat it is
auto
postkitty decides from the faces it finds. The default.
person:N
One person, counted from the left: person:1 is the leftmost.
left
The left part of the picture.
center
The middle of the picture.
right
The right part of the picture.
x:0.0-1.0
A point across the width: x:0.0 is the left edge, x:1.0 the right edge.

Platforms and limits

These tables are built from platforms.yaml, the file postkitty checks against. Every limit links to the page it came from.

Checked on

Text limits

Each platform counts text its own way, and postkitty counts the same way.

PlatformLimitRuleSource
Instagram
2,200 characters
Checked and repaired
Instagram
5 hashtags
Checked and repaired
Instagram
20 mentions
Checked and repaired
TikTok
2,200 UTF-16 units
Checked and repaired
YouTube Shorts
100 characters
Checked and repaired
X
280 weighted characters
Checked and repaired
LinkedIn
3,000 characters
Checked and repaired
Threads
500 characters
Checked and repaired
Bluesky
300 graphemes
Checked and repaired
Bluesky
3,000 bytes
Checked and repaired
Facebook
63,206 characters
Advice only
  • X: postkitty uses a port of X's published twitter-text rules. A link counts as 23 characters, and a file name such as README.md counts as a link.
  • Threads counts an emoji as its UTF-8 bytes.
  • Bluesky's 3,000 bytes apply to each post of a thread.
  • YouTube's 100 characters are the limit for the title of the Short.

Sizes and file limits

A target is one kind of file for one platform. Open a platform to see what postkitty makes for it and every limit it checks.

Instagram

Reel, feed photo, Story

Reel instagram_reelvideo, 1080 × 1920 px

LimitValueRuleSource
File type
MP4, MOV
Checked and repaired
Video codec
H.264, HEVC
Checked and repaired
Audio sample rate
48 kHz
Checked and repaired
Lowest frame rate
23 fps
Checked and repaired
Highest frame rate
60 fps
Checked and repaired
Shortest length
3 s
Checked and repaired
Longest length
15 min
Checked and repaired
Largest file
300 MB
Checked and repaired
Highest video bitrate
25 Mbit/s
Checked and repaired
Highest audio bitrate
128 kbit/s
Checked and repaired
Area to keep clear
top 14%, bottom 35%, sides 6%
Advice only

feed photo instagram_feedimage, 1080 × 1350 px

LimitValueRuleSource
Image format
JPEG
Checked and repaired
Largest file
8 MB
Checked and repaired
Aspect ratios, from and to
4:5, 1.91:1
Checked and repaired

Story instagram_storyimage or video, 1080 × 1920 px

LimitValueRuleSource
Longest length
1 min
Advice only
Area to keep clear
top 14%, bottom 35%, sides 6%
Advice only

TikTok

video

video tiktokvideo, 1080 × 1920 px

LimitValueRuleSource
File type
MP4, WEBM, MOV
Checked and repaired
Video codec
H.264, HEVC, VP8, VP9
Checked and repaired
Lowest frame rate
23 fps
Checked and repaired
Highest frame rate
60 fps
Checked and repaired
Shortest side
360 px
Checked and repaired
Longest side
4,096 px
Checked and repaired
Longest length
10 min
Checked and repaired
Largest file
4 GB
Checked and repaired
Area to keep clear
top 12.5%, bottom 34.4%, left 11.1%, right 27.8%
Advice only

YouTube Shorts

Short

Short youtube_shortsvideo, 1080 × 1920 px

LimitValueRuleSource
Longest length
3 min
Checked and repaired
Highest video bitrate
8 Mbit/s
Advice only
Area to keep clear
top 15%, bottom 35%, left 4.4%, right 17.8%
Advice only

X

video, image

video x_videovideo, 1920 × 1080 px

LimitValueRuleSource
Longest length
140 s
Checked and repaired
Largest file
512 MB
Checked and repaired

image x_imageimage, 1600 × 900 px

LimitValueRuleSource
Image format
JPEG, PNG, GIF, WEBP
Checked and repaired
Largest file
5 MB
Checked and repaired

LinkedIn

video, image

video linkedin_videovideo, 1080 × 1350 px

LimitValueRuleSource
Shortest length
3 s
Checked and repaired
Longest length
15 min
Checked and repaired
Smallest file
75 kB
Checked and repaired
Largest file
5 GB
Checked and repaired
Lowest frame rate
10 fps
Checked and repaired
Highest frame rate
60 fps
Checked and repaired
Highest video bitrate
30 Mbit/s
Checked and repaired
Narrowest aspect ratio
1:2.4
Checked and repaired
Widest aspect ratio
2.4:1
Checked and repaired

image linkedin_imageimage, 1080 × 1080 px

LimitValueRuleSource
Aspect ratios
1:1, 1.91:1, 4:5
Advice only

Threads

video, image

video threads_videovideo, 1080 × 1920 px

LimitValueRuleSource
File type
MP4, MOV
Checked and repaired
Video codec
H.264, HEVC
Checked and repaired
Lowest frame rate
23 fps
Checked and repaired
Highest frame rate
60 fps
Checked and repaired
Longest length
5 min
Checked and repaired
Largest file
1 GB
Checked and repaired

image threads_imageimage, 1080 × 1350 px

LimitValueRuleSource
Image format
JPEG, PNG
Checked and repaired
Largest file
8 MB
Checked and repaired

Bluesky

video, image

video bluesky_videovideo, 1080 × 1920 px

LimitValueRuleSource
Largest file
300 MB
Checked and repaired
Longest length
10 min
Advice only

image bluesky_imageimage, 1080 × 1350 px

LimitValueRuleSource
Largest file
2 MB
Checked and repaired

Facebook

Reel, feed photo

Reel facebook_reelvideo, 1080 × 1920 px

LimitValueRuleSource
Aspect ratios
9:16
Advice only
Area to keep clear
top 14%, bottom 35%, sides 6%
Advice only

feed photo facebook_feedimage, 1080 × 1350 px

LimitValueRuleSource
Aspect ratios
4:5
Advice only

API

The website runs the same engine behind an HTTP API, and you can call it too. It has no language model: it counts, checks and splits text, and it makes and checks files, but it does not rewrite your wording. You need no key.

https://postkitty.mertkayacs.com/api
MethodEndpointWhat it does
GET
/v1/health
Whether the service is up, and its version.
GET
/v1/limits
The service's own caps as numbers: files per job, file sizes, video length, text length and hourly rates.
GET
/v1/platforms
Every target with its size and limits.
POST
/v1/text
Counts a text for each platform and splits it into a thread where it runs over.
POST
/v1/check
Checks one uploaded file against one target.
POST
/v1/jobs
Starts a job from uploaded photos or one video. Replies at once with the job id.
GET
/v1/jobs/{id}
The state of a job: queued, running, done or failed, with its targets and files.
GET
/v1/jobs/{id}/files/{path}
One finished file.
GET
/v1/jobs/{id}/download
All finished files as one zip.
DELETE
/v1/jobs/{id}
Deletes the job and its files now.

Count and split text

Send JSON with text. platforms is optional and defaults to all eight. Set split_threads to false to get the overflow instead of a thread.

curl -s https://postkitty.mertkayacs.com/api/v1/text \
  -H "Content-Type: application/json" \
  -d '{"text": "We shipped postkitty 1.0 today.", "platforms": ["x", "bluesky"]}'
import requests

reply = requests.post(
    "https://postkitty.mertkayacs.com/api/v1/text",
    json={"text": "We shipped postkitty 1.0 today.", "platforms": ["x", "bluesky"]},
    timeout=30,
)
reply.raise_for_status()
for platform, result in reply.json()["platforms"].items():
    print(platform, result["count"], "of", result["limit"], "ok" if result["ok"] else "too long")
const reply = await fetch("https://postkitty.mertkayacs.com/api/v1/text", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ text: "We shipped postkitty 1.0 today.", platforms: ["x", "bluesky"] }),
})
if (!reply.ok) throw new Error((await reply.json()).error.message)
const { platforms } = await reply.json()
for (const [platform, result] of Object.entries(platforms)) {
  console.log(platform, result.count, "of", result.limit, result.ok ? "ok" : "too long")
}

The reply has one entry per platform: count, limit, ok, the overflow, the part that stays visible in the feed, and the thread parts.

Check a file

Send the file as multipart/form-data with a target, for example tiktok or instagram_feed. The reply says whether it passes and lists the checks that failed.

curl -s https://postkitty.mertkayacs.com/api/v1/check \
  -F "[email protected]" \
  -F "target=tiktok"
import requests

with open("clip.mp4", "rb") as clip:
    reply = requests.post(
        "https://postkitty.mertkayacs.com/api/v1/check",
        files={"file": clip},
        data={"target": "tiktok"},
        timeout=120,
    )
reply.raise_for_status()
print(reply.json())
const form = new FormData()
form.append("file", fileInput.files[0])
form.append("target", "tiktok")

const reply = await fetch("https://postkitty.mertkayacs.com/api/v1/check", { method: "POST", body: form })
if (!reply.ok) throw new Error((await reply.json()).error.message)
console.log(await reply.json())

Make files

Upload a photo or a video as files. GET /v1/limits says how many files a job takes. text, platforms, layout and focus are optional, and a job with text alone is fine. The call returns at once with the job id and its links. Ask for the job every two seconds until its status is done or failed, then download single files or the zip, which also holds the review board.

# 1. Start a job. The reply carries its id.
curl -s https://postkitty.mertkayacs.com/api/v1/jobs \
  -F "[email protected]" \
  -F "text=Meet the team behind the launch." \
  -F "platforms=instagram,bluesky"

# 2. Ask every two seconds until status is done or failed.
curl -s https://postkitty.mertkayacs.com/api/v1/jobs/JOB_ID

# 3. Download the finished files as one zip.
curl -s -o postkitty.zip https://postkitty.mertkayacs.com/api/v1/jobs/JOB_ID/download

# 4. Delete the job now instead of waiting.
curl -s -X DELETE https://postkitty.mertkayacs.com/api/v1/jobs/JOB_ID
import time
import requests

API = "https://postkitty.mertkayacs.com/api"

with open("team.jpg", "rb") as photo:
    reply = requests.post(
        f"{API}/v1/jobs",
        files={"files": photo},
        data={"text": "Meet the team behind the launch.", "platforms": "instagram,bluesky"},
        timeout=120,
    )
reply.raise_for_status()
job = reply.json()

while job["status"] in ("queued", "running"):
    time.sleep(2)
    job = requests.get(API + job["links"]["status"], timeout=30).json()

if job["status"] == "done":
    archive = requests.get(API + job["links"]["download"], timeout=120)
    with open("postkitty.zip", "wb") as out:
        out.write(archive.content)
else:
    print(job["error"]["message"], job["error"]["hint"])

requests.delete(API + job["links"]["status"], timeout=30)
const API = "https://postkitty.mertkayacs.com/api"

const form = new FormData()
form.append("files", fileInput.files[0])
form.append("text", "Meet the team behind the launch.")
form.append("platforms", "instagram,bluesky")

let job = await (await fetch(API + "/v1/jobs", { method: "POST", body: form })).json()
if (job.error) throw new Error(job.error.message)

while (job.status === "queued" || job.status === "running") {
  await new Promise((resolve) => setTimeout(resolve, 2000))
  job = await (await fetch(API + job.links.status)).json()
}

if (job.status === "done") {
  for (const file of job.files) console.log(file.path, API + file.url)
  console.log("Everything as one zip:", API + job.links.download)
} else {
  console.error(job.error.message, job.error.hint)
}

Errors

Every error has the same body. The message is one plain sentence and the hint says what to do next. Both are English, so branch on code if you translate.

{
  "error": {
    "code": "...",
    "message": "one plain sentence",
    "hint": "what to do"
  }
}

Two codes ask you to wait: rate_limited (HTTP 429) and queue_full (HTTP 503). Both send a Retry-After header with the seconds to wait.

Limits and deletion

  • Uploads only. The API never fetches a URL.
  • Photos can be JPEG, PNG, WebP or GIF; video can be MP4, MOV, MKV or WebM. Animated images are refused. GET /v1/limits returns every cap as a number: files per job, file sizes, video length and text length.
  • Calls are rate-limited per visitor and hour.
  • Your uploads are deleted when the job finishes. The finished files are deleted 60 minutes later, at the time in expires_at. DELETE removes them at once.
  • Nothing is posted, published or shared.

Architecture

For developers who want to read or change the code.

  • plugin.yaml is the manifest: name, version, tools, dependencies and settings.
  • __init__.py registers the tools, the skill and the /postkitty command with Hermes.
  • schemas.py holds what the model sees: tool names, descriptions and parameters. tools.py holds thin handlers that validate arguments and call the engine.
  • engine/ is a plain Python library with no Hermes imports. engine/pipeline.py is its public interface: scan, write, make, redo and check, each taking and returning a dict. The HTTP API calls the same functions.
  • Inside the engine: detect/ finds faces, photo/ and video/ plan and render, text/ counts and splits, verify/ checks and repairs, board/ builds the review board.
  • data/platforms.yaml holds every size and limit with its source. Change a limit there and the checks follow.
  • skills/postkitty/SKILL.md tells the model how to use the tools.

The model writes the advice and the wording. Everything else is deterministic code: detection, framing, rendering, counting, checking and repair. The tools decide whether a file passes, never the model.

More in the repository: developer docs on GitHub, SKILL.md and platforms.yaml.

Troubleshooting

The common problems, each with its fix. Start with /postkitty doctor. It finds most setup problems and says how to fix them.

Hermes does not know postkitty after the install

Open a new chat. Hermes loads a plugin's tools when a chat starts, so the chat you installed from does not have them. If they are still missing, the plugin is not turned on: run hermes plugins enable postkitty.

Video targets are skipped

ffmpeg or ffprobe is missing. Install ffmpeg, make sure ffmpeg and ffprobe run in your terminal, then ask again. On macOS the ffmpeg that Hermes brings has no ffprobe, and brew install ffmpeg adds it. Text and photos work without them.

Photos or video report that the face detector is not available

The face model downloads once, on first use, and that needs a network connection. Go online and ask again. Text works without it.

Captions are refused because ffmpeg has no libass

Your ffmpeg build cannot draw subtitles. Install a build with libass (with Homebrew on macOS that is ffmpeg-full), or leave the captions out. /postkitty doctor shows this in its Captions row.

Captions are refused because the machine has no font

ffmpeg draws captions with the fonts installed on the machine, and a bare server or a container can have none. Install one (on Ubuntu, for example, sudo apt install fonts-dejavu-core) and ask again. If the message lists characters, no installed font has them: install a font for that script or take them out of the subtitle file. An emoji that no font has is left out, and the result says so. /postkitty doctor shows a missing font in its Captions row.

A target shows failed

The row says which limit the file broke and that the repair did not fix it. Ask for another layout for that target; pad keeps the whole picture and fits most cases. The other targets keep their passing files.

The wrong person is in the frame

Say who you mean by position: “the person on the left”, “the second person from the left”. See Focus.

A text is over the limit

Hermes shortens it and counts again. On X, Threads and Bluesky you can ask for a thread instead.

An animated GIF, WebP or PNG is refused

postkitty makes still photos and video. Give it one frame as a JPEG or PNG, or give it the clip as a video file.

You changed a file and the result still shows the old one

Ask for a new run with the changed file. postkitty then scans it again and starts a new job.