From 4d8c393a94d98d0944e89de0809f706a04fcc484 Mon Sep 17 00:00:00 2001 From: Hong Phuc Date: Fri, 24 Apr 2026 01:18:12 +0700 Subject: [PATCH] Make the app runnable in Docker for GUI and CLI Build one shared container image for the Flask GUI and CLI pipeline, with Playwright, FFmpeg, and spaCy preinstalled so first runs are reliable. Add bootstrap logic for missing runtime files, bind the GUI to 0.0.0.0 in containers, and preserve state through a repo mount. Constraint: Local development needs a single image that supports both entrypoints without introducing extra services or dependencies. Rejected: Separate GUI and CLI images | duplicated maintenance and no runtime benefit for this repo. Confidence: high Scope-risk: moderate Directive: Keep runtime state creation in the container bootstrap layer; do not reintroduce host-specific assumptions into GUI startup. Tested: docker compose build; docker compose run --rm gui python -c '...'; docker compose run --rm cli python -c 'import main'; docker compose up -d gui; curl -I http://localhost:4000 Not-tested: Full end-to-end video generation with live credentials in this environment. --- .dockerignore | 22 ++++++++++-- .gitignore | 4 +++ Dockerfile | 31 ++++++++++++----- GUI.py | 32 ++++++++++-------- README.md | 39 ++++++++++++++++++++++ build.sh | 3 +- docker-compose.yml | 27 +++++++++++++++ docker-entrypoint.sh | 6 ++++ run.sh | 3 +- utils/cleanup.py | 2 +- utils/docker_bootstrap.py | 70 +++++++++++++++++++++++++++++++++++++++ utils/videos.py | 4 +-- 12 files changed, 215 insertions(+), 28 deletions(-) create mode 100644 docker-compose.yml create mode 100644 docker-entrypoint.sh create mode 100644 utils/docker_bootstrap.py diff --git a/.dockerignore b/.dockerignore index 1653ff2..35c18e0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,20 @@ -Dockerfile -results \ No newline at end of file +.git +.github +.omx +.venv +venv +__pycache__ +*.pyc +*.pyo +*.pyd +.pytest_cache +.mypy_cache +.ruff_cache +.DS_Store +config.toml +results +assets/temp +assets/backgrounds +video_creation/data/videos.json +video_creation/data/cookie-threads.json +out diff --git a/.gitignore b/.gitignore index cc6bd18..8385448 100644 --- a/.gitignore +++ b/.gitignore @@ -242,7 +242,11 @@ reddit-bot-351418-5560ebc49cac.json /.idea *.pyc video_creation/data/videos.json +video_creation/data/cookie-threads.json video_creation/data/envvars.txt +utils/backgrounds.json config.toml *.exe + +.omx diff --git a/Dockerfile b/Dockerfile index 3f53ada..5a41218 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,27 @@ -FROM python:3.10.14-slim +FROM python:3.10-slim-bookworm -RUN apt update -RUN apt-get install -y ffmpeg -RUN apt install python3-pip -y +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PLAYWRIGHT_BROWSERS_PATH=/ms-playwright -RUN mkdir /app -ADD . /app WORKDIR /app -RUN pip install -r requirements.txt -CMD ["python3", "main.py"] +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ffmpeg \ + curl \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt ./ +RUN pip install --upgrade pip \ + && pip install -r requirements.txt \ + && python -m spacy download en_core_web_sm + +RUN python -m playwright install --with-deps chromium + +COPY . . + +RUN chmod +x /app/docker-entrypoint.sh + +ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"] diff --git a/GUI.py b/GUI.py index 4588083..771b9e5 100644 --- a/GUI.py +++ b/GUI.py @@ -1,8 +1,9 @@ -import webbrowser -from pathlib import Path +import os +import webbrowser +from pathlib import Path # Used "tomlkit" instead of "toml" because it doesn't change formatting on "dump" -import tomlkit +import tomlkit from flask import ( Flask, redirect, @@ -12,12 +13,16 @@ from flask import ( url_for, ) -import utils.gui_utils as gui - -# Set the hostname -HOST = "localhost" -# Set the port number -PORT = 4000 +import utils.gui_utils as gui +from utils.docker_bootstrap import ensure_runtime_state + +ensure_runtime_state() + +# Set the hostname and port +HOST = os.environ.get("GUI_HOST", "0.0.0.0") +PORT = int(os.environ.get("GUI_PORT", "4000")) +OPEN_BROWSER = os.environ.get("GUI_OPEN_BROWSER", "1").lower() in {"1", "true", "yes", "on"} +BROWSER_URL = os.environ.get("GUI_BROWSER_URL", f"http://localhost:{PORT}") # Configure application app = Flask(__name__, template_folder="GUI") @@ -110,7 +115,8 @@ def voices(name): # Run browser and start the app -if __name__ == "__main__": - webbrowser.open(f"http://{HOST}:{PORT}", new=2) - print("Website opened in new tab. Refresh if it didn't load.") - app.run(port=PORT) +if __name__ == "__main__": + if OPEN_BROWSER: + webbrowser.open(BROWSER_URL, new=2) + print("Website opened in new tab. Refresh if it didn't load.") + app.run(host=HOST, port=PORT) diff --git a/README.md b/README.md index 8042755..57b99e0 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ The only original thing being done is the editing and gathering of all materials - Python 3.10 - Playwright (this should install automatically in installation) +- Docker and Docker Compose for the container workflow ## Installation 👩‍💻 @@ -66,6 +67,44 @@ The only original thing being done is the editing and gathering of all materials python -m playwright install-deps ``` +## Docker + +The repository now includes a shared image plus Compose services for the GUI and CLI. + +Build the image: + +```sh +docker compose build +``` + +Start the GUI: + +```sh +docker compose up gui +``` + +Open `http://localhost:4000` in your browser. + +Run the CLI pipeline: + +```sh +docker compose run --rm cli +``` + +Run the CLI for a specific post: + +```sh +docker compose run --rm cli python main.py +``` + +Stop the GUI and remove the Compose stack: + +```sh +docker compose down +``` + +The repo root is bind-mounted into the container so `config.toml`, `results/`, `assets/temp/`, and the runtime JSON files persist across rebuilds and repeated runs. + --- **EXPERIMENTAL!!!!** diff --git a/build.sh b/build.sh index 45ebd33..3f33f83 100755 --- a/build.sh +++ b/build.sh @@ -1,2 +1,3 @@ #!/bin/sh -docker build -t rvmt . +set -eu +docker compose build diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..46743eb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +services: + gui: + build: + context: . + image: videomakerbot:latest + command: ["python", "GUI.py"] + ports: + - "4000:4000" + environment: + GUI_HOST: "0.0.0.0" + GUI_PORT: "4000" + GUI_OPEN_BROWSER: "0" + GUI_BROWSER_URL: "http://localhost:4000" + volumes: + - ./:/app + shm_size: "1gb" + + cli: + build: + context: . + image: videomakerbot:latest + command: ["python", "main.py"] + environment: + PYTHONUNBUFFERED: "1" + volumes: + - ./:/app + shm_size: "1gb" diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..8b2a81a --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -eu + +python -m utils.docker_bootstrap + +exec "$@" diff --git a/run.sh b/run.sh index 1769e21..4fd95b6 100755 --- a/run.sh +++ b/run.sh @@ -1,2 +1,3 @@ #!/bin/sh -docker run -v $(pwd)/out/:/app/assets -v $(pwd)/.env:/app/.env -it rvmt +set -eu +docker compose run --rm cli "$@" diff --git a/utils/cleanup.py b/utils/cleanup.py index 8c73b15..449eca3 100644 --- a/utils/cleanup.py +++ b/utils/cleanup.py @@ -13,7 +13,7 @@ def cleanup(reddit_id) -> int: Returns: int: How many files were deleted """ - directory = f"../assets/temp/{reddit_id}/" + directory = f"assets/temp/{reddit_id}/" if exists(directory): shutil.rmtree(directory) diff --git a/utils/docker_bootstrap.py b/utils/docker_bootstrap.py new file mode 100644 index 0000000..9c3f326 --- /dev/null +++ b/utils/docker_bootstrap.py @@ -0,0 +1,70 @@ +"""Container bootstrap helpers for first-run runtime state.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Dict + +import tomlkit + + +ROOT = Path(__file__).resolve().parent.parent + + +def _default_from_template(node: Dict[str, Any]) -> Dict[str, Any]: + defaults: Dict[str, Any] = {} + for key, value in node.items(): + if isinstance(value, dict) and "optional" in value: + if "default" in value: + defaults[key] = value["default"] + else: + value_type = value.get("type") + if value_type == "bool": + defaults[key] = False + elif value_type in {"int", "float"}: + defaults[key] = 0 + else: + defaults[key] = "" + elif isinstance(value, dict): + defaults[key] = _default_from_template(value) + return defaults + + +def _ensure_json(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + if not path.exists(): + path.write_text(content, encoding="utf-8") + + +def _ensure_config(path: Path) -> None: + if path.exists(): + return + + template_path = ROOT / "utils/.config.template.toml" + template = tomlkit.loads(template_path.read_text(encoding="utf-8")) + defaults = _default_from_template(template) + path.write_text(tomlkit.dumps(defaults), encoding="utf-8") + + +def ensure_runtime_state() -> None: + """Create runtime files and directories expected by the app.""" + for relative in ( + "assets/temp", + "assets/backgrounds/audio", + "assets/backgrounds/video", + "results", + "video_creation/data", + ): + (ROOT / relative).mkdir(parents=True, exist_ok=True) + + _ensure_config(ROOT / "config.toml") + _ensure_json(ROOT / "video_creation/data/videos.json", "[]\n") + _ensure_json(ROOT / "utils/backgrounds.json", "{}\n") + + +def main() -> None: + ensure_runtime_state() + + +if __name__ == "__main__": + main() diff --git a/utils/videos.py b/utils/videos.py index 481c4c8..b352968 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -10,8 +10,8 @@ if TYPE_CHECKING: def check_done( - redditobj: Submission, -) -> Submission: + redditobj: "Submission", +) -> "Submission": # don't set this to be run anyplace that isn't subreddit.py bc of inspect stack """Checks if the chosen post has already been generated