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.
hermespluginsinstallpostkitty
Answer yes when Hermes asks about the Python dependencies and about turning the plugin on. This form skips both questions:
hermespluginsinstallpostkitty--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 `hermespluginsinstallpostkitty--yes-deps--enable` in the terminal. When it finishes, tell me to open a new chat so its tools load.
What you need
You need
For
How 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:
/postkittydoctor
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.
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.
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.
Argument
Type
What 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.
Argument
Type
What 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.
Argument
Type
What 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.
Argument
Type
What it is
jobrequired
string
The job id that postkitty_scan returned.
targetrequired
string
The target to render again, for example tiktok or instagram_feed.
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.
Argument
Type
What 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.
Command
What it does
/postkitty
What postkitty does, example requests and every target with its size and main limits.
/postkittytiktok
One platform's targets and limits. Use any platform name.
/postkittydoctor
Checks your setup and gives one line on how to fix each problem.
/postkittycheck<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:
Command
What it does
hermespostkittydoctor
The setup check.
hermespostkittyplatforms[name]
Every target and its limits, or one platform's.
hermespostkittycheckFILE[--targetID]
Checks one file against one target. The exit code is 1 when the file fails.
hermespostkittymake[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.
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.
Value
What 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.
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
Method
Endpoint
What 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.
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.
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.
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.
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.
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.