Skip to main content
MoneyPrinter V2 is designed with modularity in mind. This guide explains the project structure and shows you how to extend functionality.

Project Structure

Understanding the codebase organization is key to customization:
MoneyPrinterV2/
├── src/                    # Core application code
│   ├── main.py            # CLI entrypoint
│   ├── config.py          # Configuration management
│   ├── utils.py           # Shared utilities
│   ├── cache.py           # Cache management
│   ├── constants.py       # Application constants
│   ├── status.py          # Status/logging utilities
│   ├── llm_provider.py    # LLM provider abstraction
│   ├── cron.py            # Scheduled task runner
│   ├── art.py             # ASCII art for CLI
│   └── classes/           # Provider implementations
│       ├── YouTube.py     # YouTube automation
│       ├── Twitter.py     # Twitter/X automation
│       ├── Tts.py         # Text-to-speech
│       ├── AFM.py         # Affiliate marketing
│       └── Outreach.py    # Cold outreach automation
├── scripts/               # Helper scripts
│   ├── setup_local.sh
│   ├── preflight_local.py
│   └── upload_video.sh
├── fonts/                 # Font files for video rendering
├── assets/                # Static assets
├── docs/                  # Documentation
├── config.json            # User configuration (gitignored)
├── config.example.json    # Configuration template
└── requirements.txt       # Python dependencies

Configuration System

All user-facing configuration lives in config.json. The src/config.py module provides getter functions:

Adding New Config Options

1. Add to config.example.json:
{
  "my_custom_option": "default_value"
}
2. Create getter in src/config.py:
def get_my_custom_option() -> str:
    """
    Gets the custom option from config.
    
    Returns:
        value (str): The custom option value
    """
    with open(os.path.join(ROOT_DIR, "config.json"), "r") as file:
        return json.load(file).get("my_custom_option", "default_value")
3. Use anywhere in the codebase:
from config import get_my_custom_option

value = get_my_custom_option()

Configuration Best Practices

  • Use descriptive names (whisper_model, not wm)
  • Provide sensible defaults with .get(key, default)
  • Support environment variables for sensitive data:
    return json.load(file).get("api_key", "") or os.environ.get("API_KEY", "")
    
  • Document options in config.example.json

Extending Provider Classes

YouTube Class Extension

The YouTube class (src/classes/YouTube.py:35) handles video generation and upload. Here’s how to extend it: Example: Add Custom Video Effects
from src.classes.YouTube import YouTube
from moviepy.video.fx.all import fadein, fadeout

class CustomYouTube(YouTube):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
    
    def apply_custom_effects(self, clip):
        """
        Apply custom video effects to a clip.
        
        Args:
            clip: MoviePy video clip
            
        Returns:
            Modified clip with effects applied
        """
        # Add fade in/out effects
        clip = clip.fx(fadein, 1.0)
        clip = clip.fx(fadeout, 1.0)
        
        # Add your custom effects here
        # Example: color correction, filters, etc.
        
        return clip
    
    def combine(self) -> str:
        """
        Override combine method to add custom effects.
        """
        # Call parent method to do standard processing
        path = super().combine()
        
        # Post-process the generated video
        from moviepy.editor import VideoFileClip
        
        video = VideoFileClip(path)
        video = self.apply_custom_effects(video)
        
        # Write the modified video
        output_path = path.replace(".mp4", "_custom.mp4")
        video.write_videofile(output_path, threads=get_threads())
        
        return output_path
Example: Custom Prompt Generation
class CustomYouTube(YouTube):
    def generate_prompts(self) -> List[str]:
        """
        Override to use a custom prompt generation strategy.
        """
        # Use your own prompting logic
        custom_prompt = f"""
        Generate exactly 5 detailed image prompts for: {self.subject}
        
        Requirements:
        - Each prompt must be cinematic and visually striking
        - Include lighting, composition, and mood details
        - Match the tone: {self.niche}
        
        Return as JSON array.
        """
        
        completion = self.generate_response(custom_prompt)
        image_prompts = json.loads(completion)
        
        self.image_prompts = image_prompts
        return image_prompts
Using Your Custom Class:
# In src/cron.py or your own script
from my_custom_youtube import CustomYouTube

youtube = CustomYouTube(
    account_uuid="my-account",
    account_nickname="My Channel",
    fp_profile_path="/path/to/firefox/profile",
    niche="Technology",
    language="English"
)

video_path = youtube.generate_video(tts_instance)

Twitter Class Extension

The Twitter class (src/classes/Twitter.py:24) handles social media automation. Example: Custom Post Formatting
from src.classes.Twitter import Twitter

