End-to-end automation for MoneyPrinterTurbo: generate short-video ideas, render them, tag them, and upload them to YouTube — one repository, one command, one config file.
mpt runThat chains four stages, once per language:
refill → batch → enrich → upload
| Stage | Command | What it does |
|---|---|---|
| pilot | mpt refill |
asks an LLM for fresh video ideas and appends them to the jobs queue, deduplicated against everything you've already made |
| batch | mpt batch |
renders pending jobs through the MoneyPrinterTurbo API, with retries, timeout handling, and cache cleanup |
| enricher | mpt enrich |
generates platform-appropriate hashtags for each rendered video and writes them into its metadata sidecar |
| uploader | mpt upload |
uploads to YouTube via youtubeuploader, multi-account, with a crash-safe SQLite ledger |
Every stage also runs on its own, so you can adopt as much or as little of the chain as you want.
Coming from the four separate tools? This repository merges shorts-pilot, mpt-batch, hashtag-enricher, and yt-shorts-uploader.
- Python 3.10+ and uv
- A running MoneyPrinterTurbo instance (for the batch stage)
- An OpenAI-compatible LLM endpoint — OpenAI, Anthropic, Groq, Together, or a local Ollama / vLLM (for refill and enrich)
- youtubeuploader and a Google Cloud OAuth client (for the upload stage)
You only need the pieces for the stages you actually use.
git clone https://github.com/korosu/mpt-autopilot.git
cd mpt-autopilot
uv sync
cp config.example.yaml config.yaml
cp .env.example .env
cp jobs/jobs.example.yaml jobs.yaml
cp accounts.example.yaml accounts.yaml # only for the upload stageCheck the wiring before anything runs for real:
uv run mpt run --dry-runThat loads the config, resolves every language, checks each uploader account, and prints what each stage would do — without rendering a video, calling an LLM, or uploading anything.
One config.yaml, one section per stage, plus a shared part every stage reads:
telegram_prefix: "mpt-autopilot"
paths:
jobs_dir: "./jobs"
exports_dir: "./exports"
langs: # shared: one source of truth for every stage
en:
label: English
file_suffix: "" # jobs.yaml, seen.txt, exports/
es:
label: Spanish
file_suffix: "_es" # jobs_es.yaml, seen_es.txt, exports_es/
pilot:
generation:
count: 21
threshold: 10
batch:
api_url: "http://127.0.0.1:8080"
mpt_storage: "/root/MoneyPrinterTurbo/storage"
mpt_songs_dir: "/root/MoneyPrinterTurbo/resource/songs"
enricher:
platform: youtube
max_tags: 5
uploader:
uploader_binary: "/root/youtubeuploader/youtubeuploader"
pipeline:
accounts:
es: spanish-channel # only if your account isn't named after the languageconfig.example.yaml is the annotated version, with every
key and its default. Two things about it are worth knowing up front:
- Every relative path resolves against
config.yaml's own directory, never your current one. So cron can runmpt --config /srv/mpt/config.yaml runfrom anywhere and get identical behaviour. langs:is shared, at the top level. A language'sfile_suffixderives the jobs file, the seen registry, and the exports directory, which is exactly how the stages hand work to each other with no extra configuration.
Secrets — LLM keys and the optional Telegram bot token — go in .env, never in
config.yaml. mpt looks for .env next to config.yaml first, then in the
current directory.
Per stage:
mpt refill --lang en # generate ideas via LLM
mpt refill --lang en --topic "Why the Moon has no atmosphere" # skip the LLM
mpt init-seen --dir /videos/en # register existing videos so refill won't repeat them
mpt batch --lang en # render pending jobs
mpt batch --dry-run # preview which jobs would run
mpt batch --status # seen-registry stats
mpt batch --list-voices es # browse the 314 bundled Edge TTS voices
mpt enrich --dir ./exports_en # generate hashtags
mpt enrich --file clip.mp4 --force # re-generate for one file
mpt enrich --platform tiktok # different tag limits and prompt
mpt upload --account en # upload one channel
mpt upload --all-accounts --limit 5 # every channel, 5 videos eachThe whole chain:
mpt run # every configured language
mpt run --lang en # one language
mpt run --only batch --only enrich # a slice of the pipeline
mpt run --skip upload # produce everything, upload later
mpt run --continue-on-error # don't stop a language on the first failure--config works before or after the subcommand. Every subcommand has its own
--help.
uv sync --extra web
mpt web # http://127.0.0.1:8765One page with a stage button per command in a left sidebar and every flag that stage's parser registers, generated by introspecting the parsers themselves. Logs stream in live, one run at a time, with a Cancel button. Single user on localhost, no authentication — see docs/web.md and Web interface below.
| Code | Meaning |
|---|---|
| 0 | succeeded, or nothing to do |
| 1 | failure |
| 2 | stopped on YouTube's daily upload quota |
A quota stop gets its own code so a scheduled wrapper can tell "try again
tomorrow" apart from "something is broken". mpt run aggregates across languages
with failure taking precedence over a quota stop.
0 */6 * * * cd /srv/mpt && /usr/local/bin/mpt --config /srv/mpt/config.yaml run >> /srv/mpt/logs/run.log 2>&1Optional Telegram alerts report each run's outcome — set TELEGRAM_TOKEN and
TELEGRAM_CHAT_ID in .env, or leave them empty to disable notifications.
| docs/pilot.md | idea generation, themes, dedup, mpt refill / mpt init-seen |
| docs/batch.md | rendering, jobs.yaml, voices, retries, mpt batch |
| docs/enricher.md | hashtag generation, prompts, platform limits, mpt enrich |
| docs/uploader.md | accounts, sidecars, the ledger, mpt upload |
| docs/pipeline.md | mpt run — stage selection, failure handling, scheduling |
| docs/web.md | mpt web — the local browser interface |
mpt web puts every stage behind one local page — no command line, no
remembering flags. Pick a stage in the sidebar; the page lists every flag that
stage's parser registers, generated by introspecting the parser itself, so the
form can never drift from the CLI. Press Run and the log streams in live,
with a Cancel button.
What is in the screenshot, left to right:
- Sidebar — one button per stage:
refill,init-seen,batch,enrich,upload,run,validate. The highlighted one is open. - Fill from config / Reset — the pair in the header writes into every
stage's fields the values your
config.yamlimplies, and clears them again. Press it once, then click through the tabs: each one is already filled. The second pair, inside the form, does the same for the open stage only. - Pipeline map —
refill → batch → enrich → upload, doubling as navigation. - Header dots — the current run's state (idle, running with elapsed time, how the last run ended) and the MoneyPrinterTurbo API: green means its task list answers (the endpoint batch really calls), amber means something else owns that port, red means nothing is listening.
- Run / Cancel / preview — the preview line shows the exact command that will run before you start it.
- Settings — the header link opens a page about the MoneyPrinterTurbo side:
the exact version behind the port (
/openapi.json), whether a local process owns it, and a setup wizard — pick how MPT runs (a local systemd service, Docker, or tmux) and name it, and the page derives the restart and logs commands from the answer (saved in its ownweb-settings.yaml; hand-writtenweb.mpt_restart_command/web.mpt_logs_commandkeys still win per command). Buttons to restart MPT and to stream its logs live; for tmux the "stream" is acapture-panepoll showing only the new lines. Cancel is also cleaner now: it signals the run first on POSIX and sweeps the dead child's lock markers on Windows, so a cancelled batch never leaves aseen.txtlock behind to block the next one.
A run started in a terminal is listed under the form too, read-only — stage,
PID, start time, and the tail of that stage's log file when it provably uses
this page's config.yaml. While it lives, the page's Run button is disabled
(a line in the log pane says why) and the server refuses to start a second
run over it — the two would fight over the same jobs files and ledgers.
Screenshots live in assets/. To add or replace one, see
assets/README.md for what each file must show.
Full details — every control, the fill rules, the API — are in
docs/web.md.
uv sync --extra dev --extra web
uv run ruff check . && uv run ruff format --check . && uv run pyright src && uv run pytest tests/ -vCI runs exactly that on every push and pull request against main (--extra web because the type check covers the Flask layer and the tests cover
mpt web).
The layout mirrors the stages:
src/mpt_autopilot/
cli.py pipeline.py config.py seen.py notify.py lock.py logger.py
pilot/ batch/ enricher/ uploader/
The four stage packages never import each other — everything shared goes through the top-level modules.
Commit history from all four original repositories is preserved, so git blame
and git log --follow reach back into each file's original project:
git log --follow src/mpt_autopilot/pilot/refill.py
