Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on:
push:
branches: [main, "agent/**"]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install
run: python -m pip install -e ".[dev]"
- name: Compile
run: python -m compileall -q app
- name: Test
run: pytest -q
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
__pycache__/
*.py[cod]
.venv/
.env
.DS_Store
.pytest_cache/
.ruff_cache/
storage/*
!storage/.gitkeep
*.log
25 changes: 25 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
FROM node:22-bookworm-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
VIRTUAL_ENV=/opt/venv \
PATH="/opt/venv/bin:$PATH"

RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-venv python3-pip ffmpeg ca-certificates \
&& rm -rf /var/lib/apt/lists/*

RUN python3 -m venv /opt/venv
RUN npm install -g @openai/codex @anthropic-ai/claude-code @google/gemini-cli

WORKDIR /app
COPY pyproject.toml README.md LICENSE ./
COPY app ./app
COPY web ./web
RUN pip install --no-cache-dir .

RUN mkdir -p /app/storage/uploads /app/storage/tasks
VOLUME ["/app/storage"]
EXPOSE 8501

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8501"]
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
MIT License

Copyright (c) 2024 Harry
Copyright (c) 2026 GenRamzi contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
11 changes: 11 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Attribution and project notice

MoneyPrinterTurbo NoAPI is a modified/rebuilt project inspired by and derived from ideas and portions of the MIT-licensed MoneyPrinterTurbo project:

- Upstream: https://github.com/harry0703/MoneyPrinterTurbo
- Upstream author/copyright notice: Copyright (c) 2024 Harry
- Upstream license: MIT

This repository adds a new account/CLI provider architecture, a new web interface, a local-first media workflow, new task orchestration, and NoAPI-specific documentation and behavior.

OpenAI, ChatGPT, Codex, Anthropic, Claude, Google, Gemini, Ollama, and other product names are trademarks of their respective owners. This project is not endorsed by or affiliated with those vendors.
160 changes: 159 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,161 @@
# MoneyPrinterTurbo NoAPI

Modern no-key rebuild of MoneyPrinterTurbo. Development happens on feature branches and is merged through pull requests.
A modern, local-first rebuild of [MoneyPrinterTurbo](https://github.com/harry0703/MoneyPrinterTurbo) that removes the old **LLM Settings / API-key workflow** and replaces it with **connected AI accounts and local models**.

> Status: first functional rebuild. The UI, account-provider layer, local media pipeline, TTS, subtitles, task runner, and FFmpeg renderer are implemented. This repository intentionally does **not** scrape browser cookies or impersonate ChatGPT/Claude/Gemini web sessions.

## What changed

- New responsive web studio instead of the large Streamlit settings screen.
- No user API-key form.
- ChatGPT through the official **Codex CLI** after `codex login`.
- Claude through the official **Claude Code CLI** after account login.
- Gemini through the official **Gemini CLI** after Google login.
- Ollama as a fully local fallback with no cloud account.
- Prompt → script → neural TTS → SRT subtitles → media composition → MP4 pipeline.
- Local image/video uploads, background music, 9:16 / 16:9 / 1:1 output, and batch generation.
- FastAPI backend with task progress and downloadable results.
- No credential copying: account credentials stay where the official provider CLI stores them.

## Important meaning of “NoAPI”

“NoAPI” means **the user does not enter or manage an API key in this application**. Cloud models still communicate with their vendors through the vendor’s official authenticated software. ChatGPT, Claude, and Gemini subscriptions/usage rules still apply. Ollama is the option for fully local inference.

The app does not use unofficial cookie extraction, hidden browser endpoints, session-token copying, or credential harvesting.

## Requirements

- Python 3.11+
- FFmpeg + ffprobe available in `PATH`
- At least one AI provider below
- Internet access for cloud-account providers and Edge neural TTS

### ChatGPT

```bash
npm install -g @openai/codex
codex login
codex login status
```

### Gemini

```bash
npm install -g @google/gemini-cli
gemini
```

Inside Gemini CLI, choose **Login with Google**. After that, MoneyPrinterTurbo NoAPI uses Gemini CLI headless mode for script generation.

### Claude

```bash
npm install -g @anthropic-ai/claude-code
claude auth login
claude auth status
```

### Ollama (fully local)

Install Ollama, then:

```bash
ollama pull qwen3:8b
ollama list
```

Change the default local model with:

```bash
export MPT_OLLAMA_MODEL="qwen3:8b"
```

On Windows PowerShell:

```powershell
$env:MPT_OLLAMA_MODEL="qwen3:8b"
```

## Run locally

```bash
git clone https://github.com/GenRamzi/MoneyPrinterTurboNoAPI.git
cd MoneyPrinterTurboNoAPI
python -m venv .venv
```

Activate the virtual environment, then:

```bash
pip install -e .
python main.py
```

Open `http://127.0.0.1:8501`.

The UI can launch a provider login terminal on common desktop systems. If the app is running in Docker, a remote server, or a desktop environment where terminal launching is unavailable, it shows the exact login command instead.

## Docker

Docker is useful for the renderer and Ollama-oriented deployments. Account-based CLI login is usually simpler when running the app directly on the host OS.

```bash
docker compose up --build
```

Then open `http://localhost:8501`.

## Current video workflow

1. Choose a connected AI account.
2. Enter a topic or paste your own script.
3. Choose language, duration, aspect ratio, voice, and subtitle position.
4. Upload any number of images/videos; they are normalized and rotated across batch outputs.
5. Optionally add background music.
6. Generate. The task engine writes the script, voice, captions, intermediate media, and final MP4 files under `storage/tasks/<task-id>/`.

If no visual media is uploaded, the renderer creates a clean generated background so the pipeline remains usable without any stock-media API.

## Architecture

```text
web/ Modern zero-build frontend
app/main.py FastAPI entrypoint + SPA hosting
app/api.py Providers, uploads, tasks, downloads
app/providers/ Official CLI account adapters
app/services/script.py AI script generation
app/services/tts.py Edge neural TTS
app/services/subtitles.py SRT timing/generation
app/services/media.py FFmpeg normalization/composition
app/services/tasks.py Background job orchestration
storage/ User uploads and generated projects
```

Provider adapters are intentionally isolated. Adding another provider only requires implementing the `TextProvider` interface; the creator UI consumes the registry automatically.

## Security decisions

- No API keys are accepted by the web UI.
- No browser cookies are requested, parsed, or stored.
- Login commands are fixed per provider; user input is never concatenated into an arbitrary shell command.
- Upload and task-download paths are normalized and constrained to application storage.
- Provider credentials remain managed by the official CLIs.
- Generated files are local unless the user deploys this server elsewhere.

## Differences from upstream

This is a clean architectural rebuild inspired by and derived from MoneyPrinterTurbo’s MIT-licensed concept and workflow. It is not a claim of affiliation with Harry or the AI vendors. See `NOTICE.md` and `LICENSE`.

The original project supports many API-backed LLM and stock-media providers. This rebuild deliberately prioritizes **account login, local inference, local materials, and a much simpler product surface** instead of reproducing API-key configuration screens.

## Development

```bash
pip install -e ".[dev]"
pytest -q
python -m compileall -q app
```

## License

MIT. Upstream copyright and license notice are retained as required by the original license.
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
104 changes: 104 additions & 0 deletions app/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from __future__ import annotations

import shutil
import uuid
from pathlib import Path

from fastapi import APIRouter, File, HTTPException, UploadFile
from fastapi.responses import FileResponse

from app.core.config import settings
from app.models.schemas import GenerateRequest, TaskInfo
from app.providers.registry import provider_registry
from app.services.media import AUDIO_EXT, IMAGE_EXT, VIDEO_EXT
from app.services.tasks import task_manager

router = APIRouter(prefix="/api")
ALLOWED_UPLOADS = VIDEO_EXT | IMAGE_EXT | AUDIO_EXT


@router.get("/health")
def health() -> dict:
return {
"ok": True,
"app": settings.app_name,
"ffmpeg": bool(shutil.which(settings.ffmpeg_bin)),
"ffprobe": bool(shutil.which(settings.ffprobe_bin)),
}


@router.get("/providers")
def providers():
return provider_registry.list()


@router.post("/providers/{provider_id}/login")
def provider_login(provider_id: str) -> dict:
try:
message = provider_registry.open_login(provider_id)
except Exception as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return {"ok": True, "message": message}


@router.get("/voices")
def voices() -> list[dict[str, str]]:
return [
{"id": "ar-SA-HamedNeural", "name": "Hamed — Arabic (Saudi)"},
{"id": "ar-SA-ZariyahNeural", "name": "Zariyah — Arabic (Saudi)"},
{"id": "ar-EG-ShakirNeural", "name": "Shakir — Arabic (Egypt)"},
{"id": "ar-EG-SalmaNeural", "name": "Salma — Arabic (Egypt)"},
{"id": "en-US-AndrewNeural", "name": "Andrew — English (US)"},
{"id": "en-US-AvaNeural", "name": "Ava — English (US)"},
{"id": "en-GB-RyanNeural", "name": "Ryan — English (UK)"},
{"id": "en-GB-SoniaNeural", "name": "Sonia — English (UK)"},
]


@router.post("/uploads")
async def upload(files: list[UploadFile] = File(...)) -> list[dict[str, str]]:
saved: list[dict[str, str]] = []
max_bytes = settings.max_upload_mb * 1024 * 1024
for file in files:
suffix = Path(file.filename or "").suffix.lower()
if suffix not in ALLOWED_UPLOADS:
raise HTTPException(status_code=415, detail=f"Unsupported file type: {suffix or 'unknown'}")
data = await file.read(max_bytes + 1)
if len(data) > max_bytes:
raise HTTPException(status_code=413, detail=f"File exceeds {settings.max_upload_mb} MB")
file_id = f"{uuid.uuid4().hex}{suffix}"
target = settings.uploads_dir / file_id
target.write_bytes(data)
saved.append({"id": file_id, "name": file.filename or file_id})
return saved


@router.post("/tasks", response_model=TaskInfo)
def create_task(request: GenerateRequest) -> TaskInfo:
return task_manager.create(request)


@router.get("/tasks", response_model=list[TaskInfo])
def list_tasks() -> list[TaskInfo]:
return task_manager.list()


@router.get("/tasks/{task_id}", response_model=TaskInfo)
def get_task(task_id: str) -> TaskInfo:
task = task_manager.get(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task


@router.get("/tasks/{task_id}/files/{filename}")
def task_file(task_id: str, filename: str):
if Path(task_id).name != task_id or Path(filename).name != filename:
raise HTTPException(status_code=400, detail="Invalid path")
task = task_manager.get(task_id)
if not task or filename not in task.output_files:
raise HTTPException(status_code=404, detail="File not found")
path = (settings.tasks_dir / task_id / filename).resolve()
if path.parent != (settings.tasks_dir / task_id).resolve() or not path.exists():
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(path, media_type="video/mp4", filename=filename)
Empty file added app/core/__init__.py
Empty file.
Loading
Loading