class CustomTwitter(Twitter):
    def __init__(self, *args, hashtags=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.hashtags = hashtags or []
    
    def generate_post(self) -> str:
        """
        Override to add custom hashtags and formatting.
        """
        # Get base content from LLM
        content = super().generate_post()
        
        # Add custom formatting
        # Example: add line breaks for readability
        lines = content.split(". ")
        formatted = "\n\n".join(lines)
        
        # Add hashtags
        if self.hashtags:
            hashtag_string = " ".join([f"#{tag}" for tag in self.hashtags])
            formatted = f"{formatted}\n\n{hashtag_string}"
        
        # Ensure it fits in Twitter's character limit
        if len(formatted) > 280:
            formatted = formatted[:277] + "..."
        
        return formatted
Example: Add Image Attachments
class CustomTwitter(Twitter):
    def post_with_image(self, text: str, image_path: str) -> None:
        """
        Post tweet with an attached image.
        
        Args:
            text: Tweet content
            image_path: Path to image file
        """
        bot = self.browser
        bot.get("https://x.com/compose/post")
        
        # Find and fill text box
        text_box = self.wait.until(
            EC.element_to_be_clickable(
                (By.CSS_SELECTOR, "div[data-testid='tweetTextarea_0'][role='textbox']")
            )
        )
        text_box.click()
        text_box.send_keys(text)
        
        # Upload image
        file_input = bot.find_element(
            By.CSS_SELECTOR, "input[data-testid='fileInput']"
        )
        file_input.send_keys(os.path.abspath(image_path))
        
        time.sleep(2)  # Wait for upload
        
        # Click post button
        post_button = self.wait.until(
            EC.element_to_be_clickable(
                (By.XPATH, "//button[@data-testid='tweetButton']")
            )
        )
        post_button.click()
        
        time.sleep(2)
        self.add_post({"content": text, "image": image_path, "date": datetime.now().strftime("%m/%d/%Y, %H:%M:%S")})

Adding New Providers

To add support for a new platform (e.g., TikTok, Instagram): 1. Create a new class file: src/classes/TikTok.py
import os
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
from webdriver_manager.firefox import GeckoDriverManager

from config import get_firefox_profile_path, get_headless
from status import info, success, error

class TikTok:
    """
    Automation class for TikTok.
    """
    
    def __init__(self, account_uuid: str, fp_profile_path: str, niche: str):
        self.account_uuid = account_uuid
        self.fp_profile_path = fp_profile_path
        self.niche = niche
        
        # Initialize Selenium
        self.options = Options()
        if get_headless():
            self.options.add_argument("--headless")
        
        self.options.add_argument("-profile")
        self.options.add_argument(fp_profile_path)
        
        self.service = Service(GeckoDriverManager().install())
        self.browser = webdriver.Firefox(
            service=self.service, 
            options=self.options
        )
    
    def upload_video(self, video_path: str, title: str, description: str) -> bool:
        """
        Upload a video to TikTok.
        
        Args:
            video_path: Path to video file
            title: Video title
            description: Video description
            
        Returns:
            success: Whether upload succeeded
        """
        try:
            self.browser.get("https://www.tiktok.com/upload")
            time.sleep(3)
            
            # Find file input and upload
            file_input = self.browser.find_element(
                By.CSS_SELECTOR, "input[type='file']"
            )
            file_input.send_keys(os.path.abspath(video_path))
            
            # Fill caption/description
            caption_box = self.browser.find_element(
                By.CSS_SELECTOR, "div[contenteditable='true']"
            )
            caption_box.click()
            caption_box.send_keys(f"{title}\n\n{description}")
            
            # Click post button
            post_button = self.browser.find_element(
                By.XPATH, "//button[contains(text(), 'Post')]"
            )
            post_button.click()
            
            time.sleep(5)
            success(f"Uploaded video to TikTok: {title}")
            return True
            
        except Exception as e:
            error(f"Failed to upload to TikTok: {e}")
            return False
        finally:
            self.browser.quit()
2. Add configuration options in config.json:
{
  "tiktok_accounts": [
    {
      "id": "tiktok-account-1",
      "niche": "Comedy",
      "firefox_profile": "/path/to/profile"
    }
  ]
}
3. Create helper functions in src/config.py:
def get_tiktok_accounts() -> List[dict]:
    with open(os.path.join(ROOT_DIR, "config.json"), "r") as file:
        return json.load(file).get("tiktok_accounts", [])
4. Integrate into src/cron.py or CLI:
from classes.TikTok import TikTok

def run_tiktok_automation(account_id: str):
    accounts = get_tiktok_accounts()
    account = next((a for a in accounts if a["id"] == account_id), None)
    
    if not account:
        error(f"TikTok account {account_id} not found")
        return
    
    tiktok = TikTok(
        account_uuid=account["id"],
        fp_profile_path=account["firefox_profile"],
        niche=account["niche"]
    )
    
    # Generate video using YouTube class logic
    # (or create separate TikTok video generation)
    video_path = generate_tiktok_video(account["niche"])
    
    tiktok.upload_video(
        video_path=video_path,
        title="My TikTok Video",
        description="Generated with MoneyPrinter V2"
    )

Custom LLM Providers

The src/llm_provider.py module abstracts LLM calls. To add a new provider: Example: Add Anthropic Claude
# In src/llm_provider.py

import anthropic
from config import get_anthropic_api_key

def generate_text_anthropic(prompt: str, model: str = "claude-3-5-sonnet-20241022") -> str:
    """
    Generate text using Anthropic Claude API.
    """
    client = anthropic.Anthropic(api_key=get_anthropic_api_key())
    
    message = client.messages.create(
        model=model,
        max_tokens=1024,
        messages=[
            {"role": "user", "content": prompt}
        ]
    )
    
    return message.content[0].text

def generate_text(prompt: str, model_name: str = None) -> str:
    """
    Main entry point for text generation.
    """
    provider = get_llm_provider()  # From config.json
    
    if provider == "anthropic":
        return generate_text_anthropic(prompt, model_name)
    elif provider == "local_ollama":
        return generate_text_ollama(prompt, model_name)
    # ... other providers
Update config.json:
{
  "llm_provider": "anthropic",
  "anthropic_api_key": "sk-ant-...",
  "anthropic_model": "claude-3-5-sonnet-20241022"
}

Modifying Prompts

Prompts are scattered throughout provider classes. To systematically customize them: Centralize prompts in src/prompts.py:
# src/prompts.py

def get_youtube_script_prompt(subject: str, language: str, sentence_length: int) -> str:
    return f"""
    Generate a {sentence_length}-sentence video script about: {subject}
    
    Requirements:
    - Language: {language}
    - Engaging hook in first sentence
    - Clear structure with beginning, middle, end
    - Conversational tone
    - No markdown formatting
    
    Output only the raw script text.
    """

def get_youtube_title_prompt(subject: str) -> str:
    return f"""
    Generate a compelling YouTube title for: {subject}
    
    Requirements:
    - Under 100 characters
    - Include relevant hashtags
    - Clickable but not clickbait
    
    Output only the title.
    """

def get_image_prompt_template(subject: str, script: str, n_prompts: int) -> str:
    return f"""
    Generate {n_prompts} detailed AI image generation prompts for: {subject}
    
    Context: {script}
    
    Requirements:
    - Photorealistic, cinematic style
    - Include lighting, composition, mood
    - Relevant to the script narrative
    
    Output as JSON array: ["prompt1", "prompt2", ...]
    """
Use in classes:
# In src/classes/YouTube.py
from prompts import get_youtube_script_prompt

class YouTube:
    def generate_script(self) -> str:
        prompt = get_youtube_script_prompt(
            subject=self.subject,
            language=self.language,
            sentence_length=get_script_sentence_length()
        )
        completion = self.generate_response(prompt)
        # ... rest of method
This makes it easy to A/B test prompts or swap them based on niche.

Testing Your Extensions

When developing custom features: 1. Use verbose mode:
{
  "verbose": true
}
2. Test in isolation:
# test_custom_youtube.py
from my_extensions import CustomYouTube
from classes.Tts import TTS

if __name__ == "__main__":
    youtube = CustomYouTube(
        account_uuid="test",
        account_nickname="Test",
        fp_profile_path="/path/to/profile",
        niche="Technology",
        language="English"
    )
    
    # Test individual methods
    topic = youtube.generate_topic()
    print(f"Topic: {topic}")
    
    script = youtube.generate_script()
    print(f"Script: {script}")
3. Run preflight checks:
python3 scripts/preflight_local.py
4. Monitor logs: The status.py module provides logging:
from status import info, success, warning, error

info("Starting custom process...")
success("Process completed successfully!")
warning("Non-critical issue detected")
error("Critical failure occurred")

Contributing Extensions

If you build something useful:
  1. Document your changes in docs/
  2. Update config.example.json with new options
  3. Add examples and usage instructions
  4. Submit a pull request to the main repository
  5. See CONTRIBUTING.md for guidelines
Community extensions help everyone! Share your customizations on the Discord.

Build docs developers (and LLMs) love