forked from LiveCarta/ContentGeneration
Refactor src layout and add logging lifecycle + tests
This commit is contained in:
@@ -43,7 +43,7 @@ docker run --rm --gpus all --env-file .env -v "$(pwd)":/app -w /app content-gene
|
|||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
- `run_video_pipeline.py`: main entrypoint.
|
- `run_video_pipeline.py`: main entrypoint.
|
||||||
- `src/scripts/`: helper scripts used by the pipeline.
|
- `src/`: helper scripts used by the pipeline.
|
||||||
- `HunyuanVideo-1.5/`: Hunyuan inference code and model dependencies.
|
- `HunyuanVideo-1.5/`: Hunyuan inference code and model dependencies.
|
||||||
- `reel_script.json`: required script input with `shots`.
|
- `reel_script.json`: required script input with `shots`.
|
||||||
- `images/`, `audios/`, `videos/`, `merged/`, `results/`: working/output folders.
|
- `images/`, `audios/`, `videos/`, `merged/`, `results/`: working/output folders.
|
||||||
@@ -198,5 +198,5 @@ docker run --rm --gpus all \
|
|||||||
8. Verify syntax quickly before running.
|
8. Verify syntax quickly before running.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m py_compile run_video_pipeline.py src/scripts/*.py
|
python3 -m py_compile run_video_pipeline.py src/*.py
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from src.scripts.logging_config import configure_logging
|
from src.logging_config import configure_logging, debug_log_lifecycle
|
||||||
from src.scripts.s3_video_storage import S3VideoStorage
|
from src.s3_video_storage import S3VideoStorage
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||||
SCRIPT_DIR = PROJECT_ROOT / "src" / "scripts"
|
SCRIPT_DIR = PROJECT_ROOT / "src"
|
||||||
DEFAULT_BASE_DIR = PROJECT_ROOT
|
DEFAULT_BASE_DIR = PROJECT_ROOT
|
||||||
DEFAULT_HUNYUAN_DIR = DEFAULT_BASE_DIR / "HunyuanVideo-1.5"
|
DEFAULT_HUNYUAN_DIR = DEFAULT_BASE_DIR / "HunyuanVideo-1.5"
|
||||||
DEFAULT_REEL_SCRIPT = DEFAULT_BASE_DIR / "reel_script.json"
|
DEFAULT_REEL_SCRIPT = DEFAULT_BASE_DIR / "reel_script.json"
|
||||||
@@ -43,10 +43,15 @@ def parse_args() -> argparse.Namespace:
|
|||||||
parser.add_argument("--skip-merge", action="store_true")
|
parser.add_argument("--skip-merge", action="store_true")
|
||||||
parser.add_argument("--skip-concat", action="store_true")
|
parser.add_argument("--skip-concat", action="store_true")
|
||||||
parser.add_argument("--skip-s3-upload", action="store_true")
|
parser.add_argument("--skip-s3-upload", action="store_true")
|
||||||
parser.add_argument("--log-level", default="INFO")
|
parser.add_argument(
|
||||||
|
"--log-level",
|
||||||
|
default=None,
|
||||||
|
help="Logging level (overrides LOG_LEVEL env var)",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def run_step(name: str, cmd: list[str], cwd: Path | None = None) -> None:
|
def run_step(name: str, cmd: list[str], cwd: Path | None = None) -> None:
|
||||||
LOGGER.info("=== %s ===", name)
|
LOGGER.info("=== %s ===", name)
|
||||||
LOGGER.info("$ %s", " ".join(str(part) for part in cmd))
|
LOGGER.info("$ %s", " ".join(str(part) for part in cmd))
|
||||||
@@ -55,6 +60,13 @@ def run_step(name: str, cmd: list[str], cwd: Path | None = None) -> None:
|
|||||||
subprocess.run(cmd, check=True, cwd=str(cwd) if cwd else None)
|
subprocess.run(cmd, check=True, cwd=str(cwd) if cwd else None)
|
||||||
|
|
||||||
|
|
||||||
|
def _with_log_level(cmd: list[str], log_level: str | None) -> list[str]:
|
||||||
|
if not log_level:
|
||||||
|
return cmd
|
||||||
|
return [*cmd, "--log-level", log_level]
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def maybe_upload_to_s3(output_path: Path) -> None:
|
def maybe_upload_to_s3(output_path: Path) -> None:
|
||||||
bucket = os.getenv("AWS_S3_BUCKET")
|
bucket = os.getenv("AWS_S3_BUCKET")
|
||||||
if not bucket:
|
if not bucket:
|
||||||
@@ -75,6 +87,7 @@ def maybe_upload_to_s3(output_path: Path) -> None:
|
|||||||
LOGGER.info("Uploaded output to %s", s3_uri)
|
LOGGER.info("Uploaded output to %s", s3_uri)
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
configure_logging(args.log_level)
|
configure_logging(args.log_level)
|
||||||
@@ -100,10 +113,10 @@ def main() -> int:
|
|||||||
if not args.skip_generate and not args.reel_script.exists():
|
if not args.skip_generate and not args.reel_script.exists():
|
||||||
run_step(
|
run_step(
|
||||||
"Generate Reel Script",
|
"Generate Reel Script",
|
||||||
[
|
_with_log_level([
|
||||||
sys.executable,
|
sys.executable,
|
||||||
str(SCRIPT_DIR / "generate_script.py"),
|
str(SCRIPT_DIR / "generate_script.py"),
|
||||||
],
|
], args.log_level),
|
||||||
cwd=args.base_dir,
|
cwd=args.base_dir,
|
||||||
)
|
)
|
||||||
if not args.reel_script.exists():
|
if not args.reel_script.exists():
|
||||||
@@ -113,7 +126,7 @@ def main() -> int:
|
|||||||
if not args.skip_generate:
|
if not args.skip_generate:
|
||||||
run_step(
|
run_step(
|
||||||
"Generate Videos",
|
"Generate Videos",
|
||||||
[
|
_with_log_level([
|
||||||
sys.executable,
|
sys.executable,
|
||||||
str(SCRIPT_DIR / "generate_videos.py"),
|
str(SCRIPT_DIR / "generate_videos.py"),
|
||||||
"--hunyuan-dir",
|
"--hunyuan-dir",
|
||||||
@@ -128,13 +141,13 @@ def main() -> int:
|
|||||||
str(args.audios_dir),
|
str(args.audios_dir),
|
||||||
"--seed",
|
"--seed",
|
||||||
str(args.seed),
|
str(args.seed),
|
||||||
],
|
], args.log_level),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not args.skip_merge:
|
if not args.skip_merge:
|
||||||
run_step(
|
run_step(
|
||||||
"Merge Audio + Video",
|
"Merge Audio + Video",
|
||||||
[
|
_with_log_level([
|
||||||
sys.executable,
|
sys.executable,
|
||||||
str(SCRIPT_DIR / "merge_audio_video.py"),
|
str(SCRIPT_DIR / "merge_audio_video.py"),
|
||||||
"--videos-dir",
|
"--videos-dir",
|
||||||
@@ -143,20 +156,20 @@ def main() -> int:
|
|||||||
str(args.audios_dir),
|
str(args.audios_dir),
|
||||||
"--output-dir",
|
"--output-dir",
|
||||||
str(args.merged_dir),
|
str(args.merged_dir),
|
||||||
],
|
], args.log_level),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not args.skip_concat:
|
if not args.skip_concat:
|
||||||
run_step(
|
run_step(
|
||||||
"Concatenate Merged Videos",
|
"Concatenate Merged Videos",
|
||||||
[
|
_with_log_level([
|
||||||
sys.executable,
|
sys.executable,
|
||||||
str(SCRIPT_DIR / "concat_merged.py"),
|
str(SCRIPT_DIR / "concat_merged.py"),
|
||||||
"--merged-dir",
|
"--merged-dir",
|
||||||
str(args.merged_dir),
|
str(args.merged_dir),
|
||||||
"--output",
|
"--output",
|
||||||
str(args.output),
|
str(args.output),
|
||||||
],
|
], args.log_level),
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError as exc:
|
except subprocess.CalledProcessError as exc:
|
||||||
LOGGER.exception("Pipeline failed at command: %s", exc.cmd)
|
LOGGER.exception("Pipeline failed at command: %s", exc.cmd)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from logging_config import configure_logging
|
from logging_config import configure_logging, debug_log_lifecycle
|
||||||
|
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
@@ -30,10 +30,15 @@ def parse_args() -> argparse.Namespace:
|
|||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--merged-dir", type=Path, default=DEFAULT_MERGED_DIR)
|
parser.add_argument("--merged-dir", type=Path, default=DEFAULT_MERGED_DIR)
|
||||||
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
||||||
parser.add_argument("--log-level", default="INFO")
|
parser.add_argument(
|
||||||
|
"--log-level",
|
||||||
|
default=None,
|
||||||
|
help="Logging level (overrides LOG_LEVEL env var)",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
configure_logging(args.log_level)
|
configure_logging(args.log_level)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -7,7 +8,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from elevenlabs.client import ElevenLabs
|
from elevenlabs.client import ElevenLabs
|
||||||
from logging_config import configure_logging
|
from logging_config import configure_logging, debug_log_lifecycle
|
||||||
|
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
@@ -18,8 +19,20 @@ load_dotenv(PROJECT_ROOT / ".env")
|
|||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log-level",
|
||||||
|
default=None,
|
||||||
|
help="Logging level (overrides LOG_LEVEL env var)",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
configure_logging("INFO")
|
args = parse_args()
|
||||||
|
configure_logging(args.log_level)
|
||||||
api_key = os.getenv("ELEVENLABS_API_KEY")
|
api_key = os.getenv("ELEVENLABS_API_KEY")
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise RuntimeError("ELEVENLABS_API_KEY is not set")
|
raise RuntimeError("ELEVENLABS_API_KEY is not set")
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from diffusers import FluxPipeline
|
from diffusers import FluxPipeline
|
||||||
from logging_config import configure_logging
|
from logging_config import configure_logging, debug_log_lifecycle
|
||||||
|
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
@@ -15,8 +16,20 @@ PROJECT_ROOT = SCRIPT_DIR.parents[1]
|
|||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log-level",
|
||||||
|
default=None,
|
||||||
|
help="Logging level (overrides LOG_LEVEL env var)",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
configure_logging("INFO")
|
args = parse_args()
|
||||||
|
configure_logging(args.log_level)
|
||||||
reel_script = PROJECT_ROOT / "reel_script.json"
|
reel_script = PROJECT_ROOT / "reel_script.json"
|
||||||
images_dir = PROJECT_ROOT / "images"
|
images_dir = PROJECT_ROOT / "images"
|
||||||
images_dir.mkdir(parents=True, exist_ok=True)
|
images_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import argparse
|
||||||
import torch
|
import torch
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -5,7 +6,7 @@ from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
|
|||||||
import re
|
import re
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from logging_config import configure_logging
|
from logging_config import configure_logging, debug_log_lifecycle
|
||||||
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
@@ -19,6 +20,17 @@ MAX_VOICEOVER_WORDS = int(MAX_VOICEOVER_SECONDS * WORDS_PER_SECOND)
|
|||||||
MIN_VOICEOVER_WORDS = 5
|
MIN_VOICEOVER_WORDS = 5
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log-level",
|
||||||
|
default=None,
|
||||||
|
help="Logging level (overrides LOG_LEVEL env var)",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def load_model(model_id: str = MODEL_ID):
|
def load_model(model_id: str = MODEL_ID):
|
||||||
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
|
||||||
bnb_config = BitsAndBytesConfig(
|
bnb_config = BitsAndBytesConfig(
|
||||||
@@ -37,6 +49,7 @@ def load_model(model_id: str = MODEL_ID):
|
|||||||
return model, tokenizer
|
return model, tokenizer
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def generate_reel_scenario(
|
def generate_reel_scenario(
|
||||||
model,
|
model,
|
||||||
tokenizer,
|
tokenizer,
|
||||||
@@ -231,6 +244,7 @@ def extract_field(label: str, next_label: Optional[str], text: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def parse_reel_scenario(raw_scenario: str) -> dict:
|
def parse_reel_scenario(raw_scenario: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Parse the shot-by-shot reel scenario into a structured dict.
|
Parse the shot-by-shot reel scenario into a structured dict.
|
||||||
@@ -336,7 +350,8 @@ def parse_reel_scenario(raw_scenario: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
configure_logging("INFO")
|
args = parse_args()
|
||||||
|
configure_logging(args.log_level)
|
||||||
|
|
||||||
with open("topic_description.txt", "r") as f:
|
with open("topic_description.txt", "r") as f:
|
||||||
topic = f.read()
|
topic = f.read()
|
||||||
@@ -10,7 +10,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from logging_config import configure_logging
|
from logging_config import configure_logging, debug_log_lifecycle
|
||||||
|
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
@@ -32,10 +32,15 @@ def parse_args() -> argparse.Namespace:
|
|||||||
parser.add_argument("--videos-dir", type=Path, default=DEFAULT_VIDEOS_DIR)
|
parser.add_argument("--videos-dir", type=Path, default=DEFAULT_VIDEOS_DIR)
|
||||||
parser.add_argument("--audios-dir", type=Path, default=DEFAULT_AUDIOS_DIR)
|
parser.add_argument("--audios-dir", type=Path, default=DEFAULT_AUDIOS_DIR)
|
||||||
parser.add_argument("--seed", type=int, default=1)
|
parser.add_argument("--seed", type=int, default=1)
|
||||||
parser.add_argument("--log-level", default="INFO")
|
parser.add_argument(
|
||||||
|
"--log-level",
|
||||||
|
default=None,
|
||||||
|
help="Logging level (overrides LOG_LEVEL env var)",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def get_audio_duration(audio_path: Path) -> float:
|
def get_audio_duration(audio_path: Path) -> float:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[
|
[
|
||||||
@@ -55,6 +60,7 @@ def get_audio_duration(audio_path: Path) -> float:
|
|||||||
return float(result.stdout.strip())
|
return float(result.stdout.strip())
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def duration_to_video_length(duration: float) -> int:
|
def duration_to_video_length(duration: float) -> int:
|
||||||
frames = int(duration * 24) + 1
|
frames = int(duration * 24) + 1
|
||||||
if frames % 2 == 0:
|
if frames % 2 == 0:
|
||||||
@@ -62,6 +68,7 @@ def duration_to_video_length(duration: float) -> int:
|
|||||||
return max(49, min(frames, 169))
|
return max(49, min(frames, 169))
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
configure_logging(args.log_level)
|
configure_logging(args.log_level)
|
||||||
50
src/logging_config.py
Normal file
50
src/logging_config.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_LOG_FORMAT = "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
|
||||||
|
DEFAULT_LOG_LEVEL = "INFO"
|
||||||
|
LOG_LEVEL_ENV_VAR = "LOG_LEVEL"
|
||||||
|
|
||||||
|
F = TypeVar("F", bound=Callable[..., Any])
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_log_level(
|
||||||
|
cli_level: str | None,
|
||||||
|
*,
|
||||||
|
default_level: str = DEFAULT_LOG_LEVEL,
|
||||||
|
env_var: str = LOG_LEVEL_ENV_VAR,
|
||||||
|
) -> str:
|
||||||
|
level = default_level
|
||||||
|
env_level = os.getenv(env_var)
|
||||||
|
if env_level:
|
||||||
|
level = env_level
|
||||||
|
if cli_level:
|
||||||
|
level = cli_level
|
||||||
|
return level
|
||||||
|
|
||||||
|
|
||||||
|
def configure_logging(level: str | None = None, *, default_level: str = DEFAULT_LOG_LEVEL) -> None:
|
||||||
|
resolved = resolve_log_level(level, default_level=default_level)
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, resolved.upper(), logging.INFO),
|
||||||
|
format=DEFAULT_LOG_FORMAT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def debug_log_lifecycle(func: F) -> F:
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
logger = logging.getLogger(func.__module__)
|
||||||
|
logger.debug("Start %s", func.__qualname__)
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
finally:
|
||||||
|
logger.debug("End %s", func.__qualname__)
|
||||||
|
|
||||||
|
return wrapper # type: ignore[return-value]
|
||||||
@@ -9,7 +9,7 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from logging_config import configure_logging
|
from logging_config import configure_logging, debug_log_lifecycle
|
||||||
|
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
@@ -31,10 +31,15 @@ def parse_args() -> argparse.Namespace:
|
|||||||
parser.add_argument("--videos-dir", type=Path, default=DEFAULT_VIDEOS_DIR)
|
parser.add_argument("--videos-dir", type=Path, default=DEFAULT_VIDEOS_DIR)
|
||||||
parser.add_argument("--audios-dir", type=Path, default=DEFAULT_AUDIOS_DIR)
|
parser.add_argument("--audios-dir", type=Path, default=DEFAULT_AUDIOS_DIR)
|
||||||
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
|
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
|
||||||
parser.add_argument("--log-level", default="INFO")
|
parser.add_argument(
|
||||||
|
"--log-level",
|
||||||
|
default=None,
|
||||||
|
help="Logging level (overrides LOG_LEVEL env var)",
|
||||||
|
)
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@debug_log_lifecycle
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
configure_logging(args.log_level)
|
configure_logging(args.log_level)
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_LOG_FORMAT = "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
|
|
||||||
|
|
||||||
|
|
||||||
def configure_logging(level: str = "INFO") -> None:
|
|
||||||
logging.basicConfig(
|
|
||||||
level=getattr(logging, level.upper(), logging.INFO),
|
|
||||||
format=DEFAULT_LOG_FORMAT,
|
|
||||||
)
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Run the full video pipeline: generate, merge, and concatenate."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from logging_config import configure_logging
|
|
||||||
from s3_video_storage import S3VideoStorage
|
|
||||||
|
|
||||||
|
|
||||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
||||||
PROJECT_ROOT = SCRIPT_DIR.parents[1]
|
|
||||||
DEFAULT_BASE_DIR = PROJECT_ROOT
|
|
||||||
DEFAULT_HUNYUAN_DIR = DEFAULT_BASE_DIR / "HunyuanVideo-1.5"
|
|
||||||
DEFAULT_REEL_SCRIPT = DEFAULT_BASE_DIR / "reel_script.json"
|
|
||||||
DEFAULT_IMAGES_DIR = DEFAULT_BASE_DIR / "images"
|
|
||||||
DEFAULT_VIDEOS_DIR = DEFAULT_BASE_DIR / "videos"
|
|
||||||
DEFAULT_AUDIOS_DIR = DEFAULT_BASE_DIR / "audios"
|
|
||||||
DEFAULT_MERGED_DIR = DEFAULT_BASE_DIR / "merged"
|
|
||||||
DEFAULT_OUTPUT = DEFAULT_BASE_DIR / "results" / "final_output.mp4"
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument("--base-dir", type=Path, default=DEFAULT_BASE_DIR)
|
|
||||||
parser.add_argument("--hunyuan-dir", type=Path, default=DEFAULT_HUNYUAN_DIR)
|
|
||||||
parser.add_argument("--reel-script", type=Path, default=DEFAULT_REEL_SCRIPT)
|
|
||||||
parser.add_argument("--images-dir", type=Path, default=DEFAULT_IMAGES_DIR)
|
|
||||||
parser.add_argument("--videos-dir", type=Path, default=DEFAULT_VIDEOS_DIR)
|
|
||||||
parser.add_argument("--audios-dir", type=Path, default=DEFAULT_AUDIOS_DIR)
|
|
||||||
parser.add_argument("--merged-dir", type=Path, default=DEFAULT_MERGED_DIR)
|
|
||||||
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
|
|
||||||
parser.add_argument("--seed", type=int, default=1)
|
|
||||||
parser.add_argument("--skip-generate", action="store_true")
|
|
||||||
parser.add_argument("--skip-merge", action="store_true")
|
|
||||||
parser.add_argument("--skip-concat", action="store_true")
|
|
||||||
parser.add_argument("--skip-s3-upload", action="store_true")
|
|
||||||
parser.add_argument("--log-level", default="INFO")
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def run_step(name: str, cmd: list[str]) -> None:
|
|
||||||
LOGGER.info("=== %s ===", name)
|
|
||||||
LOGGER.info("$ %s", " ".join(str(part) for part in cmd))
|
|
||||||
subprocess.run(cmd, check=True)
|
|
||||||
|
|
||||||
|
|
||||||
def maybe_upload_to_s3(output_path: Path) -> None:
|
|
||||||
bucket = os.getenv("AWS_S3_BUCKET")
|
|
||||||
if not bucket:
|
|
||||||
LOGGER.warning("Skipping S3 upload: AWS_S3_BUCKET is not set")
|
|
||||||
return
|
|
||||||
|
|
||||||
storage = S3VideoStorage(
|
|
||||||
{
|
|
||||||
"bucket_name": bucket,
|
|
||||||
"region_name": os.getenv("AWS_REGION"),
|
|
||||||
"endpoint_url": os.getenv("AWS_S3_ENDPOINT_URL"),
|
|
||||||
"aws_access_key_id": os.getenv("AWS_ACCESS_KEY_ID"),
|
|
||||||
"aws_secret_access_key": os.getenv("AWS_SECRET_ACCESS_KEY"),
|
|
||||||
"aws_session_token": os.getenv("AWS_SESSION_TOKEN"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
s3_uri = storage.store_file(output_path)
|
|
||||||
LOGGER.info("Uploaded output to %s", s3_uri)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
args = parse_args()
|
|
||||||
configure_logging(args.log_level)
|
|
||||||
|
|
||||||
# If only base-dir is overridden, derive the common subpaths from it.
|
|
||||||
if args.base_dir != DEFAULT_BASE_DIR:
|
|
||||||
if args.hunyuan_dir == DEFAULT_HUNYUAN_DIR:
|
|
||||||
args.hunyuan_dir = args.base_dir / "HunyuanVideo-1.5"
|
|
||||||
if args.reel_script == DEFAULT_REEL_SCRIPT:
|
|
||||||
args.reel_script = args.base_dir / "reel_script.json"
|
|
||||||
if args.images_dir == DEFAULT_IMAGES_DIR:
|
|
||||||
args.images_dir = args.base_dir / "images"
|
|
||||||
if args.videos_dir == DEFAULT_VIDEOS_DIR:
|
|
||||||
args.videos_dir = args.base_dir / "videos"
|
|
||||||
if args.audios_dir == DEFAULT_AUDIOS_DIR:
|
|
||||||
args.audios_dir = args.base_dir / "audios"
|
|
||||||
if args.merged_dir == DEFAULT_MERGED_DIR:
|
|
||||||
args.merged_dir = args.base_dir / "merged"
|
|
||||||
if args.output == DEFAULT_OUTPUT:
|
|
||||||
args.output = args.base_dir / "results" / "final_output.mp4"
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not args.skip_generate:
|
|
||||||
run_step(
|
|
||||||
"Generate Videos",
|
|
||||||
[
|
|
||||||
sys.executable,
|
|
||||||
str(SCRIPT_DIR / "generate_videos.py"),
|
|
||||||
"--hunyuan-dir",
|
|
||||||
str(args.hunyuan_dir),
|
|
||||||
"--reel-script",
|
|
||||||
str(args.reel_script),
|
|
||||||
"--images-dir",
|
|
||||||
str(args.images_dir),
|
|
||||||
"--videos-dir",
|
|
||||||
str(args.videos_dir),
|
|
||||||
"--audios-dir",
|
|
||||||
str(args.audios_dir),
|
|
||||||
"--seed",
|
|
||||||
str(args.seed),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
if not args.skip_merge:
|
|
||||||
run_step(
|
|
||||||
"Merge Audio + Video",
|
|
||||||
[
|
|
||||||
sys.executable,
|
|
||||||
str(SCRIPT_DIR / "merge_audio_video.py"),
|
|
||||||
"--videos-dir",
|
|
||||||
str(args.videos_dir),
|
|
||||||
"--audios-dir",
|
|
||||||
str(args.audios_dir),
|
|
||||||
"--output-dir",
|
|
||||||
str(args.merged_dir),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
if not args.skip_concat:
|
|
||||||
run_step(
|
|
||||||
"Concatenate Merged Videos",
|
|
||||||
[
|
|
||||||
sys.executable,
|
|
||||||
str(SCRIPT_DIR / "concat_merged.py"),
|
|
||||||
"--merged-dir",
|
|
||||||
str(args.merged_dir),
|
|
||||||
"--output",
|
|
||||||
str(args.output),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
except subprocess.CalledProcessError as exc:
|
|
||||||
LOGGER.exception("Pipeline failed at command: %s", exc.cmd)
|
|
||||||
return exc.returncode
|
|
||||||
|
|
||||||
if not args.skip_s3_upload:
|
|
||||||
try:
|
|
||||||
maybe_upload_to_s3(args.output)
|
|
||||||
except Exception:
|
|
||||||
LOGGER.exception("Failed uploading output to S3")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
LOGGER.info("Pipeline complete")
|
|
||||||
LOGGER.info("Final output: %s", args.output)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
25
tests/test_logging_decorator.py
Normal file
25
tests/test_logging_decorator.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from src.logging_config import debug_log_lifecycle
|
||||||
|
|
||||||
|
|
||||||
|
class TestDebugLogLifecycle(unittest.TestCase):
|
||||||
|
def test_logs_function_start_and_end(self) -> None:
|
||||||
|
@debug_log_lifecycle
|
||||||
|
def sample(a: int, b: int) -> int:
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
with self.assertLogs(sample.__module__, level="DEBUG") as captured:
|
||||||
|
result = sample(2, 3)
|
||||||
|
|
||||||
|
self.assertEqual(result, 5)
|
||||||
|
joined = "\n".join(captured.output)
|
||||||
|
self.assertIn("Start TestDebugLogLifecycle.test_logs_function_start_and_end.<locals>.sample", joined)
|
||||||
|
self.assertIn("End TestDebugLogLifecycle.test_logs_function_start_and_end.<locals>.sample", joined)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user