Refactor CLI and add preprocessing pipeline
- New CLI structure: -p project, -a action (required flags) - Add -i import, -f force, -v verbose, --dry-run, --no-cache options - Add preprocessor.py with chroma key filter (ProRes 4444 output) - Support background images from shared_assets folder - Support video metadata JSON files (talkinghead.json) - Add validation for preprocessed output before render - Update gnommo.sh with import command and new CLI interface - Fix Python 3.9 compatibility (Optional[] instead of | None) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+338
-228
@@ -8,18 +8,11 @@ from pathlib import Path
|
||||
|
||||
from . import __version__
|
||||
from .errors import GnommoError, ParseError, ValidationError, RenderError
|
||||
from .parser import (
|
||||
parse_manuscript,
|
||||
parse_project_config,
|
||||
parse_slides,
|
||||
parse_transcript,
|
||||
parse_videos,
|
||||
)
|
||||
from .validator import validate_project
|
||||
from .transformer import build_render_plan
|
||||
from .renderer import render, generate_ffmpeg_command_string
|
||||
from .transcriber import transcribe_video, save_transcript, load_transcript
|
||||
from .aligner import align_markers, save_aligned_transcript
|
||||
|
||||
|
||||
class NotImplementedException(GnommoError):
|
||||
"""Feature not yet implemented."""
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -34,120 +27,79 @@ def main() -> int:
|
||||
version=f"%(prog)s {__version__}",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# validate command
|
||||
validate_parser = subparsers.add_parser(
|
||||
"validate",
|
||||
help="Validate project without rendering",
|
||||
# Required arguments
|
||||
parser.add_argument(
|
||||
"-p", "--project",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Project name (directory in current folder)",
|
||||
)
|
||||
validate_parser.add_argument(
|
||||
"project",
|
||||
type=Path,
|
||||
help="Path to project directory",
|
||||
parser.add_argument(
|
||||
"-a", "--action",
|
||||
type=str,
|
||||
choices=["validate", "preprocess", "render", "all", "transcribe", "align"],
|
||||
required=True,
|
||||
help="Action to perform",
|
||||
)
|
||||
|
||||
# render command
|
||||
render_parser = subparsers.add_parser(
|
||||
"render",
|
||||
help="Render video from project",
|
||||
# Optional arguments
|
||||
parser.add_argument(
|
||||
"-i", "--import",
|
||||
dest="import_assets",
|
||||
action="store_true",
|
||||
help="Import assets and generate metadata JSON files",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"project",
|
||||
type=Path,
|
||||
help="Path to project directory",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
"-o", "--output",
|
||||
type=Path,
|
||||
help="Output file path (default: project/out/final.mp4)",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="store_true",
|
||||
help="Print FFmpeg command",
|
||||
help="Verbose output",
|
||||
)
|
||||
render_parser.add_argument(
|
||||
parser.add_argument(
|
||||
"-f", "--force",
|
||||
action="store_true",
|
||||
help="Force destructive changes (overwrite existing files)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-cache",
|
||||
action="store_true",
|
||||
help="Force cache break (not implemented)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print FFmpeg command without executing",
|
||||
)
|
||||
|
||||
# generate-slides command
|
||||
gen_slides_parser = subparsers.add_parser(
|
||||
"generate-slides",
|
||||
help="Generate slides.json from Keynote export folder",
|
||||
)
|
||||
gen_slides_parser.add_argument(
|
||||
"directory",
|
||||
type=Path,
|
||||
help="Path to slides directory (e.g., media/slides/Video1)",
|
||||
)
|
||||
gen_slides_parser.add_argument(
|
||||
"--type",
|
||||
default="square",
|
||||
help="Slide type for all slides (default: square)",
|
||||
)
|
||||
|
||||
# transcribe command
|
||||
transcribe_parser = subparsers.add_parser(
|
||||
"transcribe",
|
||||
help="Transcribe video audio using Whisper",
|
||||
)
|
||||
transcribe_parser.add_argument(
|
||||
"video",
|
||||
type=Path,
|
||||
help="Path to video file",
|
||||
)
|
||||
transcribe_parser.add_argument(
|
||||
"-o", "--output",
|
||||
type=Path,
|
||||
help="Output JSON file (default: <video>.transcript.json)",
|
||||
)
|
||||
transcribe_parser.add_argument(
|
||||
"--model",
|
||||
default="base",
|
||||
choices=["tiny", "base", "small", "medium", "large"],
|
||||
help="Whisper model size (default: base)",
|
||||
)
|
||||
|
||||
# align command
|
||||
align_parser = subparsers.add_parser(
|
||||
"align",
|
||||
help="Align manuscript markers to transcript timestamps",
|
||||
)
|
||||
align_parser.add_argument(
|
||||
"project",
|
||||
type=Path,
|
||||
help="Path to project directory",
|
||||
)
|
||||
align_parser.add_argument(
|
||||
"--transcript",
|
||||
type=Path,
|
||||
help="Path to transcript JSON (default: media/talking_head.transcript.json)",
|
||||
)
|
||||
align_parser.add_argument(
|
||||
"--offset",
|
||||
type=float,
|
||||
default=-1.0,
|
||||
help="Seconds to offset marker times (default: -1.0)",
|
||||
help="Show what would be done without executing",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Resolve project path
|
||||
project_path = Path(args.project)
|
||||
if not project_path.is_absolute():
|
||||
project_path = Path.cwd() / project_path
|
||||
|
||||
try:
|
||||
if args.command == "validate":
|
||||
return cmd_validate(args.project)
|
||||
elif args.command == "render":
|
||||
output = args.output or (args.project / "out" / "final.mp4")
|
||||
return cmd_render(args.project, output, args.verbose, args.dry_run)
|
||||
elif args.command == "generate-slides":
|
||||
return cmd_generate_slides(args.directory, args.type)
|
||||
elif args.command == "transcribe":
|
||||
output = args.output or args.video.with_suffix(".transcript.json")
|
||||
return cmd_transcribe(args.video, output, args.model)
|
||||
elif args.command == "align":
|
||||
return cmd_align(args.project, args.transcript, args.offset)
|
||||
# Check for --no-cache
|
||||
if args.no_cache:
|
||||
raise NotImplementedException("--no-cache is not yet implemented")
|
||||
|
||||
# Handle import mode
|
||||
if args.import_assets:
|
||||
return cmd_import(project_path, args.force, args.verbose)
|
||||
|
||||
# Handle actions
|
||||
if args.action == "validate":
|
||||
return cmd_validate(project_path, args.verbose)
|
||||
elif args.action == "preprocess":
|
||||
return cmd_preprocess(project_path, args.verbose, args.dry_run)
|
||||
elif args.action == "render":
|
||||
return cmd_render(project_path, args.verbose, args.dry_run)
|
||||
elif args.action == "transcribe":
|
||||
return cmd_transcribe(project_path, args.verbose)
|
||||
elif args.action == "align":
|
||||
return cmd_align(project_path, args.verbose)
|
||||
elif args.action == "all":
|
||||
return cmd_all(project_path, args.verbose, args.dry_run)
|
||||
|
||||
except GnommoError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
@@ -158,9 +110,109 @@ def main() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_validate(project_path: Path) -> int:
|
||||
"""Run validation only."""
|
||||
print(f"Validating project: {project_path}")
|
||||
# =============================================================================
|
||||
# Import Command
|
||||
# =============================================================================
|
||||
|
||||
def cmd_import(project_path: Path, force: bool, verbose: bool) -> int:
|
||||
"""Import assets and generate metadata JSON files."""
|
||||
print(f"Importing assets for: {project_path.name}")
|
||||
|
||||
if not project_path.exists():
|
||||
print(f"Error: Project directory not found: {project_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Check for existing files that would be overwritten
|
||||
slides_base = project_path / "media" / "slides"
|
||||
slides_dirs = [d for d in slides_base.glob("*/") if d.is_dir()] if slides_base.exists() else []
|
||||
videos_json = project_path / "videos.json"
|
||||
|
||||
files_to_create = []
|
||||
|
||||
# Check for slide directories to import
|
||||
for slides_dir in slides_dirs:
|
||||
slides_json = slides_dir / "slides.json"
|
||||
if slides_json.exists() and not force:
|
||||
print(f"Warning: {slides_json} already exists. Use -f to overwrite.")
|
||||
return 1
|
||||
files_to_create.append(("slides", slides_dir))
|
||||
|
||||
if not force and files_to_create:
|
||||
print("\nThe following files will be created/overwritten:")
|
||||
for ftype, fpath in files_to_create:
|
||||
print(f" - {fpath}/slides.json")
|
||||
print("\nUse -f/--force to proceed.")
|
||||
return 1
|
||||
|
||||
# Generate slides.json for each directory
|
||||
for ftype, slides_dir in files_to_create:
|
||||
if ftype == "slides":
|
||||
_generate_slides_json(slides_dir, verbose)
|
||||
|
||||
print("Import complete.")
|
||||
return 0
|
||||
|
||||
|
||||
def _generate_slides_json(directory: Path, verbose: bool) -> None:
|
||||
"""Generate slides.json from Keynote export folder."""
|
||||
extensions = {".png", ".gif", ".pdf", ".jpg", ".jpeg"}
|
||||
files = [f for f in directory.iterdir() if f.suffix.lower() in extensions]
|
||||
|
||||
if not files:
|
||||
print(f" Warning: No image files in {directory}")
|
||||
return
|
||||
|
||||
# Extract numeric suffix from filenames like "Video1.001.png"
|
||||
pattern = re.compile(r"\.(\d+)\.[^.]+$")
|
||||
|
||||
slides = {}
|
||||
for file in files:
|
||||
match = pattern.search(file.name)
|
||||
if match:
|
||||
num = int(match.group(1))
|
||||
slide_id = f"S{num}"
|
||||
slides[slide_id] = {
|
||||
"image": file.name,
|
||||
"type": "fullscreen",
|
||||
}
|
||||
|
||||
if not slides:
|
||||
print(f" Warning: No valid slide files in {directory}")
|
||||
return
|
||||
|
||||
# Sort by slide number
|
||||
sorted_slides = dict(sorted(slides.items(), key=lambda x: int(x[0][1:])))
|
||||
|
||||
# Write slides.json
|
||||
output_path = directory / "slides.json"
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(sorted_slides, f, indent=2)
|
||||
|
||||
print(f" Generated {output_path} ({len(sorted_slides)} slides)")
|
||||
if verbose:
|
||||
for slide_id in sorted_slides:
|
||||
print(f" [{slide_id}]")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Validate Command
|
||||
# =============================================================================
|
||||
|
||||
def cmd_validate(project_path: Path, verbose: bool) -> int:
|
||||
"""Validate project configuration."""
|
||||
from .parser import (
|
||||
parse_manuscript,
|
||||
parse_project_config,
|
||||
parse_slides,
|
||||
parse_videos,
|
||||
)
|
||||
from .validator import validate_project
|
||||
|
||||
print(f"Validating: {project_path.name}")
|
||||
|
||||
if not (project_path / "project.json").exists():
|
||||
print(f"Error: project.json not found in {project_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Parse all files
|
||||
_, markers, malformed = parse_manuscript(project_path)
|
||||
@@ -168,6 +220,11 @@ def cmd_validate(project_path: Path) -> int:
|
||||
slides = parse_slides(project_path, config)
|
||||
videos = parse_videos(project_path)
|
||||
|
||||
if verbose:
|
||||
print(f" - Markers in manuscript: {len(markers)}")
|
||||
print(f" - Slides defined: {len(slides)}")
|
||||
print(f" - Videos defined: {len(videos)}")
|
||||
|
||||
# Validate
|
||||
validate_project(project_path, markers, config, slides, videos, malformed)
|
||||
|
||||
@@ -175,140 +232,155 @@ def cmd_validate(project_path: Path) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_render(project_path: Path, output_path: Path, verbose: bool, dry_run: bool) -> int:
|
||||
"""Run full render pipeline."""
|
||||
print(f"Rendering project: {project_path}")
|
||||
print(f"Output: {output_path}")
|
||||
print()
|
||||
# =============================================================================
|
||||
# Preprocess Command
|
||||
# =============================================================================
|
||||
|
||||
# Stage 1: Extract
|
||||
print("Stage 1/4: Parsing input files...")
|
||||
def cmd_preprocess(project_path: Path, verbose: bool, dry_run: bool) -> int:
|
||||
"""Run preprocessing pipeline on video sources."""
|
||||
from .parser import parse_project_config, parse_videos
|
||||
from .preprocessor import preprocess_video
|
||||
|
||||
print(f"Preprocessing: {project_path.name}")
|
||||
|
||||
config = parse_project_config(project_path)
|
||||
videos = parse_videos(project_path)
|
||||
|
||||
for video_id, video_source in videos.items():
|
||||
print(f"\n Processing: {video_id}")
|
||||
|
||||
if not video_source.preprocess:
|
||||
print(" No preprocessing steps defined, skipping.")
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
print(f" Would preprocess: {video_source.file}")
|
||||
for step in video_source.preprocess:
|
||||
print(f" - {step}")
|
||||
else:
|
||||
preprocess_video(project_path, video_id, video_source, verbose)
|
||||
|
||||
print("\nPreprocessing complete.")
|
||||
return 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Render Command
|
||||
# =============================================================================
|
||||
|
||||
def cmd_render(project_path: Path, verbose: bool, dry_run: bool) -> int:
|
||||
"""Render final video."""
|
||||
from .parser import (
|
||||
parse_manuscript,
|
||||
parse_project_config,
|
||||
parse_slides,
|
||||
parse_transcript,
|
||||
parse_videos,
|
||||
)
|
||||
from .validator import validate_project
|
||||
from .transformer import build_render_plan
|
||||
from .renderer import render, generate_ffmpeg_command_string
|
||||
|
||||
print(f"Rendering: {project_path.name}")
|
||||
|
||||
# Stage 1: Parse
|
||||
print("\n[1/4] Parsing...")
|
||||
_, markers, malformed = parse_manuscript(project_path)
|
||||
config = parse_project_config(project_path)
|
||||
slides = parse_slides(project_path, config)
|
||||
videos = parse_videos(project_path)
|
||||
transcript = parse_transcript(project_path)
|
||||
|
||||
print(f" - Found {len(markers)} slide markers in manuscript")
|
||||
print(f" - Found {len(slides)} slide definitions")
|
||||
print(f" - Found {len(transcript)} transcript entries")
|
||||
print()
|
||||
if verbose:
|
||||
print(f" - Markers: {len(markers)}")
|
||||
print(f" - Slides: {len(slides)}")
|
||||
print(f" - Transcript entries: {len(transcript)}")
|
||||
|
||||
# Stage 2: Validate
|
||||
print("Stage 2/4: Validating...")
|
||||
print("\n[2/4] Validating...")
|
||||
validate_project(project_path, markers, config, slides, videos, malformed)
|
||||
print(" - Validation passed")
|
||||
print()
|
||||
print(" Passed.")
|
||||
|
||||
# Stage 3: Transform
|
||||
print("Stage 3/4: Building render plan...")
|
||||
print("\n[3/4] Building render plan...")
|
||||
plan = build_render_plan(project_path, config, slides, videos, transcript)
|
||||
print(f" - Video duration: {plan.total_duration:.2f}s")
|
||||
print(f" - Duration: {plan.total_duration:.1f}s")
|
||||
print(f" - Slide events: {len(plan.slide_events)}")
|
||||
for event in plan.slide_events:
|
||||
print(f" - [{event.slide_id}] {event.start_time:.2f}s - {event.end_time:.2f}s")
|
||||
print()
|
||||
|
||||
if verbose:
|
||||
for event in plan.slide_events:
|
||||
print(f" [{event.slide_id}] {event.start_time:.1f}s - {event.end_time:.1f}s")
|
||||
|
||||
# Stage 4: Render
|
||||
output_path = project_path / "out" / "final.mp4"
|
||||
|
||||
if dry_run:
|
||||
print("Stage 4/4: Generating FFmpeg command (dry run)...")
|
||||
print()
|
||||
print("\n[4/4] FFmpeg command (dry run):")
|
||||
print(generate_ffmpeg_command_string(plan, output_path))
|
||||
return 0
|
||||
|
||||
print("Stage 4/4: Rendering video...")
|
||||
print("\n[4/4] Rendering...")
|
||||
render(plan, output_path, verbose=verbose)
|
||||
print(f" - Output written to: {output_path}")
|
||||
print()
|
||||
print("Done.")
|
||||
print(f" Output: {output_path}")
|
||||
|
||||
print("\nDone.")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_generate_slides(directory: Path, slide_type: str) -> int:
|
||||
"""Generate slides.json from Keynote export folder."""
|
||||
directory = directory.resolve()
|
||||
# =============================================================================
|
||||
# Transcribe Command
|
||||
# =============================================================================
|
||||
|
||||
if not directory.exists():
|
||||
print(f"Error: Directory not found: {directory}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not directory.is_dir():
|
||||
print(f"Error: Not a directory: {directory}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Find all image files (png, gif, pdf)
|
||||
extensions = {".png", ".gif", ".pdf", ".jpg", ".jpeg"}
|
||||
files = [f for f in directory.iterdir() if f.suffix.lower() in extensions]
|
||||
|
||||
if not files:
|
||||
print(f"Error: No image files found in {directory}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Extract numeric suffix from filenames like "Video1.001.png"
|
||||
# Pattern: anything followed by .NNN. followed by extension
|
||||
pattern = re.compile(r"\.(\d+)\.[^.]+$")
|
||||
|
||||
slides = {}
|
||||
for file in files:
|
||||
match = pattern.search(file.name)
|
||||
if match:
|
||||
num = int(match.group(1)) # "001" -> 1
|
||||
slide_id = f"S{num}"
|
||||
slides[slide_id] = {
|
||||
"image": file.name,
|
||||
"type": slide_type,
|
||||
}
|
||||
else:
|
||||
print(f" Warning: Could not parse slide number from: {file.name}")
|
||||
|
||||
if not slides:
|
||||
print("Error: No valid slide files found", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Sort by slide number
|
||||
sorted_slides = dict(sorted(slides.items(), key=lambda x: int(x[0][1:])))
|
||||
|
||||
# Write slides.json in the same directory
|
||||
output_path = directory / "slides.json"
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(sorted_slides, f, indent=2)
|
||||
|
||||
print(f"Generated {output_path}")
|
||||
print(f" - Found {len(sorted_slides)} slides")
|
||||
for slide_id, slide_def in sorted_slides.items():
|
||||
print(f" [{slide_id}] {slide_def['image']}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_transcribe(video_path: Path, output_path: Path, model: str) -> int:
|
||||
def cmd_transcribe(project_path: Path, verbose: bool) -> int:
|
||||
"""Transcribe video audio using Whisper."""
|
||||
print(f"Transcribing: {video_path}")
|
||||
print(f"Model: {model}")
|
||||
print()
|
||||
from .transcriber import transcribe_video, save_transcript
|
||||
from .parser import parse_videos
|
||||
|
||||
words = transcribe_video(video_path, model=model)
|
||||
print(f"Transcribing: {project_path.name}")
|
||||
|
||||
videos = parse_videos(project_path)
|
||||
if not videos:
|
||||
print("Error: No videos defined in videos.json", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Use first video
|
||||
video_id = next(iter(videos.keys()))
|
||||
video_source = videos[video_id]
|
||||
video_path = project_path / video_source.file
|
||||
|
||||
if not video_path.exists():
|
||||
print(f"Error: Video not found: {video_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f" Video: {video_path.name}")
|
||||
|
||||
words = transcribe_video(video_path, model="base")
|
||||
|
||||
output_path = video_path.with_suffix(".transcript.json")
|
||||
save_transcript(words, output_path)
|
||||
|
||||
print(f" - Transcribed {len(words)} words")
|
||||
print(f" - Duration: {words[-1].end:.1f}s" if words else " - No words found")
|
||||
print(f" - Saved: {output_path}")
|
||||
|
||||
save_transcript(words, output_path)
|
||||
print(f" - Saved to: {output_path}")
|
||||
|
||||
# Show first few words as preview
|
||||
if words:
|
||||
if verbose and words:
|
||||
preview = " ".join(w.word for w in words[:10])
|
||||
print(f" - Preview: {preview}...")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_align(project_path: Path, transcript_path: Path = None, offset: float = -1.0) -> int:
|
||||
# =============================================================================
|
||||
# Align Command
|
||||
# =============================================================================
|
||||
|
||||
def cmd_align(project_path: Path, verbose: bool) -> int:
|
||||
"""Align manuscript markers to transcript timestamps."""
|
||||
print(f"Aligning: {project_path}")
|
||||
print(f"Offset: {offset}s")
|
||||
print()
|
||||
from .transcriber import load_transcript
|
||||
from .aligner import align_markers, save_aligned_transcript
|
||||
from .parser import parse_videos
|
||||
|
||||
print(f"Aligning: {project_path.name}")
|
||||
|
||||
# Load manuscript
|
||||
manuscript_path = project_path / "manuscript.txt"
|
||||
@@ -318,45 +390,83 @@ def cmd_align(project_path: Path, transcript_path: Path = None, offset: float =
|
||||
|
||||
manuscript_text = manuscript_path.read_text(encoding="utf-8")
|
||||
|
||||
# Load transcript
|
||||
if transcript_path is None:
|
||||
# Try to find transcript in media folder
|
||||
transcript_path = project_path / "media" / "talking_head.transcript.json"
|
||||
# Find transcript
|
||||
videos = parse_videos(project_path)
|
||||
video_id = next(iter(videos.keys()))
|
||||
video_source = videos[video_id]
|
||||
video_path = project_path / video_source.file
|
||||
transcript_path = video_path.with_suffix(".transcript.json")
|
||||
|
||||
if not transcript_path.exists():
|
||||
print(f"Error: Transcript not found: {transcript_path}", file=sys.stderr)
|
||||
print("Run 'gnommo transcribe' first to generate the transcript.", file=sys.stderr)
|
||||
print("Run with -a transcribe first.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f" - Loading transcript: {transcript_path}")
|
||||
print(f" Loading: {transcript_path.name}")
|
||||
transcript = load_transcript(transcript_path)
|
||||
print(f" - Loaded {len(transcript)} words")
|
||||
print(f" - {len(transcript)} words")
|
||||
|
||||
# Align markers
|
||||
print(" - Aligning markers...")
|
||||
alignments = align_markers(manuscript_text, transcript, offset_seconds=offset)
|
||||
# Align
|
||||
print(" Aligning markers...")
|
||||
alignments = align_markers(manuscript_text, transcript, offset_seconds=-1.0)
|
||||
|
||||
# Report results
|
||||
print()
|
||||
print("Alignment results:")
|
||||
# Report
|
||||
unmatched = 0
|
||||
for a in alignments:
|
||||
if a.timestamp >= 0:
|
||||
print(f" [{a.marker_id}] @ {a.timestamp:.2f}s - \"{a.matched_phrase}...\"")
|
||||
if verbose:
|
||||
print(f" [{a.marker_id}] @ {a.timestamp:.1f}s")
|
||||
else:
|
||||
print(f" [{a.marker_id}] NOT FOUND - \"{a.matched_phrase}...\"")
|
||||
print(f" [{a.marker_id}] NOT FOUND")
|
||||
unmatched += 1
|
||||
|
||||
if unmatched > 0:
|
||||
print(f"\nWarning: {unmatched} markers could not be aligned")
|
||||
print(f"\n Warning: {unmatched} markers not aligned")
|
||||
|
||||
# Save aligned transcript.csv
|
||||
# Save
|
||||
output_path = project_path / "transcript.csv"
|
||||
save_aligned_transcript(alignments, transcript, output_path)
|
||||
print(f"\nSaved: {output_path}")
|
||||
print(f"\n Saved: {output_path}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# All Command (Full Pipeline)
|
||||
# =============================================================================
|
||||
|
||||
def cmd_all(project_path: Path, verbose: bool, dry_run: bool) -> int:
|
||||
"""Run full pipeline: transcribe → align → render."""
|
||||
from .parser import parse_videos
|
||||
|
||||
print(f"=== Full Pipeline: {project_path.name} ===\n")
|
||||
|
||||
# Check if transcript exists
|
||||
videos = parse_videos(project_path)
|
||||
if videos:
|
||||
video_id = next(iter(videos.keys()))
|
||||
video_source = videos[video_id]
|
||||
video_path = project_path / video_source.file
|
||||
transcript_path = video_path.with_suffix(".transcript.json")
|
||||
|
||||
if not transcript_path.exists():
|
||||
print(">>> Step 1/3: Transcribe\n")
|
||||
result = cmd_transcribe(project_path, verbose)
|
||||
if result != 0:
|
||||
return result
|
||||
else:
|
||||
print(f">>> Step 1/3: Transcribe (cached: {transcript_path.name})\n")
|
||||
|
||||
# Align
|
||||
print("\n>>> Step 2/3: Align\n")
|
||||
result = cmd_align(project_path, verbose)
|
||||
if result != 0:
|
||||
return result
|
||||
|
||||
# Render
|
||||
print("\n>>> Step 3/3: Render\n")
|
||||
return cmd_render(project_path, verbose, dry_run)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
Reference in New Issue
Block a user