# MoneyPrinterV2 User Guide

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.

```bash
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
```

```text
============ 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](https://ollama.com/) 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`. |

### Method 1 — Helper script (recommended on macOS/Linux)

```bash
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)

```bash
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
```

```text
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](https://www.mozilla.org/firefox/) and sign into the YouTube and X accounts you plan to drive.
3. Install [ImageMagick](https://imagemagick.org/script/download.php) and note the absolute path to `magick.exe`.
4. Install [Ollama](https://ollama.com/) 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

```bash
source venv/bin/activate
python src/main.py
```

```text
============ 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

```json
{
  "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

```text
============ 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:

```text
============ 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:

```text
============ 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.

```json
{
  "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.

```json
{
  "stt_provider": "third_party_assemblyai",
  "assembly_ai_api_key": "your-assemblyai-key"
}
```

To go back to local:

```json
{
  "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](https://api.post-bridge.com/reference) and connect your TikTok and Instagram accounts there.
2. Edit `config.json`:

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

```json
{
  "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:

   ```bash
   /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](https://github.com/FujiwaraChoki/MoneyPrinterV2/issues/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](https://github.com/FujiwaraChoki/MoneyPrinterV2/issues/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:**

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](https://github.com/FujiwaraChoki/MoneyPrinterV2/issues/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](https://github.com/FujiwaraChoki/MoneyPrinterV2/issues/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](https://github.com/FujiwaraChoki/MoneyPrinterV2/issues).
Real recent bugs (e.g. #228, #230, #231, #232, #256) are tracked there.

---

_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. Unofficial; not affiliated with or reviewed by the project._
