Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

MoneyPrinterV2 User Guide

Generated April 2026 from FujiwaraChoki/MoneyPrinterV2 at b8fd5ce

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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.sh instead of the manual steps above. The helper creates the virtualenv, installs requirements, copies config.example.json to config.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 pip and venv
  • [ ] 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 (magick on Windows, convert on 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

  1. Pick option 1. YouTube Shorts Automation at the main menu.
  2. When prompted "No accounts found in cache. Create one now?", answer Yes.
  3. Provide a nickname, the absolute path to a Firefox profile that is logged into YouTube Studio, the channel niche, and a language.
  4. Back at the YouTube submenu, choose 1. Upload Short to generate a topic, script, image prompts, images, voiceover, subtitles, and the final 1080x1920 MP4.
  5. When asked "Do you want to upload this video to YouTube?", answer Yes to 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.
git clone https://github.com/FujiwaraChoki/MoneyPrinterV2.git
cd MoneyPrinterV2
bash scripts/setup_local.sh

The script will:

  1. Copy config.example.json to config.json if it does not exist.
  2. Create a venv/ virtual environment and install everything in requirements.txt.
  3. Detect ImageMagick (magick or convert) and write its path into config.json.
  4. Find a default-release Firefox profile under ~/Library/Application Support/Firefox/Profiles and seed firefox_profile.
  5. Query the Ollama HTTP API (/api/tags), prefer a known-good model from a curated list, and write ollama_model into config.json.
  6. Run scripts/preflight_local.py to flag missing dependencies before you try to launch the app.

Important

The setup script writes plaintext API-style values into config.json. Add config.json to your local .gitignore if 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)

  1. Install Python 3.10+ from python.org and tick "Add to PATH".
  2. Install Firefox and sign into the YouTube and X accounts you plan to drive.
  3. Install ImageMagick and note the absolute path to magick.exe.
  4. Install Ollama and pull a model: ollama pull llama3.2:3b.
  5. Open PowerShell, clone the repo, create the virtualenv, and pip install -r requirements.txt as above.
  6. Copy config.example.json to config.json and fill in imagemagick_path, firefox_profile, and an ollama_model you actually have pulled.

Warning

Selenium's webdriver_manager will download a matching geckodriver binary 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.json as 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:

  1. Generate topic — Ollama produces a one-sentence niche idea.
  2. Generate script — Ollama writes a script of script_sentence_length short sentences in language.
  3. Generate metadata — Ollama writes a YouTube title (capped at 100 characters, retried if too long) and description.
  4. Generate image prompts — One JSON-array of prompts, sized to roughly len(script) / 3.
  5. Generate images — Each prompt is sent to Nano Banana 2 (Gemini image API), and each PNG is persisted to .mp/.
  6. TTS — KittenTTS synthesizes the script with the configured voice (default Jasper), at 24 kHz.
  7. Subtitles — Either local Whisper (faster-whisper) or AssemblyAI, depending on stt_provider. The SRT is equalized to a 10-character cap so subtitle lines are readable.
  8. 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.
  9. 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 (productTitle and feature-bullets element 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:

  1. Confirms go is installed.
  2. Downloads and unzips google_maps_scraper (default v0.9.7) if not already present.
  3. Builds the Go binary (google-maps-scraper.exe on Windows, google-maps-scraper elsewhere).
  4. Writes google_maps_scraper_niche to niche.txt.
  5. Runs the scraper with -input niche.txt -results <results path> for scraper_timeout seconds (default 300).
  6. 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.
  7. For every row that ended up with a valid email, sends a message via yagmail using your configured SMTP credentials, replacing {{COMPANY_NAME}} placeholders in the subject and body file (outreach_message.html by 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

  1. Launch the app: python src/main.py.
  2. Pick 1. YouTube Shorts Automation at the main menu.
  3. Select an existing YouTube account (or create one with your channel's Firefox profile, niche, and language).
  4. From the YouTube submenu, pick 1. Upload Short.
  5. 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.
  6. When asked "Do you want to upload this video to YouTube? (Yes/No):", answer No.
  7. 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

  1. 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.
  2. 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).
  3. The app stays in the foreground. Leave the terminal running — the in-process schedule loop fires python src/cron.py youtube <uuid> <model> at each scheduled time.
  4. To stop the schedule, choose option 4. Quit from the YouTube submenu or Ctrl+C the 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

  1. Open config.json.
  2. Edit the tts_voice key — the default is "Jasper". Pick another voice ID supported by KittenTTS (KittenML/kitten-tts-mini-0.8).
  3. 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-whisper is 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

  1. From the main menu choose 2. Twitter Bot.
  2. Select the Twitter account you set up.
  3. From the Twitter submenu choose 3. Setup CRON Job, then option 3. Thrice a day (08:00, 12:00, 18:00 local time).
  4. Leave the terminal running. The schedule loop will spawn python src/cron.py twitter <uuid> <model> at each window.

Warning

Triple-daily automation pushes against X's automation rules. Use a dedicated account, vary topic between 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

  1. Sign up for Post Bridge and connect your TikTok and Instagram accounts there.

  2. Edit config.json:

    {
      "post_bridge": {
        "enabled": true,
        "api_key": "your-post-bridge-key",
        "platforms": ["tiktok", "instagram"],
        "account_ids": [],
        "auto_crosspost": false
      }
    }
    
  3. 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.

  4. 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_ids to skip the prompt next time.

{
  "post_bridge": {
    "account_ids": [12345, 67890]
  }
}

Tip

Set auto_crosspost to true once you have populated account_ids. The CRON path requires this — it never prompts, so the cross-post is skipped silently if auto_crosspost is false.

Run unattended on a server (headless mode)

  1. Install Firefox, Xvfb (Linux) or use a Windows server with a desktop session.

  2. In config.json, set "headless": true. Selenium will launch Firefox with --headless.

  3. 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.

  4. Install Ollama and pull the model named in ollama_model.

  5. 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

  1. Re-run with verbose: true in config.json. The app prints every step ("Setting title...", "Setting description...", "Clicking next...").
  2. Verify the Firefox profile is still signed into YouTube Studio. Stale sessions are the most common cause of upload failures.
  3. The upload is wrapped in try/except and silently returns False on any error (the bare except clause in upload_video()). If the cross-post step is skipped with "YouTube upload failed", that is the signal.
  4. Inspect .mp/ — the rendered MP4 is still there. You can drag-drop it into YouTube Studio to publish manually while you investigate.
  5. 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:

  1. Open Firefox, type about:profiles in the URL bar, and copy the absolute "Root Directory" of the profile you want to drive.
  2. Paste it into the cached account record (delete the account from the menu and recreate it) or into config.json's firefox_profile if you are using a global profile.
  3. 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.

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:

  1. 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.
  2. Confirm ollama_base_url in config.json matches (default: http://127.0.0.1:11434).
  3. 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:

  1. Open the Firefox profile manually and sign into X with the account you intend to drive.
  2. Resolve any "verify you are human" or 2FA prompt that appears.
  3. 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:

  1. Set zip_url in config.json to a public URL of a .zip containing one or more .mp3/.wav/.m4a/.aac/.ogg/.flac files.
  2. Re-launch. The app extracts only safe paths (no .. or absolute paths inside the archive) into Songs/ 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:

  1. Manually download the zip pointed to by google_maps_scraper in config.json.
  2. Unzip it in the project root.
  3. 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:

  1. Confirm go version works on your PATH (go install must be functional, not just present).
  2. 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.