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