diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..c8cfe395 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/Makefile b/Makefile index 9881d360..aeb48600 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,73 @@ -PYTHON ?= python3 +PYTHON_VERSION ?= 3.10 +VENV_DIR ?= .venv +BASE_PYTHON ?= $(shell command -v python$(PYTHON_VERSION) 2>/dev/null || command -v python3 2>/dev/null || command -v python 2>/dev/null) +RUN_PYTHON = $(shell for dir in "$$VIRTUAL_ENV" "$$CONDA_PREFIX" "$(VENV_DIR)"; do if [ -n "$$dir" ] && [ -x "$$dir/bin/python" ]; then printf "%s/bin/python" "$$dir"; exit 0; elif [ -n "$$dir" ] && [ -x "$$dir/Scripts/python.exe" ]; then printf "%s/Scripts/python.exe" "$$dir"; exit 0; fi; done; if [ "$(OS)" = "Windows_NT" ]; then printf "%s/Scripts/python.exe" "$(VENV_DIR)"; else printf "%s/bin/python" "$(VENV_DIR)"; fi) +PIP = $(RUN_PYTHON) -m pip -.PHONY: setup install install-dev install-gpu test test-contracts lint clean preflight demo demo-list hyperframes-doctor hyperframes-warm +.DEFAULT_GOAL := setup + +.PHONY: setup install install-dev install-gpu test test-contracts lint clean preflight demo demo-list hyperframes-doctor hyperframes-warm venv ensure-venv + +# ---- Virtual environment ---- + +ensure-venv: + @if [ -n "$$VIRTUAL_ENV" ] && { [ -x "$$VIRTUAL_ENV/bin/python" ] || [ -x "$$VIRTUAL_ENV/Scripts/python.exe" ]; }; then \ + echo "==> Using active virtual environment: $$VIRTUAL_ENV"; \ + elif [ -n "$$CONDA_PREFIX" ] && { [ -x "$$CONDA_PREFIX/bin/python" ] || [ -x "$$CONDA_PREFIX/Scripts/python.exe" ]; }; then \ + echo "==> Using active conda environment: $$CONDA_PREFIX"; \ + elif [ -x "$(VENV_DIR)/bin/python" ] || [ -x "$(VENV_DIR)/Scripts/python.exe" ]; then \ + echo "==> Using existing virtual environment: $(VENV_DIR)"; \ + elif command -v uv >/dev/null 2>&1; then \ + echo "==> Creating virtual environment with uv (Python $(PYTHON_VERSION)+): $(VENV_DIR)"; \ + uv venv --python $(PYTHON_VERSION) "$(VENV_DIR)"; \ + else \ + if [ -z "$(BASE_PYTHON)" ]; then \ + echo "ERROR: Python $(PYTHON_VERSION)+ is required, but no python executable was found."; \ + exit 1; \ + fi; \ + "$(BASE_PYTHON)" -c "import sys; required=tuple(map(int, '$(PYTHON_VERSION)'.split('.')[:2])); raise SystemExit(0 if sys.version_info[:2] >= required else 1)" || { \ + echo "ERROR: OpenMontage requires Python $(PYTHON_VERSION)+."; \ + echo "Install uv or Python $(PYTHON_VERSION)+, then run make again."; \ + exit 1; \ + }; \ + echo "==> Creating virtual environment with Python venv: $(VENV_DIR)"; \ + "$(BASE_PYTHON)" -m venv "$(VENV_DIR)" || { \ + echo "ERROR: Could not create $(VENV_DIR). Install uv or ensure python venv support is available."; \ + exit 1; \ + }; \ + fi + @$(RUN_PYTHON) -c "import sys; required=tuple(map(int, '$(PYTHON_VERSION)'.split('.')[:2])); raise SystemExit(0 if sys.version_info[:2] >= required else 1)" || { \ + echo "ERROR: OpenMontage requires Python $(PYTHON_VERSION)+."; \ + echo "Current interpreter is $$($(RUN_PYTHON) -c 'import sys; print(\".\".join(map(str, sys.version_info[:3])))' 2>/dev/null || echo unavailable): $(RUN_PYTHON)"; \ + echo "Activate a compatible environment or remove it so make can create $(VENV_DIR)."; \ + exit 1; \ + } + @$(RUN_PYTHON) -m pip --version >/dev/null 2>&1 || $(RUN_PYTHON) -m ensurepip --upgrade >/dev/null + +venv: ensure-venv + @echo "==> Virtual environment ready." + @echo " Python: $(RUN_PYTHON)" + @if [ -z "$$VIRTUAL_ENV" ] && [ -z "$$CONDA_PREFIX" ]; then if [ "$(OS)" = "Windows_NT" ]; then echo " Activate with: $(VENV_DIR)\\Scripts\\Activate.ps1"; else echo " Activate with: source $(VENV_DIR)/bin/activate"; fi; fi # ---- One-command setup ---- -setup: +setup: ensure-venv @echo "==> Installing Python dependencies..." - $(PYTHON) -m pip install -r requirements.txt + $(PIP) install -r requirements.txt @echo "" @echo "==> Installing Remotion composer..." cd remotion-composer && npm install @echo "" @echo "==> Installing free offline TTS (Piper)..." - $(PYTHON) -m pip install piper-tts || echo " [skip] piper-tts install failed — TTS will use cloud providers instead" + $(PIP) install piper-tts || echo " [skip] piper-tts install failed — TTS will use cloud providers instead" @echo "" @echo "==> Installing HyperFrames runtime (cache-warm via npx)..." @echo " Pulls the 'hyperframes' npm package into the local npx cache so the" @echo " first render doesn't pay a 30-60s cold-fetch penalty. ~20MB of disk." @npx --yes hyperframes --version >/dev/null 2>&1 && echo " HyperFrames CLI cached (npx)" || echo " [skip] HyperFrames cache-warm failed — offline or npm unavailable; first render will fetch on demand" - @$(PYTHON) -c "from tools.video.hyperframes_compose import HyperFramesCompose; HyperFramesCompose._npm_resolve_cache=None; c=HyperFramesCompose()._runtime_check(); print(f' HyperFrames runtime_available={c[\"runtime_available\"]}, npm={c.get(\"npm_package_version\") or c.get(\"npm_resolve_error\")}'); [print(f' note: {r}') for r in c['reasons']]" || echo " [skip] HyperFrames check failed — runtime can be set up later" + @$(RUN_PYTHON) -c "from tools.video.hyperframes_compose import HyperFramesCompose; HyperFramesCompose._npm_resolve_cache=None; c=HyperFramesCompose()._runtime_check(); print(f' HyperFrames runtime_available={c[\"runtime_available\"]}, npm={c.get(\"npm_package_version\") or c.get(\"npm_resolve_error\")}'); [print(f' note: {r}') for r in c['reasons']]" || echo " [skip] HyperFrames check failed — runtime can be set up later" @echo "" - $(PYTHON) -c "import shutil, os; e=os.path.exists('.env'); shutil.copy('.env.example','.env') if not e else None; print('==> Created .env from .env.example — add your API keys there.' if not e else '==> .env already exists — skipping.')" + $(RUN_PYTHON) -c "import shutil, os; e=os.path.exists('.env'); shutil.copy('.env.example','.env') if not e else None; print('==> Created .env from .env.example — add your API keys there.' if not e else '==> .env already exists — skipping.')" @echo "" @echo "Done! Open this project in your AI coding assistant and start creating." @echo " Optional: add API keys to .env to unlock cloud providers." @@ -30,32 +77,32 @@ setup: # ---- Individual installs ---- -install: - $(PYTHON) -m pip install -r requirements.txt +install: ensure-venv + $(PIP) install -r requirements.txt -install-dev: - $(PYTHON) -m pip install -r requirements-dev.txt +install-dev: ensure-venv + $(PIP) install -r requirements-dev.txt -install-gpu: - $(PYTHON) -m pip install -r requirements-gpu.txt - $(PYTHON) -m pip install diffusers transformers accelerate +install-gpu: ensure-venv + $(PIP) install -r requirements-gpu.txt + $(PIP) install diffusers transformers accelerate # ---- Testing ---- -test: - $(PYTHON) -m pytest tests/ -v +test: ensure-venv + $(RUN_PYTHON) -m pytest tests/ -v -test-contracts: - $(PYTHON) -m pytest tests/contracts/ -v +test-contracts: ensure-venv + $(RUN_PYTHON) -m pytest tests/contracts/ -v # ---- Utilities ---- -preflight: - $(PYTHON) -c "from tools.tool_registry import registry; import json; registry.discover(); print(json.dumps(registry.provider_menu(), indent=2))" +preflight: ensure-venv + $(RUN_PYTHON) -c "from tools.tool_registry import registry; import json; registry.discover(); print(json.dumps(registry.provider_menu(), indent=2))" -hyperframes-doctor: +hyperframes-doctor: ensure-venv @echo "==> Probing HyperFrames runtime (node/ffmpeg/npx + hyperframes doctor)..." - $(PYTHON) -c "from tools.video.hyperframes_compose import HyperFramesCompose; r=HyperFramesCompose().execute({'operation':'doctor'}); import json; print(json.dumps(r.data, indent=2)); print('OK' if r.success else f'FAIL: {r.error}')" + $(RUN_PYTHON) -c "from tools.video.hyperframes_compose import HyperFramesCompose; r=HyperFramesCompose().execute({'operation':'doctor'}); import json; print(json.dumps(r.data, indent=2)); print('OK' if r.success else f'FAIL: {r.error}')" hyperframes-warm: @echo "==> Refreshing the HyperFrames npx cache to latest..." @@ -63,20 +110,20 @@ hyperframes-warm: npx --yes --prefer-online hyperframes --version @echo "==> Cache warm complete." -demo: +demo: ensure-venv @echo "==> Rendering zero-key demo videos (no API keys needed)..." @echo " These use only Remotion components — animated charts, text, data viz." @echo "" - $(PYTHON) render_demo.py + $(RUN_PYTHON) render_demo.py -demo-list: - @$(PYTHON) render_demo.py --list +demo-list: ensure-venv + $(RUN_PYTHON) render_demo.py --list -lint: - $(PYTHON) -m py_compile tools/base_tool.py - $(PYTHON) -m py_compile tools/tool_registry.py - $(PYTHON) -m py_compile tools/cost_tracker.py - $(PYTHON) -m py_compile tools/analysis/composition_validator.py +lint: ensure-venv + $(RUN_PYTHON) -m py_compile tools/base_tool.py + $(RUN_PYTHON) -m py_compile tools/tool_registry.py + $(RUN_PYTHON) -m py_compile tools/cost_tracker.py + $(RUN_PYTHON) -m py_compile tools/analysis/composition_validator.py clean: - $(PYTHON) -c "import pathlib, shutil; [shutil.rmtree(p) for p in pathlib.Path('.').rglob('__pycache__')]; [p.unlink() for p in pathlib.Path('.').rglob('*.pyc')]" + $(BASE_PYTHON) -c "import pathlib, shutil; excluded=[pathlib.Path('$(VENV_DIR)'), pathlib.Path('venv')]; skip=lambda p: any(p == root or root in p.parents for root in excluded); roots=[p for p in pathlib.Path('.').rglob('__pycache__') if not skip(p)]; [shutil.rmtree(p) for p in roots]; files=[p for p in pathlib.Path('.').rglob('*.pyc') if not skip(p)]; [p.unlink() for p in files]" diff --git a/README.md b/README.md index 0801102f..168f4ca2 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,9 @@ Or if you want the real-footage path: That's it. The agent researches your topic with live web search, generates AI images, writes and narrates the script with voice direction, finds royalty-free background music automatically, burns in word-level subtitles, and renders the final video. Before you see anything, the system runs a multi-point self-review — ffprobe validation, frame sampling, audio level analysis, delivery promise verification, and subtitle checks. Every provider selection is scored across 7 dimensions with an auditable decision log. Every creative decision gets your approval. -> **No `make`?** Run manually: `pip install -r requirements.txt && cd remotion-composer && npm install && cd .. && pip install piper-tts && cp .env.example .env` +> **No `make`?** macOS/Linux: `python3 -m venv .venv && source .venv/bin/activate && python -m pip install -r requirements.txt && cd remotion-composer && npm install && cd .. && python -m pip install piper-tts && cp .env.example .env` +> +> Windows PowerShell: `py -3 -m venv .venv; .\.venv\Scripts\Activate.ps1; python -m pip install -r requirements.txt; cd remotion-composer; npm install; cd ..; python -m pip install piper-tts; Copy-Item .env.example .env` > > **Windows:** If `npm install` fails with `ERR_INVALID_ARG_TYPE`, use `npx --yes npm install` instead. diff --git a/README_zh-CN.md b/README_zh-CN.md index f119fcbd..e962b0c3 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -136,7 +136,9 @@ make setup 就是这么简单。智能体会通过实时网络搜索研究您的主题,生成 AI 图像,撰写并配音带有语音指导的脚本,自动寻找免版税的背景音乐,烧录词级字幕,并渲染最终视频。在您看到任何内容之前,系统会运行多点自我审查——ffprobe 验证、帧采样、音频电平分析、交付承诺验证以及字幕检查。每一个提供商的选择都会在 7 个维度上进行评分,并附有可审计的决策日志。每一个创意决定都需要您的批准。 -> **没有 `make`?** 可手动运行:`pip install -r requirements.txt && cd remotion-composer && npm install && cd .. && pip install piper-tts && cp .env.example .env` +> **没有 `make`?** macOS/Linux:`python3 -m venv .venv && source .venv/bin/activate && python -m pip install -r requirements.txt && cd remotion-composer && npm install && cd .. && python -m pip install piper-tts && cp .env.example .env` +> +> Windows PowerShell:`py -3 -m venv .venv; .\.venv\Scripts\Activate.ps1; python -m pip install -r requirements.txt; cd remotion-composer; npm install; cd ..; python -m pip install piper-tts; Copy-Item .env.example .env` > > **Windows:** 如果 `npm install` 报错 `ERR_INVALID_ARG_TYPE`,请改用 `npx --yes npm install`。