harry0703/MoneyPrinterTurbo
github.com/harry0703/MoneyPrinterTurbo ↗ · 177 files · 3.2 MB
MoneyPrinterTurbo 💸 一站式 AI 短视频生成工具 只需提供视频 主题 或 关键词 ,即可自动生成视频脚本、匹配素材、生成字幕和背景音乐,并合成高清短视频。 简体中文 | English | 日本語 | 版本发布 | 问题反馈 界面预览 🖥️ WebUI API --- 特别感谢 ❤️ 感谢 Kimi 赞助本项目!Kimi K3 是 Moonshot AI 迄今能力最强的模型,也是全球首个开源 3T 级模型,拥有原生视觉能力与 100 万 Token 上下文,在知识工作、推理和长周期任务中展现
Overview
MoneyPrinterTurbo v1.3.7 is a Python 3.11+ application that generates finished short videos end-to-end: an LLM writes a script and material search terms, a TTS engine produces the voiceover, subtitles are built from TTS cues or Whisper, stock or AI-generated clips are downloaded per keyword, and MoviePy/FFmpeg renders final MP4s that can optionally be cross-posted to TikTok/Instagram/YouTube. It ships three frontends — a FastAPI service (port 8080), a Streamlit WebUI (port 8501), and a batch-capable argparse CLI — that all converge on one shared pipeline in app/services/task.py. Its distinctive traits are a very large provider matrix (~20 LLM providers via a registry, ~11 TTS engines, ~10 material sources including several paid text-to-video APIs with explicit charge-confirmation flags) and heavy hardening around config locking, path confinement, and task-state consistency for what is fundamentally a single-user local tool.
System map
The system is a single-process Python app with three entrypoints that share one orchestration layer. main.py starts uvicorn serving the FastAPI app built in app/asgi.py; webui.sh/webui.bat/Dockerfile start `streamlit run webui/Main.py`; cli.py runs the pipeline synchronously and prints JSON. All routes under /api/v1 are registered in app/router.py and guarded by an optional x-api-key check. Task state lives in an in-memory dict or Redis depending on config, task scheduling in a bounded thread-backed queue, and artifacts on disk under storage/tasks/<task_id>. Deployment is a single Docker image exposing either the WebUI (8501) or API (8080).
- main.py is the API entrypoint: it calls uvicorn.run(app='app.asgi:app', host=config.listen_host, port=config.listen_port) with defaults 0.0.0.0:8080 from config.toml.
- app/asgi.py get_application() builds the FastAPI instance, includes root_api_router, registers HttpException and RequestValidationError handlers, mounts StaticFiles at /tasks (storage/tasks) and / (resource/public), and installs middleware that requires verify_token for /tasks/* and rejects untrusted browser Origins.
- app/router.py aggregates three routers: ping (/ping health check) plus the v1 video and llm controllers; app/controllers/v1/base.py new_router() stamps prefix '/api/v1' and attaches the verify_token dependency to every route.
- webui/Main.py is an ~8,262-line Streamlit application that inserts the repo root into sys.path, loads i18n JSON locales, and submits generation through app/services/webui_task.py; webui.sh picks an interpreter (.venv → uv → PATH streamlit) and scans ports 8501-8599 for a free one.
- cli.py is a third entrypoint exposing every pipeline parameter as argparse flags, a --stop-at stage selector over ('script','terms','audio','subtitle','materials','video'), and a --batch-file mode accepting a JSON array or JSONL manifest capped at 100 tasks / 1 MiB.
- app/services/task.py is the single shared pipeline: start() wraps _run_pipeline(), which all three frontends call, and converts unexpected exceptions into failed task states via _mark_task_failed.
- app/config/config.py loads config.toml (auto-copied from config.example.toml on first run, with a UTF-8 BOM retry path), exposes section dicts wrapped in _SynchronizedConfig, and serializes saves through a reentrant lock plus mkstemp+os.replace atomic writes with an EBUSY fallback for bind-mounted files.
- Task state is pluggable: app/services/state.py picks MemoryState (dict + RLock) or RedisState (per-task Redis hashes, SCAN-paginated listing, Lua-scripted patch_task) based on config.app.enable_redis.
- Task scheduling is pluggable: InMemoryTaskManager uses a bounded queue.Queue while RedisTaskManager pushes JSON jobs onto a Redis list 'task_queue' and maps function names through FUNC_MAP on dequeue; both enforce max_concurrent_tasks (default 5) and max_queued_tasks (default 100) with TaskQueueFullError → HTTP 429.
- Docker packaging is a single python:3.11-slim-bullseye image that apt-installs git+ffmpeg (with Chinese-mirror fallback logic), pip-installs requirements.txt, and by default runs the Streamlit WebUI; docker-compose.yml runs two containers (webui on 127.0.0.1:8501, api on 127.0.0.1:8080) sharing the repo as a volume.
- Filesystem layout is rooted at the repo: utils.storage_dir()/task_dir() create storage/{tasks,cache_videos,local_videos,bgm} on demand, while resource/{fonts,songs,public} hold shipped assets; task artifacts are per-task subdirectories.
- Dependencies are declared in pyproject.toml (moviepy 2.2.1, streamlit 1.59.1, fastapi 0.136.3, edge-tts 7.2.7, faster-whisper, redis, litellm, google-genai, etc.) pinned by uv.lock; requirements.txt duplicates the list 'for legacy pip install support' and the project is configured package=false (not installable).
Feature catalog
Features span a REST API for video/subtitle/audio generation and task management, a full Streamlit WebUI, and a CLI with batch mode. The provider surface is the headline: ~20 LLM providers, ~11 TTS engines, ~10 video material sources (three stock-footage search APIs, six paid text-to-video generators, an OpenAI-compatible image endpoint, and local files), two subtitle engines, three BGM modes plus two AI music services, and Upload-Post cross-posting. Supporting features include BGM/material upload endpoints, range-streaming and download endpoints, a 24h material search cache, Whisper transcription, i18n, task history, and an update checker.
- POST /api/v1/videos creates a full video task; POST /api/v1/subtitle and POST /api/v1/audio run the same pipeline truncated at intermediate stages via create_task(stop_at=...).
- Task management endpoints: GET /api/v1/tasks (paginated), GET /api/v1/tasks/{id} (rewrites stored file paths into /tasks/* URIs using the configured endpoint), DELETE /api/v1/tasks/{id} which refuses busy tasks (409) and rmtree's the task directory.
- LLM utility endpoints: POST /api/v1/scripts (script generation), POST /api/v1/terms (material search terms), and POST /api/v1/social-metadata (titles/descriptions/hashtags per platform).
- Media asset endpoints: GET/POST /api/v1/musics lists/uploads BGM (≤30 MB, UUID storage name, FFmpeg-validated) and GET/POST /api/v1/video_materials lists/uploads local clips/images into storage/local_videos with sanitized filenames.
- GET /api/v1/stream/{file_path} returns HTTP 206 range-streamed video with single-range parsing and 416 handling; GET /api/v1/download/{file_path} returns FileResponse — both confined to the tasks directory by file_security.resolve_path_within_directory.
- The CLI supports per-stage execution (--stop-at), --batch-file JSON/JSONL manifests, and mandatory --confirm-{wavespeed,seedance,ofox,metaso-minimax,muapi}-charge flags before paid generation sources run.
- LLM_PROVIDER_REGISTRY in app/models/llm_provider.py declares ~20+ providers (moonshot, openai, anthropic, gemini, deepseek, qwen, azure, volcengine, grok, minimax, mimo, cloudflare, modelscope, aihubmix, aimlapi, evolink, openrouter, api_route, ollama, claude_code, oneapi, litellm, groq, pollinations) with per-provider default models, base URLs, regional endpoints, and deprecated-value migration.
- TTS dispatch in voice.py covers Azure v1 (edge-tts), Azure Speech v2, SiliconFlow, Gemini, MiMo, MiniMax, ElevenLabs, Chatterbox, Kokoro, Fish Audio, VoxCPM (with reference-audio cloning inputs), and a 'no-voice' silent-audio mode; provider identity is encoded in voice-name prefixes (gemini:, mimo:, elevenlabs:, chatterbox:, kokoro:, voxcpm:).
- Video material sources include stock search (pexels, pixabay, coverr with a 24h result cache), paid text-to-video generation (wavespeed, volcengine_seedance, ofox, muapi, metaso_minimax, loomloom), openai_image (OpenAI-compatible /images/generations rendered into zoom clips), and local files.
- Subtitle generation supports 'edge' (from the TTS SubMaker timeline, sentence or word_by_word mode, pop_spring animation) and 'whisper' (faster-whisper transcription with model auto-download plus an LLM-free correct() alignment pass).
- Video rendering features: random/sequential concat, seven transition modes, clip speed 0.5-2.0, aspect ratios 9:16/16:9/1:1, cover/contain fit, video_count batch output, subtitle positioning (top/center/bottom/two_thirds_bottom/custom), and an FFmpeg encoder fallback chain (libx264, h264_nvenc/amf/qsv/mf/videotoolbox) that probes encoder availability at runtime.
- Background music modes: 'random'/'custom' from resource/songs and storage/bgm, plus AI-generated music via Sonilo (.m4a) or ElevenLabs (.mp3) registered in _VIDEO_MUSIC_PROVIDERS with per-provider prompt limits and graceful degradation to a warning when generation fails after the video is otherwise complete.
Data flow
A request becomes a pydantic VideoParams, gets a UUID task_id, a state record, and a storage/tasks/<task_id> workspace. The pipeline transforms subject→script→terms→audio→subtitle→materials→final MP4, writing intermediate files (script.json, audio.mp3, subtitle.srt, combined-N.mp4, final-N.mp4) into the task dir and progress/state into MemoryState or RedisState. Outputs leave via the /tasks static mount, /stream, /download, or cross-posting to upload-post.com. Configuration flows the other direction: config.toml → section dicts, with WebUI runtime writes funneled through a non-blocking pending-update queue and atomic file saves.
- Ingress: TaskVideoRequest (a VideoParams subclass) is validated by pydantic, dumped into a task record {task_id, request_id, params}, persisted via sm.state.update_task, then tm.start is scheduled through task_manager.add_task — with rollback of the state record if scheduling throws.
- Script/terms stage: generate_script uses params.video_script verbatim or calls llm.generate_script; generate_terms calls llm.generate_terms (5 terms, or 8 in match-script-order mode) and optionally reranks through twelvelabs.rerank_terms_by_subject; results are persisted to <task_dir>/script.json via task_artifacts.write_script_data's atomic temp-file+os.replace write.
- Audio stage: resolve_custom_audio_file confines submitted paths to the task directory unless allow_server_file_input=True (CLI only); otherwise voice.tts writes audio.mp3 and returns an edge-tts SubMaker timeline; audio_duration is measured from the written file, not cue end times.
- Subtitle stage: 'edge' provider serializes SubMaker cues to subtitle.srt (never auto-falling back to Whisper, to avoid surprise multi-GB model downloads); 'whisper' transcribes the audio file with faster-whisper and runs subtitle.correct() against the script.
- Materials stage: stock sources go through _search_videos_with_cache (24h per-term cache in material_cache, double-checked locking) then download_videos; paid generation sources take _download_videos_*_on_demand paths that generate clips one at a time and stop once audio_duration*video_count is covered; 'local' preprocesses user files; 'loomloom' executes a confirmed SkillBot run and persists run_id immediately since billing has already started.
- Render stage: generate_final_videos loops video_count times calling video.combine_videos (→ combined-N.mp4), optionally calls the AI music provider's generate_bgm, then video.generate_video (muxing narration, subtitles, BGM → final-N.mp4); progress is written to state at ~5/10/20/30/40/50→100 milestones.
- Egress: task state stores absolute file paths; GET /tasks/{id} converts them to /tasks/* URLs via _task_file_to_uri (which re-validates each path inside the task dir), and the StaticFiles mount plus /stream//download endpoints serve bytes from storage/tasks.
- Config read path: load_config() → _cfg TOML dict → section dicts (app, azure, whisper, proxy, ui, and per-TTS sections); reads anywhere use config.<section>.get(...); app.redis_host is overridable via MPT_APP_REDIS_HOST/REDIS_HOST env vars.
- Config write path (WebUI): controls call update_config_nonblocking/delete_config_nonblocking which enqueue into _pending_config_updates keyed by id(section); a background 'mpt-config-flush' thread applies them when runtime_config_lock is free; save_config writes config.toml atomically.
- In Redis mode the task queue is a Redis list of JSON payloads; enqueue serializes VideoParams via model_dump and stores the function name, and dequeue re-validates against FUNC_MAP and current VideoParams — dropping stale entries while marking their tasks failed so they never appear stuck in 'processing'.
- Cross-post flow: on success the task is marked COMPLETE with cross_post_state='pending' and cross_post_owner='hostname:pid:uuid'; _schedule_cross_post submits to a bounded ThreadPoolExecutor(max_workers=2) gated by a BoundedSemaphore, and terminal state is written via patch_task so a deleted task is never resurrected.
- Failure propagation: every stage funnels into _mark_task_failed(state=-1, failed_stage, error) which preserves remote IDs (volcengine_seedance_task_id, ofox_task_id, metaso_minimax_task_id, muapi_task_id, loomloom_run_id) so paid remote jobs remain recoverable from provider dashboards.
Behavior traces
Three flows were traced end-to-end. API startup goes main.py → asgi app construction → lifespan recovery. The primary action (video generation) is identical across frontends: a state record is created before scheduling, _run_pipeline walks preflight→script→terms→audio→subtitle→materials→video, and cross-posting is scheduled only after the task is COMPLETE. Persistence flows through two channels — task state (memory dict or Redis hashes) and task artifacts (atomic JSON writes into storage/tasks/<id>) — plus config.toml for settings.
- API startup trace: `python main.py` → uvicorn.run('app.asgi:app') → module-level get_application() builds routes/middleware/static mounts → parse_cors_allowed_origins(CORS_ALLOWED_ORIGINS) → lifespan logs startup, warns when app.api_key is empty, and calls task_service.recover_interrupted_cross_posts() to fail orphaned pending/processing cross-posts whose owner process no longer exists.
- First-run config trace: importing app.config.config → load_config() → if config.toml is a directory it is deleted, if missing it is copied from config.example.toml → _load_toml_config parses with a utf-8-sig BOM fallback → section dicts are wrapped in _SynchronizedConfig → ffmpeg_path (if set) is exported as IMAGEIO_FFMPEG_EXE for imageio/moviepy.
- API video-generation trace: POST /api/v1/videos → verify_token dependency → create_task → utils.get_uuid() → sm.state.update_task(task_id) → task_manager.add_task(tm.start, ...) → TaskManager.execute_task spawns a thread running run_task → tm.start → _run_pipeline → on completion update_task(COMPLETE, progress=100, videos=...) → optional _schedule_cross_post.
- Pipeline order inside _run_pipeline: (0) preflight — provider is_enabled() checks for seedance/ofox/metaso_minimax/muapi/openai_image, music-provider key+prompt-length+access validation, then utils.check_ffmpeg_ready() unless stop_at is script/terms; (1) generate_script → (2) generate_terms (skipped for local source) → save_script_data → (3) generate_audio → (4) generate_subtitle → (5) get_video_materials → (6) generate_final_videos → (7) mark COMPLETE + schedule cross-post.
- stop-at truncation trace: each stage boundary checks stop_at and returns early with COMPLETE state — 'script' returns {script}, 'terms' returns {script,terms}, 'audio' returns {audio_file,audio_duration}, 'subtitle' returns {subtitle_path}, 'materials' returns {materials}; the API exposes this as the /subtitle and /audio endpoints and the CLI as --stop-at.
- WebUI generation trace: Main.py button handler → _prepare_generation_task() → webui_task.submit_generation() writes state record before scheduling → InMemoryTaskManager(max_concurrent_tasks=1) → _run_generation attaches a thread-filtered loguru sink, acquires config.runtime_config_lock(), calls tm.start → a @st.fragment(run_every=0.5s) polls webui_task.get_task_logs() to stream logs to the page.
- CLI trace: `python cli.py ...` → _force_utf8_console() → run_cli() → parse_args() (custom argparse types validate colors, speeds, UUID task ids, subtitle enums) → params built by merging CLI flags over [ui] config defaults → tm.start(allow_server_file_input=True) → prints one JSON result object to stdout; batch mode merges each manifest object over CLI defaults and runs tasks sequentially.
- Failure trace: any unhandled exception inside _run_pipeline is caught in start(), logged, and converted by _mark_task_failed into state=-1 with failed_stage and error — visible via GET /tasks/{id}; the CLI maps a failed result to exit code 1, argument/manifest errors to exit code 2.
- Task-deletion trace: DELETE /api/v1/tasks/{id} → sm.state.get_task → tm.is_task_busy refuses deletion while state==PROCESSING or cross_post_state in {pending,processing} (409) → shutil.rmtree(storage/tasks/<id>) → sm.state.delete_task.
- Cross-post recovery trace: each cross-post future is registered in _cross_post_futures keyed by task_id with owner 'hostname:pid:uuid' persisted in task state; on startup or deletion checks, _is_cross_post_owner_alive inspects the owner PID (Win32 OpenProcess on Windows, os.kill(pid,0) elsewhere, conservative-alive for foreign hosts) and stranded states are marked failed.
Testing signals
Testing is unusually thorough for this genre: ~70 test files under test/ mixing pytest functions and unittest.TestCase, executed by pytest in CI on Python 3.11 and 3.13 with a real Redis service, plus a Windows smoke job. Branch coverage over app/, cli.py, main.py, webui/, and docs/skill/ is gated at ≥70%. Ruff lint runs in CI (3.11 only) and compileall checks every source tree. Live provider calls are opt-in via MPT_RUN_INTEGRATION_TESTS, so CI coverage is unit/integration-level with mocks rather than true end-to-end renders.
- Test layout: test/test_main.py plus ~70 files under test/services/ named test_<domain>.py (task, task_manager, cli, controllers, voice, video, material, providers, webui_* helpers); test/README.md documents the pytest commands and conventions.
- CI (.github/workflows/ci.yml) runs on ubuntu-latest across Python 3.11 and 3.13 with a redis:7-alpine service (MPT_TEST_REDIS_HOST/PORT/DB env), installs via `uv sync --frozen`, compiles all sources, ruff-checks on 3.11, and runs `coverage run -m pytest -q test`.
- Coverage gate: [tool.coverage] uses branch=true over source=['app','cli','webui','main','docs/skill'] with fail_under=70 — the comment notes measured branch coverage exceeds 70.4% on both Python versions.
- A windows-latest smoke job runs a curated subset (test_config, test_state, test_task, test_task_manager, test_upload_post, test_controller_video, test_webui_task, test_webui_headless_task_actions) — implying the full suite is not guaranteed Windows-clean.
- test/services/test_cli.py (~106 test functions) and test/services/test_task.py (~62) are the largest suites, covering CLI arg validation, manifest handling, and pipeline stage behavior — consistent with cli.py and task.py being the most defect-sensitive surfaces.
- Security behavior is directly tested: test_api_authentication.py, test_asgi_cors.py, and test_asgi_static_files.py exercise the x-api-key check, Origin rejection middleware, and /tasks static-mount protection.
- test_main.py verifies the uvicorn entrypoint wiring by mocking uvicorn.run and asserting host/port/reload/log_level — entrypoint correctness without binding a socket.
- Live provider tests are skipped by default and gated behind MPT_RUN_INTEGRATION_TESTS=1 with real credentials, per test/README.md — so provider adapters (LLM/TTS/material APIs) are largely exercised through mocks.
- Redis-specific behavior (atomic patch_task Lua semantics, stale-queue-entry dropping, state recovery) is covered by test_state.py, test_task_manager.py, and friends that run against the CI Redis service.
- WebUI internals are tested headlessly: ~20 test_webui_*.py files cover settings transfer, task history, log streaming, provider dropdowns, and i18n — but nothing renders an actual browser or end-to-end ffmpeg video in CI. (inferred)
- Lint scope: ruff checks app, cli.py, main.py, webui, test, docs/skill with a per-file ignore for E402 in webui/Main.py (the sys.path insertion must precede app imports).
- Release pipeline is separate: .github/workflows/docker-ghcr.yml builds multi-arch Docker images to ghcr.io/harry0703/moneyprinterturbo on pushes to main, v* tags, and dispatch — tests do not gate image publication beyond sharing the same push.
Risk map
The largest risk is the default-open posture: empty api_key means no authentication while listen_host defaults to 0.0.0.0, exposing task creation, file uploads, and artifact downloads to the LAN. Redis state uses ast.literal_eval on stored values inside a documented trust boundary. Several code paths shell out (ffmpeg probing of uploaded media, the claude_code provider spawning the local claude CLI). Paid text-to-video providers create billable remote tasks — guarded by CLI confirm flags but only a config/api-key check on the API path. The 8k-line webui/Main.py and duplicated requirements.txt are maintainability hotspots.
- Authentication is opt-in: when app.api_key is empty verify_token returns immediately and every /api/v1 route plus /tasks downloads are open; combined with listen_host='0.0.0.0' default, an unconfigured instance is LAN-exposed (the code only logs a warning at startup).
- RedisState._convert_to_original_type applies ast.literal_eval to values read from Redis; the class docstring explicitly documents Redis as a trust boundary — a Redis DB writable by other tenants could inject crafted task records.
- Uploaded media is probed by FFmpeg/ffprobe subprocesses (MATERIAL_VALIDATION_TIMEOUT_SECONDS=120) and PIL for images — attacker-controlled files reach the media toolchain; mitigations are extension whitelist, size caps (200MB video/20MB image), UUID storage names, and filename character sanitization.
- The claude_code LLM provider shells out to the locally installed `claude` CLI; build_claude_code_env scrubs conflicting ANTHROPIC_*/CLAUDE_CODE_USE_* env vars and Docker deployment requires injecting CLAUDE_CODE_OAUTH_TOKEN — a subscription credential — into the container environment.
- Paid material sources (wavespeed, volcengine_seedance, ofox, muapi, metaso_minimax, loomloom) create billable remote tasks; the CLI requires --confirm-*-charge flags and WebUI requires confirmation, but the HTTP API path only checks is_enabled() — an unauthenticated caller could spend money if api_key is unset.
- config.toml persists all provider API keys in plaintext at repo root and is rewritten atomically by save_config on WebUI interactions; config.example.toml warns not to commit it, and logs deliberately avoid printing keys (material._redact_secret, llm._sanitize_error_message).
- webui/Main.py is a single 8,262-line module mixing i18n, settings persistence, task history, upload handling, and generation orchestration — a high blast-radius file where any edit risks regressions across many features.
- Duplicate dependency manifests: requirements.txt mirrors pyproject.toml 'for legacy pip' while Docker builds use requirements.txt and CI uses uv.lock — version drift between the two lists would produce different runtime vs tested environments.
- Dockerfile runs `chmod 777 /MoneyPrinterTurbo` and mounts the whole repo into containers; combined with Streamlit's --server.enableCORS=True in the launch command, container posture is more permissive than the API's own Origin checks.
- VideoParams field defaults (subtitle_position, custom_position, subtitle display/animation) read config.ui at class-definition/import time — a stale or invalid config.toml value is frozen into the pydantic model before validation can run; cli.py documents this and keeps its own fallback constants.
- Cross-post process-liveness detection uses os.kill(pid, 0) on POSIX and a Win32 OpenProcess/GetExitCodeProcess path — best-effort heuristics where a PID reuse could keep a dead task marked busy; foreign-host owners are conservatively treated as alive.
- save_config has an EBUSY fallback that writes config.toml in place when atomic replace fails (Docker bind-mounted single file) — a crash mid-write there could leave a truncated config.
Extension points
The codebase is deliberately registry/dispatch oriented. New LLM providers plug into LLM_PROVIDER_REGISTRY plus (only for non-OpenAI-compatible protocols) an adapter branch in llm._generate_response. New video material sources extend the download_videos dispatch plus the CLI's _CLI_VIDEO_SOURCES and mpt_agent's SUPPORTED_SOURCES. New AI-music providers slot into _VIDEO_MUSIC_PROVIDERS. New TTS engines add is_*_voice detectors and a _single_tts branch. State and queue backends are subclassable, routes extend via new_router, and UI strings via i18n JSON files.
- LLM providers: append an LLMProviderSpec to LLM_PROVIDER_REGISTRY (provider_id, defaults, endpoints, extra_fields); OpenAI-compatible providers need no adapter code — only gemini, qwen, azure, claude_code, litellm have dedicated branches in _generate_response.
- Video material sources: add a branch in material.download_videos plus the source id in cli.py's _CLI_VIDEO_SOURCES, docs/skill/mpt_agent.py's SUPPORTED_SOURCES (kept aligned by comment), and config.example.toml keys; WebUI-specific sources like loomloom bypass download_videos entirely via get_video_materials in task.py.
- AI music providers: register in task.py's _VIDEO_MUSIC_PROVIDERS dict — a service module implementing is_enabled() and generate_bgm() plus error_type/suffix/warning_code/display_name metadata; orchestration, zero-volume short-circuit, and failure degradation are shared.
- TTS engines: add an is_<name>_voice() detector on the voice-name prefix and a dispatch branch in _single_tts; voice lists surface via get_<name>_voices() helpers consumed by the WebUI.
- Subtitle engines: config.app.subtitle_provider selects among 'edge', 'whisper', or empty inside task.generate_subtitle — adding a provider means a new branch there plus a service module like subtitle.py.
- State backends: subclass BaseState (update_task/get_task/get_all_tasks/patch_task/delete_task) and switch the singleton at the bottom of state.py; queue backends subclass TaskManager's create_queue/enqueue/dequeue/is_queue_empty/queue_size.
- API surface: create a router with controllers/v1/base.new_router(dependencies=[Depends(base.verify_token)]) and include it in app/router.py — the pattern automatically yields /api/v1 prefix, V1 tag, and auth.
- Cross-post platforms: extend upload_post_platforms config values plus the _CROSS_POST_SOCIAL_PLATFORMS map in task.py that translates platform ids into LLM social-metadata platform names.
- UI languages: drop a new locale JSON into webui/i18n/ — utils.load_locales scans the directory at startup; locale keys like llm_provider_label.<id> are referenced by the provider registry.
- Config schema: add keys under config.example.toml sections and read them via config.<section>.get(); entirely new sections require adding a dict to save_config's serialization list or they will be dropped on save.
- Pipeline parameters: new VideoParams fields automatically flow into API request models (TaskVideoRequest subclasses VideoParams) and CLI batch manifests (unknown fields rejected, known fields merged), but CLI single-task flags must be added manually to parse_args.
- FFmpeg encoder options are config-driven: app.video_codec accepts libx264/h264_nvenc/h264_amf/h264_qsv/h264_mf/h264_videotoolbox with runtime probing and automatic fallback — new encoders plug into _get_effective_video_codec's probe path.
Contributor handoff
Start at app/services/task.py (_run_pipeline is the shared spine), app/models/schema.py (VideoParams), and app/config/config.py (global config + locking). All three frontends must stay behaviorally identical — comments throughout document past drift bugs where a value list or check existed in only one entrypoint. Tests run via uv + pytest with a 70% branch-coverage floor; ruff lints the same file set as coverage. Preserve the security invariants: path confinement, optional auth, no secrets in logs, and never resurrecting deleted task records.
- Read first: app/services/task.py (pipeline + failure semantics), app/models/schema.py (VideoParams and request models), app/config/config.py (config load/save/locks), cli.py's top constant block, and the relevant section of webui/Main.py before editing any pipeline-adjacent code.
- Test commands: `uv sync --frozen` then `uv run python -X utf8 -m pytest -q test`; coverage parity with CI is `coverage run -m pytest -q test` + `coverage report` (fail_under=70, branch mode); lint with `ruff check app cli.py main.py webui test docs/skill`.
- Keep value lists synchronized across cli.py constants (_CLI_VIDEO_SOURCES, _SUBTITLE_POSITION_VALUES, _CLI_MUSIC_BGM_TYPES), app/models/schema.py enums, app/services/video.py render branches, webui/Main.py dropdowns, and docs/skill/mpt_agent.py SUPPORTED_SOURCES — inline comments explicitly cite past bugs caused by drift.
- Any new file-serving or file-accepting endpoint must go through file_security.resolve_path_within_directory and _sanitize_upload_filename-style normalization — never join user paths directly; the /tasks StaticFiles mount is guarded by middleware, not router dependencies.
- Task state writes must use sm.state.update_task (create/overwrite) vs patch_task (merge-only, safe against deleted tasks) correctly — redis_manager and cross-post code rely on patch_task's Lua atomicity to avoid resurrecting deleted tasks.
- WebUI config writes must use update_config_nonblocking/delete_config_nonblocking/try_save_config — direct config.toml or config.<section> writes bypass the pending-update queue and can deadlock a page against runtime_config_lock held by a running task.
- Never log credentials: material.py's _redact_secret/_redact_request_error, llm.py's _sanitize_error_message, and the asgi lifespan's 'log only that auth is enabled' pattern are deliberate conventions to extend, not bypass.
- Custom audio file paths are intentionally asymmetric: HTTP/WebUI callers are confined to the task directory; only the CLI passes allow_server_file_input=True — do not widen this for API callers.
- VideoParams defaults that read config.ui are frozen at import time; changing [ui] handling must account for the fact that cli.py deliberately duplicates these defaults to keep CLI behavior stable under invalid saved values.
- Edge subtitle failures must NOT auto-fallback to Whisper (would trigger a multi-GB model download); Whisper loads only on explicit subtitle_provider='whisper' — preserve this when touching subtitle logic.
- Code comments are predominantly Chinese (with some English); docstrings and comments carry unusually detailed rationale for past fixes — match this style and keep the explanations when editing. (inferred)
- Dependencies: pyproject.toml is the source of truth pinned by uv.lock, but requirements.txt must be manually kept in sync for Dockerfile/legacy pip builds — check both when bumping.
Languages
| Language | Files | Share of source |
|---|---|---|
| Python | 121 | 97% |
| JSON | 17 | docs/config |
| YAML | 9 | docs/config |
| Markdown | 6 | docs/config |
| TOML | 3 | 2% |
| CSS | 1 | docs/config |
| HTML | 1 | docs/config |
| Shell | 1 | 1% |
Start reading here
Probable source entrypoint: main.py — inferred from the directory tree and manifest, not the upstream docs.
Source roots
app/ (47 files)
Source layout — app/
| Path | Files |
|---|---|
app/config/ | 2 |
app/controllers/ | 8 |
app/models/ | 5 |
app/services/ | 26 |
app/utils/ | 3 |
app/__init__.py | 1 |
app/asgi.py | 1 |
app/router.py | 1 |
Repository structure
| Top-level path | Files |
|---|---|
test/ | 72 |
app/ | 47 |
webui/ | 17 |
docs/ | 12 |
.github/ | 6 |
resource/ | 1 |
Docs found: README.mddocs/skill/SKILL.md
Package
pyproject.toml — moneyprinterturbo v1.3.7, 6 declared dependencies.
###twelvelabstwelvelabs>=1.2.8"]
CI & config entrypoints
README.md— Repository readmemain.py— Python entrypoint.github/workflows/docker-ghcr.yml— GitHub Actions workflow.github/workflows/ci.yml— GitHub Actions workflow
Structural health signals
Average score 6.4/10 across 96 analyzed source files; 45 hotspots surfaced.
app/services/video.py— 1191 lines, score 4.7 (Large file, Deep nesting)app/services/loomloom.py— 979 lines, score 4.7 (Large file, Deep nesting)test/services/test_material_openai_image.py— 810 lines, score 5.2 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
3d5f4e4219272026-09-21 — fix(webui): cache video cache stats as plain dict to stay pickle-safe919170b058312026-09-20 — fix(api): parse subtitle enabled as booleane0c9cb77ffd02026-09-18 — fix(asgi): normalize CORS origins (#1392)fdcf249298dc2026-09-18 — feat(i18n): add Catalan WebUI support26edbbd9d9f22026-09-17 — fix(llm): replace the retired Groq default model with openai/gpt-oss-120b