AI Influencers
A content engine for one persona. Research through to a finished draft.
- Python
- OpenAI
- Anthropic
- YAML config
Most of the work in one short video isn't judgement. It's fetching. Find a format that's already working. Write to it. Find backgrounds. Lay out the slides. Generate the voice. Grain it, queue it.
I got tired of the fetching.
So this does the fetching and stops where the judgement starts. A persona is four files on disk: who they are, how they look, how they sound, and the niche. After that each stage is one button.
Two routes. The slideshow one leads. It lists Shorts off YouTube's hashtag pages over 100,000 views, I pick one, it samples six frames of that reference, writes five hooks of under ten words, drafts a six slide script and renders the slides at 1080 by 1920 with 2.5% film grain and no metadata. The video route is secondary: script, voiceover, a Kling motion prompt, the clip muxed onto the audio and slowed to 0.75x with pitch correction. What lands is a folder of assets and a calendar entry.
The stages that are still a person
Gemini and Kling are subscriptions I pay for in a browser, not API keys I own. So I made that a provider kind. A provider marked manual calls nothing. It writes the prompt and an instruction file, I generate in the web UI and save the result where it says. Image and video default to that.
Pinterest blocks scraping, so it gets the same treatment: five search queries into a drop folder, and no slideshow until six images turn up.
Where the work actually went
Voice. Almost all of it.
The default backend is fish-speech, running locally, free and private, with one bad habit. It sometimes predicts end of speech a step early and drops the last word of a line. It's stochastic, so the same text is clean on one run and clipped on the next. The adapter measures every render and tries up to three times when one comes in under 0.31 seconds per word, keeping the longest. Long scripts get split into chunks of about 25 words and stitched back with a 15 millisecond fade at each seam so the joins don't click.
There's also a guard for the thirteen inline tags fish understands. Models invent others, and it strips the brackets, not the words, so a hallucinated [cough] reads as "cough" rather than eating the line. Caveat: it's written and tested but not wired into synthesis. Nothing calls it yet.
The tests are here too. 65 of them, on voice and text handling, a third of a second, no server and no GPU. The slideshow and scheduling paths have none.
What it won't do
It won't publish. Scheduling writes a calendar, copies the assets into a draft folder and fires a macOS notification when a slot arrives. Posting through an API leaves a fingerprint that gets accounts throttled, so a person taps post. That one is a decision.
It won't lip-sync. Kling animates the persona image but doesn't match mouth shapes to the audio, so the video route is voiceover over a moving portrait. A lip-sync backend would slot in as another role in the config. I haven't added one. That's a gap.
It won't run unattended. The slideshow route stops twice, for the reference URL and at the Pinterest folder. The video route stops twice more, at Gemini and at Kling. And fish-speech ships under a research licence, so commercial use needs a licence from Fish Audio.
It runs on my laptop, one persona at a time, from a Streamlit panel or the CLI. Three personas exist and one is a test. No package, no release, no remote, nobody else runs it.
Stack: Python for the pipeline and Streamlit for the panel, because the panel is a control surface over local files, not a real web app. ffmpeg does every media operation. Slides render through Node's canvas module, which draws text onto an image better than anything I found in Python. Every provider is a line in a YAML file: DeepSeek by default for writing with Claude and OpenAI beside it, fish-speech or ElevenLabs for voice, and a manual kind for whatever I only have in a browser.