MoneyPrinterV2 is a menu-driven Python pipeline that automates short-form content creation and publishing across YouTube Shorts and X (Twitter), with built-in workflows for affiliate marketing pitches and cold-email outreach. The app glues together a local Ollama LLM for scripts and titles, the Nano Banana 2 (Gemini image) API for visuals, the local KittenTTS engine for voiceover, MoviePy for video assembly, and Firefox/Selenium for posting — optionally cross-posting finished Shorts to TikTok and Instagram via the Post Bridge API.
1. Quick Start
The fastest way to run MoneyPrinterV2 is to clone the repo, install the Python dependencies, copy the example configuration, and launch the menu. The app drives everything else interactively from there.
git clone https://github.com/FujiwaraChoki/MoneyPrinterV2.git
cd MoneyPrinterV2
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp config.example.json config.json
python src/main.py
============ OPTIONS ============
1. YouTube Shorts Automation
2. Twitter Bot
3. Affiliate Marketing
4. Outreach
5. Quit
=================================
Select an option:
Tip
On macOS or Linux, run
bash scripts/setup_local.shinstead of the manual steps above. The helper creates the virtualenv, installs requirements, copiesconfig.example.jsontoconfig.json, auto-detects ImageMagick and your Firefox profile, picks an installed Ollama model, and runs the local preflight check before exiting.
What you need before the first run
- [ ] Python 3.10+ with
pipandvenv - [ ] Firefox installed, with profiles already logged into the YouTube and X accounts you intend to drive
- [ ] Ollama running locally with at least one model pulled (
ollama pull llama3.2:3b) - [ ] ImageMagick (
magickon Windows,converton Linux/macOS) for the MoviePy text caption renderer - [ ] Optional: a Nano Banana 2 / Gemini API key for image generation, an AssemblyAI key if you choose cloud subtitles, and a Post Bridge API key for TikTok/Instagram cross-posting
Your first short — minimal walkthrough
- Pick option 1. YouTube Shorts Automation at the main menu.
- When prompted "No accounts found in cache. Create one now?", answer
Yes. - Provide a nickname, the absolute path to a Firefox profile that is logged into YouTube Studio, the channel niche, and a language.
- Back at the YouTube submenu, choose 1. Upload Short to generate a topic, script, image prompts, images, voiceover, subtitles, and the final 1080x1920 MP4.
- When asked "Do you want to upload this video to YouTube?", answer
Yesto drive Firefox to YouTube Studio and walk through the upload flow automatically.
Note
The first run downloads a background music archive into a
Songs/directory and creates a hidden.mp/working folder beside the project root. Both are normal and required.
2. Installation
System requirements
| Component | Minimum | Notes |
|---|---|---|
| Python | 3.10+ | The project uses modern type-hint syntax (str | None) that requires 3.10. |
| OS | macOS, Linux, or Windows 10+ | Selenium needs a desktop session unless headless is enabled. |
| RAM | 8 GB | 16 GB recommended if you run local Whisper subtitles or large Ollama models. |
| Disk | 5 GB free | Ollama models, the songs archive, and intermediate .mp/ artifacts add up quickly. |
| Browser | Firefox latest | Posting/uploading is automated through selenium-firefox against a real Firefox profile. |
| Ollama | Latest | Required for script, title, and pitch generation. Default base URL is http://127.0.0.1:11434. |
Method 1 — Helper script (recommended on macOS/Linux)
git clone https://github.com/FujiwaraChoki/MoneyPrinterV2.git
cd MoneyPrinterV2
bash scripts/setup_local.sh
The script will:
- Copy
config.example.jsontoconfig.jsonif it does not exist. - Create a
venv/virtual environment and install everything inrequirements.txt. - Detect ImageMagick (
magickorconvert) and write its path intoconfig.json. - Find a default-release Firefox profile under
~/Library/Application Support/Firefox/Profilesand seedfirefox_profile. - Query the Ollama HTTP API (
/api/tags), prefer a known-good model from a curated list, and writeollama_modelintoconfig.json. - Run
scripts/preflight_local.pyto flag missing dependencies before you try to launch the app.
Important
The setup script writes plaintext API-style values into
config.json. Addconfig.jsonto your local.gitignoreif you fork this repo — the file is not ignored by default.
Method 2 — Manual install (all platforms)
git clone https://github.com/FujiwaraChoki/MoneyPrinterV2.git
cd MoneyPrinterV2
python3 -m venv venv
# macOS / Linux
source venv/bin/activate
# Windows (PowerShell)
.\venv\Scripts\Activate.ps1
pip install --upgrade pip setuptools wheel
pip install -r requirements.txt
cp config.example.json config.json # Windows: copy config.example.json config.json
Successfully installed termcolor schedule requests kittentts soundfile prettytable
webdriver_manager selenium_firefox selenium ollama moviepy Pillow yagmail assemblyai
faster-whisper srt_equalizer undetected_chromedriver platformdirs ...
The requirements.txt shipped with the repo pins every dependency the
program touches, including the kittentts wheel pulled directly from
the KittenTTS GitHub release URL.
Method 3 — From source on Windows (no helper script)
- Install Python 3.10+ from python.org and tick "Add to PATH".
- Install Firefox and sign into the YouTube and X accounts you plan to drive.
- Install ImageMagick and note the absolute path to
magick.exe. - Install Ollama and pull a model:
ollama pull llama3.2:3b. - Open PowerShell, clone the repo, create the virtualenv, and
pip install -r requirements.txtas above. - Copy
config.example.jsontoconfig.jsonand fill inimagemagick_path,firefox_profile, and anollama_modelyou actually have pulled.
Warning
Selenium's
webdriver_managerwill download a matchinggeckodriverbinary on first launch. Allow it through any endpoint-protection or antivirus prompt — the binary is fetched at runtime, not bundled.
Verify the install
source venv/bin/activate
python src/main.py
============ OPTIONS ============
1. YouTube Shorts Automation
2. Twitter Bot
3. Affiliate Marketing
4. Outreach
5. Quit
=================================
If you see the ASCII banner followed by the options menu and an Ollama
model selector, the install is healthy. If Could not connect to Ollama
appears instead, start Ollama (ollama serve if it is not already
running as a service) and re-launch the app.
3. Configuration
Every runtime knob lives in a single file at the repo root: config.json,
created from config.example.json. The application reads it on demand —
edit, save, restart the relevant flow.
Annotated example
{
"verbose": true,
"firefox_profile": "/Users/me/Library/Application Support/Firefox/Profiles/abc.default-release",
"headless": false,
"ollama_base_url": "http://127.0.0.1:11434",
"ollama_model": "llama3.2:3b",
"twitter_language": "English",
"nanobanana2_api_base_url": "https://generativelanguage.googleapis.com/v1beta",
"nanobanana2_api_key": "",
"nanobanana2_model": "gemini-3.1-flash-image-preview",
"nanobanana2_aspect_ratio": "9:16",
"threads": 2,
"zip_url": "",
"is_for_kids": false,
"google_maps_scraper": "https://github.com/gosom/google-maps-scraper/archive/refs/tags/v0.9.7.zip",
"email": {
"smtp_server": "smtp.gmail.com",
"smtp_port": 587,
"username": "",
"password": ""
},
"google_maps_scraper_niche": "",
"scraper_timeout": 300,
"outreach_message_subject": "I have a question...",
"outreach_message_body_file": "outreach_message.html",
"stt_provider": "local_whisper",
"whisper_model": "base",
"whisper_device": "auto",
"whisper_compute_type": "int8",
"assembly_ai_api_key": "",
"tts_voice": "Jasper",
"font": "bold_font.ttf",
"imagemagick_path": "/usr/bin/convert",
"script_sentence_length": 4,
"post_bridge": {
"enabled": false,
"api_key": "",
"platforms": ["tiktok", "instagram"],
"account_ids": [],
"auto_crosspost": false
}
}
What you typically have to change
| Key | Why you change it |
|---|---|
firefox_profile |
Must point at a Firefox profile already signed into the target YouTube or X account. |
ollama_model |
Must be the exact name of a model you have pulled with ollama pull <name>. |
imagemagick_path |
Absolute path to magick.exe (Windows) or convert (Linux/macOS). MoviePy needs it for text overlays. |
nanobanana2_api_key |
Gemini API key for image generation. Falls back to the GEMINI_API_KEY environment variable. |
tts_voice |
KittenTTS voice ID. The default Jasper is shipped by KittenTTS. |
stt_provider |
local_whisper (default, free) or third_party_assemblyai (cloud, faster). |
assembly_ai_api_key |
Required only when stt_provider is third_party_assemblyai. |
email |
SMTP credentials used by the Outreach flow's yagmail client. |
post_bridge |
Cross-post the YouTube Short to TikTok and Instagram. See the recipes below. |
Important
Treat
config.jsonas a secret. It can hold a Gemini API key, an AssemblyAI key, an SMTP password, and a Post Bridge API key. Never commit it, share it, or paste it into a bug report.
Note
For an exhaustive description of every key — defaults, fallbacks, and what reads it — see the Configuration Reference in the MoneyPrinterV2 Administrator and Developer Guide.
Environment variables that override config
| Variable | Used by | Effect |
|---|---|---|
GEMINI_API_KEY |
get_nanobanana2_api_key() |
Used when nanobanana2_api_key is blank in config.json. |
POST_BRIDGE_API_KEY |
get_post_bridge_config() |
Used when post_bridge.api_key is blank. |
4. Core Usage
The app is fully menu-driven. Launching python src/main.py always
prints an ASCII banner, ensures the .mp/ working folder exists,
fetches the background music archive (if Songs/ is empty), and asks
you to choose an Ollama model — either by reading ollama_model from
config or by listing every pulled model interactively.
The main menu
============ OPTIONS ============
1. YouTube Shorts Automation
2. Twitter Bot
3. Affiliate Marketing
4. Outreach
5. Quit
=================================
Each option is its own sub-pipeline, gated behind an account-cache
flow: pick from saved accounts, create a new one, or type d to
delete one.
YouTube Shorts Automation (option 1)
Manages a per-channel cache (UUID, nickname, Firefox profile path, niche, language). Once an account is selected the YouTube submenu appears:
============ OPTIONS ============
1. Upload Short
2. Show all Shorts
3. Setup CRON Job
4. Quit
=================================
| Option | What it does |
|---|---|
| 1. Upload Short | Generates a topic, script, metadata, image prompts, images, TTS audio, subtitles, and a final 1080x1920 MP4, then asks whether to upload it. |
| 2. Show all Shorts | Lists every video previously generated for the selected account from the local YouTube cache. |
| 3. Setup CRON Job | Schedules src/cron.py runs (once or twice daily) using the schedule library. |
| 4. Quit | Returns to the main menu. |
The end-to-end pipeline behind option 1 is:
- Generate topic — Ollama produces a one-sentence niche idea.
- Generate script — Ollama writes a script of
script_sentence_lengthshort sentences inlanguage. - Generate metadata — Ollama writes a YouTube title (capped at 100 characters, retried if too long) and description.
- Generate image prompts — One JSON-array of prompts, sized to roughly
len(script) / 3. - Generate images — Each prompt is sent to Nano Banana 2 (Gemini image API), and each PNG is persisted to
.mp/. - TTS — KittenTTS synthesizes the script with the configured voice (default
Jasper), at 24 kHz. - Subtitles — Either local Whisper (
faster-whisper) or AssemblyAI, depending onstt_provider. The SRT is equalized to a 10-character cap so subtitle lines are readable. - Combine — MoviePy concatenates ImageClips to fill the audio duration, crops/resizes everything to 1080x1920, lays a yellow stroked text caption, mixes the TTS with a randomly chosen background song at 10% volume, and writes the MP4.
- Upload — If you say yes, Selenium drives
https://www.youtube.com/upload, fills the title/description, sets "made for kids", clicks Next three times, switches the privacy radio to Unlisted, and presses Done. The final video URL is recorded in the YouTube cache.
Note
Generated Shorts are uploaded as Unlisted. You must promote them to Public manually in YouTube Studio. This is by design — it gives you a chance to review the AI-generated content before publishing.
Twitter Bot (option 2)
Manages a per-account Twitter cache (UUID, nickname, Firefox profile, topic). The submenu:
============ OPTIONS ============
1. Post something
2. Show all Posts
3. Setup CRON Job
4. Quit
=================================
| Option | What it does |
|---|---|
| 1. Post something | Generates a 2-sentence post about topic in twitter_language (capped at 260 characters), navigates to https://x.com/compose/post in Firefox, types the text, and clicks Post. |
| 2. Show all Posts | Prints a table of every post previously made for this account from the local Twitter cache. |
| 3. Setup CRON Job | Schedules src/cron.py twitter <uuid> runs once, twice, or three times a day. |
| 4. Quit | Returns to the main menu. |
The post composer tries three CSS/XPath selectors for the textarea and three for the Post button, so it survives small DOM tweaks on X.
Affiliate Marketing (option 3)
Pairs an affiliate link to one of your saved Twitter accounts. On run
it spins up a Firefox session using that account's profile, opens the
affiliate URL (Amazon-style page), scrapes the productTitle and the
feature-bullets element, asks Ollama to write a brief pitch, appends
"You can buy the product here: <link>", and shares the pitch by
calling the Twitter posting flow on your behalf.
Warning
The scraper expects an Amazon product page layout (
productTitleandfeature-bulletselement IDs). Other affiliate networks will not populate the title/features and the generated pitch will be useless.
Outreach (option 4)
Reaches out to local businesses by email. The flow:
- Confirms
gois installed. - Downloads and unzips
google_maps_scraper(default v0.9.7) if not already present. - Builds the Go binary (
google-maps-scraper.exeon Windows,google-maps-scraperelsewhere). - Writes
google_maps_scraper_nichetoniche.txt. - Runs the scraper with
-input niche.txt -results <results path>forscraper_timeoutseconds (default 300). - Reads the results CSV, follows each business's website, regex-greps the first email address out of the page HTML, and appends it to the row.
- For every row that ended up with a valid email, sends a message via
yagmailusing your configured SMTP credentials, replacing{{COMPANY_NAME}}placeholders in the subject and body file (outreach_message.htmlby default).
Caution
Mass cold email is regulated. Check CAN-SPAM (US), CASL (Canada), GDPR (EU), and your local laws before running the Outreach flow. You are the sender of every message; you are responsible for unsubscribe handling, accuracy, and consent.
5. Common Tasks
Generate a single YouTube Short and review it before uploading
- Launch the app:
python src/main.py. - Pick 1. YouTube Shorts Automation at the main menu.
- Select an existing YouTube account (or create one with your channel's Firefox profile, niche, and language).
- From the YouTube submenu, pick 1. Upload Short.
- Wait for the topic/script/metadata/images/audio/subtitle/combine pipeline to finish — the MP4 is written into
.mp/with a UUID name, and its absolute path is logged. - When asked "Do you want to upload this video to YouTube? (Yes/No):", answer
No. - Open the MP4 from
.mp/to review. If you like it, drag-drop into YouTube Studio yourself.
Batch-generate from a niche prompt list with CRON
- Confirm the niche and language baked into the saved YouTube account match the content you want to mass-produce. Each run picks a new one-sentence topic from that niche, so variety comes from the LLM, not from a prompt list.
- From the YouTube submenu, pick 3. Setup CRON Job and choose a frequency (Once a day or Twice a day at 10:00 and 16:00 local time).
- The app stays in the foreground. Leave the terminal running — the in-process
scheduleloop firespython src/cron.py youtube <uuid> <model>at each scheduled time. - To stop the schedule, choose option 4. Quit from the YouTube submenu or
Ctrl+Cthe process.
Tip
On servers and unattended workstations, prefer launching
python src/cron.py youtube <account_uuid> <ollama_model>directly from your system scheduler (cron, systemd timers, Windows Task Scheduler). That sidesteps the in-process loop and survives reboots.
Change the voiceover voice via TTS settings
- Open
config.json. - Edit the
tts_voicekey — the default is"Jasper". Pick another voice ID supported by KittenTTS (KittenML/kitten-tts-mini-0.8). - Save the file. The next call to
TTS().synthesize(...)will read the new voice.
{
"tts_voice": "Luna"
}
Swap subtitle generation between local Whisper and AssemblyAI
Local Whisper is free and offline; AssemblyAI is faster but requires a paid API key.
{
"stt_provider": "third_party_assemblyai",
"assembly_ai_api_key": "your-assemblyai-key"
}
To go back to local:
{
"stt_provider": "local_whisper",
"whisper_model": "base",
"whisper_device": "auto",
"whisper_compute_type": "int8"
}
Note
If you select local Whisper but
faster-whisperis missing, the app raises a clear error pointing you to install the package or switch providers. The same is not true for AssemblyAI — set the API key before switching providers.
Customize the visual style (font, captions, aspect ratio)
| Knob | File | Effect |
|---|---|---|
font |
config.json |
Filename of a TTF inside fonts/. Default is bold_font.ttf. |
nanobanana2_aspect_ratio |
config.json |
Image aspect ratio. Default 9:16 matches Shorts; pick 1:1 or 16:9 for other formats. |
nanobanana2_model |
config.json |
Gemini image model name. Default gemini-3.1-flash-image-preview. |
script_sentence_length |
config.json |
Number of short sentences in the LLM-generated script. Default 4. Lower = punchier shorts. |
| Caption color/stroke | src/classes/YouTube.py |
The caption is hardcoded yellow with a black stroke (color="#FFFF00", stroke_color="black"). Edit the combine() method to change. |
Schedule auto-posting to Twitter three times a day
- From the main menu choose 2. Twitter Bot.
- Select the Twitter account you set up.
- From the Twitter submenu choose 3. Setup CRON Job, then option 3. Thrice a day (08:00, 12:00, 18:00 local time).
- Leave the terminal running. The
scheduleloop will spawnpython src/cron.py twitter <uuid> <model>at each window.
Warning
Triple-daily automation pushes against X's automation rules. Use a dedicated account, vary
topicbetween accounts, and watch for rate-limit prompts in the Firefox session. Rotating posting times (rather than fixed clock slots) reduces footprint.
Cross-post a finished Short to TikTok and Instagram via Post Bridge
-
Sign up for Post Bridge and connect your TikTok and Instagram accounts there.
-
Edit
config.json:{ "post_bridge": { "enabled": true, "api_key": "your-post-bridge-key", "platforms": ["tiktok", "instagram"], "account_ids": [], "auto_crosspost": false } } -
Generate a Short the normal way through option 1. After the YouTube upload succeeds the app will ask:
Cross-post this video to tiktok, instagram via Post Bridge? (Yes/No):— answer Yes. -
The first time, the app lists your connected Post Bridge accounts per platform. Pick one for each platform; the app then suggests the IDs you can paste back into
account_idsto skip the prompt next time.
{
"post_bridge": {
"account_ids": [12345, 67890]
}
}
Tip
Set
auto_crossposttotrueonce you have populatedaccount_ids. The CRON path requires this — it never prompts, so the cross-post is skipped silently ifauto_crosspostisfalse.
Run unattended on a server (headless mode)
-
Install Firefox, Xvfb (Linux) or use a Windows server with a desktop session.
-
In
config.json, set"headless": true. Selenium will launch Firefox with--headless. -
Pre-create the Firefox profile while you can see a screen: open Firefox under that profile and sign into both YouTube and X manually. The app cannot solve a CAPTCHA or 2FA prompt for you.
-
Install Ollama and pull the model named in
ollama_model. -
Launch with a system scheduler:
/path/to/MoneyPrinterV2/venv/bin/python /path/to/MoneyPrinterV2/src/cron.py youtube <account_uuid> <ollama_model>
Caution
Headless mode does not bypass logged-out states, X's automation challenges, or YouTube's "verify you are human" prompts. If a session goes stale you will see silent failures (issues #232, #229) — open Firefox manually under the same profile, log back in, and re-run.
Debug a failed YouTube upload
- Re-run with
verbose: trueinconfig.json. The app prints every step ("Setting title...", "Setting description...", "Clicking next..."). - Verify the Firefox profile is still signed into YouTube Studio. Stale sessions are the most common cause of upload failures.
- The upload is wrapped in
try/exceptand silently returnsFalseon any error (the bareexceptclause inupload_video()). If the cross-post step is skipped with "YouTube upload failed", that is the signal. - Inspect
.mp/— the rendered MP4 is still there. You can drag-drop it into YouTube Studio to publish manually while you investigate. - Check open issue #218 — Firefox profile path validation is the most common stumbling block on first runs.
6. Troubleshooting
Error: Firefox profile path does not exist or is not a directory: ...
Cause: The Twitter, YouTube, or AffiliateMarketing class checked os.path.isdir(fp_profile_path) before launching the browser and found nothing.
Solution:
- Open Firefox, type
about:profilesin the URL bar, and copy the absolute "Root Directory" of the profile you want to drive. - Paste it into the cached account record (delete the account from the menu and recreate it) or into
config.json'sfirefox_profileif you are using a global profile. - Confirm the path exists and is a directory, not a file.
Prevention: Run bash scripts/setup_local.sh on macOS — it auto-detects a *default-release* profile under ~/Library/Application Support/Firefox/Profiles.
Tracked in issue #218.
Error: Affiliate link is invalid. Expected a full URL, got: ...
Cause: The Affiliate Marketing flow validated the link with urlparse and either the scheme is missing (amazon.com/...) or it is not http/https.
Solution: Always paste the full URL including https://.
Prevention: Copy the link from your affiliate dashboard rather than the address bar.
Error: Could not connect to Ollama: ...
Cause: The app called ollama.Client(host=ollama_base_url).list() at startup and the daemon was unreachable.
Solution:
- Confirm Ollama is installed and the daemon is running. On macOS/Linux start it with
ollama serve. On Windows the installer registers a background service. - Confirm
ollama_base_urlinconfig.jsonmatches (default:http://127.0.0.1:11434). - From a terminal:
curl http://127.0.0.1:11434/api/tags— you should get a JSON list of installed models.
Prevention: Use the helper script (bash scripts/setup_local.sh); it queries the same endpoint and only writes a model name into config when it finds one installed.
Error: No models found on Ollama. Pull a model first ...
Cause: Ollama is reachable but no models are installed.
Solution: ollama pull llama3.2:3b (or any other model that fits your RAM). Re-launch.
Prevention: Add the model name to ollama_model in config.json so the app uses it directly without falling back to interactive selection.
Error: Could not find tweet text box. Ensure you are logged into X in this Firefox profile.
Cause: The Twitter class navigated to https://x.com/compose/post and could not match any of three textarea selectors.
Solution:
- Open the Firefox profile manually and sign into X with the account you intend to drive.
- Resolve any "verify you are human" or 2FA prompt that appears.
- Re-run the Twitter posting flow.
Prevention: Schedule a weekly manual login on each automation profile to keep the session warm.
Error: Could not find the Post button on X compose screen.
Cause: Selenium typed the post body but failed to locate the Post button via three fallback selectors.
Solution: Same as above — refresh the X session manually. If X redesigned the compose screen, watch the issues tab on GitHub for an update.
Error: Unsupported provider 'X'. Expected 'twitter' or 'youtube'.
Cause: The cache.get_provider_cache_path helper rejected an unknown provider. This is an internal error path and should never appear in normal use.
Solution: Ensure you are running the app from the project root and that nothing has shadowed src/cache.py. If you see this in cron.py, the first CLI argument is wrong — pass twitter or youtube exactly.
Error: Empty input is not allowed.
Cause: You hit Enter at a menu prompt without typing anything.
Solution: Type the option number and press Enter. The app catches this ValueError and re-prompts.
Error: Could not download a valid songs archive from any configured URL
Cause: Startup tried to fetch the background music archive from zip_url and failed (empty URL or network error).
Solution:
- Set
zip_urlinconfig.jsonto a public URL of a.zipcontaining one or more.mp3/.wav/.m4a/.aac/.ogg/.flacfiles. - Re-launch. The app extracts only safe paths (no
..or absolute paths inside the archive) intoSongs/and deletes the zip.
Prevention: Once Songs/ contains at least one audio file, fetch is skipped — pre-seed the directory with your own background tracks.
Error: No audio files found in Songs directory
Cause: combine() tried to pick a random song but Songs/ is empty.
Solution: Drop one or more .mp3 files into Songs/ and re-run the YouTube generation.
Error: Could not locate extracted google-maps-scraper directory.
Cause: The Outreach flow could not find an unzipped google-maps-scraper-* directory containing a go.mod.
Solution:
- Manually download the zip pointed to by
google_maps_scraperinconfig.json. - Unzip it in the project root.
- Re-run Outreach.
Prevention: Check open issue #256 — stale scraper_results.csv from a previous failed run can also confuse the flow.
Error: Expected built scraper binary at: ...
Cause: go build succeeded but google-maps-scraper (or .exe on Windows) was not produced.
Solution:
- Confirm
go versionworks on your PATH (go installmust be functional, not just present). - Re-run Outreach. The build is idempotent — it skips if the binary already exists.
Error: Post Bridge is enabled but no API key is configured. ...
Cause: post_bridge.enabled is true but neither post_bridge.api_key nor the POST_BRIDGE_API_KEY environment variable is set.
Solution: Either disable Post Bridge or set the API key in one of those two locations.
Error: Post Bridge cross-post failed: Post Bridge API returned HTTP 4XX: ...
Cause: The Post Bridge API rejected the request. Common reasons: invalid API key, expired token, removed social account, video too long, or rate limit.
Solution: Read the message — Post Bridge returns a structured error. Verify the API key, that the social account is still connected on the Post Bridge dashboard, and that the file is supported.
Prevention: The client retries on 429/500/502/503/504 automatically up to max_retries=3. If you keep hitting these, your Post Bridge plan may be over its quota.
7. FAQ
Q: Does this need a GPU?
Only if you want fast local Whisper transcription. Everything else runs
on CPU. Local Whisper with whisper_device: "auto" and
whisper_compute_type: "int8" is reasonable on a modern CPU; switch to
AssemblyAI if it is too slow.
Q: Where do generated videos and intermediate files live?
Generated MP4s, WAV TTS files, SRT subtitles, and PNG images are all
written into a hidden .mp/ directory at the repo root. It is created
on first run by assert_folder_structure(). The folder is cleared of
non-.json files at the start of each YouTube generation run via
rem_temp_files().
Q: Can I keep the JSON caches across machines?
Yes — the per-provider caches live at .mp/twitter.json,
.mp/youtube.json, and .mp/afm.json. Copy .mp/ between machines and
the same accounts/products will be available, but firefox_profile
paths inside the cache are absolute. Update them, or delete and recreate
the affected accounts.
Q: How are my passwords and API keys stored?
In plaintext in config.json and in JSON caches under .mp/. There is
no encryption layer. Keep config.json out of source control and treat
the repo directory as sensitive.
Q: Does the app save anything to a remote server?
Generated content is sent to Ollama (local), the Nano Banana 2 / Gemini endpoint (Google), AssemblyAI (only if you opt in), Post Bridge (only if you opt in), and SMTP (only if you run Outreach). The repo itself hosts no remote services.
Q: Does the YouTube uploader publish my Shorts publicly?
No — every uploaded Short is set to Unlisted (radio button index 2 in the privacy step). Promote to Public manually in YouTube Studio when you are happy with the result.
Q: How do I delete an account from the cache?
At the YouTube or Twitter account list, type d instead of an account
number. The app will ask for the account number to delete and confirm
before removing it.
Q: Why is the script always exactly four sentences?
Because script_sentence_length defaults to 4. Bump it in config.json
for longer Shorts; the LLM is instructed not to exceed the configured
sentence count.
Q: How do I change the language of generated content?
For YouTube Shorts, set language when creating the YouTube account in
the menu (e.g. "Spanish", "German"). For Twitter posts, set
twitter_language in config.json. The LLM is told to honour these.
Q: Can I use OpenAI / Claude / Gemini instead of Ollama for text?
Not in the shipped code — src/llm_provider.py is hardcoded to call
the Ollama HTTP API. There is an open thread (issue #215) discussing
adding Gemini as an LLM backend, but as of the current commit the only
text generator is Ollama.
Q: What happens if I cancel during a video generation?
The half-finished MP4 stays in .mp/. The next YouTube run begins with
rem_temp_files(), which deletes everything in .mp/ except .json
files (the caches). So caches are preserved, intermediates are wiped.
Q: Why does the Affiliate Marketing flow only work on Amazon?
Because the scraper hardcodes the productTitle and feature-bullets
element IDs Amazon uses. Other affiliate networks use different
markup.
Q: How do I unsubscribe a recipient from outreach emails?
The shipped Outreach flow does not include unsubscribe handling — it is
your responsibility to add an Unsubscribe link to the body file at
outreach_message_body_file (default outreach_message.html) and to
honour replies. See the CAUTION in section 4.
Q: Where do I get help?
File issues at github.com/FujiwaraChoki/MoneyPrinterV2/issues. Real recent bugs (e.g. #228, #230, #231, #232, #256) are tracked there.