Adding more verbose logging to ffmpg
This commit is contained in:
+11
-4
@@ -1430,7 +1430,10 @@ def _import_narration_segments(narration_dir: Path, config, verbose: bool) -> No
|
||||
"source_file": f"processed/{video_file.name}",
|
||||
}
|
||||
narration_entry["use_audio_channels"] = "auto"
|
||||
narration_entry["defer_loudnorm"] = True
|
||||
# Loudnorm is applied per-segment during preprocess (not deferred to
|
||||
# stitch), so the processed files are already normalized and ready to be
|
||||
# concatenated directly at render time.
|
||||
narration_entry["defer_loudnorm"] = False
|
||||
|
||||
existing_narration[segment_id] = narration_entry
|
||||
added_count += 1
|
||||
@@ -1455,7 +1458,10 @@ def _import_narration_segments(narration_dir: Path, config, verbose: bool) -> No
|
||||
narration_entry["filter"] = "talkinghead"
|
||||
|
||||
narration_entry["use_audio_channels"] = "auto"
|
||||
narration_entry["defer_loudnorm"] = True
|
||||
# Loudnorm is applied per-segment during preprocess (not deferred to
|
||||
# stitch), so the processed files are already normalized and ready to be
|
||||
# concatenated directly at render time.
|
||||
narration_entry["defer_loudnorm"] = False
|
||||
|
||||
existing_narration[segment_id] = narration_entry
|
||||
added_count += 1
|
||||
@@ -2567,7 +2573,8 @@ def cmd_preprocess(
|
||||
filter=filter_list,
|
||||
output_file=output_file,
|
||||
use_audio_channels=existing_entry.get("use_audio_channels", "auto"),
|
||||
defer_loudnorm=existing_entry.get("defer_loudnorm", True),
|
||||
# Default False: apply loudnorm now (in preprocess), not deferred.
|
||||
defer_loudnorm=existing_entry.get("defer_loudnorm", False),
|
||||
)
|
||||
segments_to_process.append((segment_id, video_source))
|
||||
|
||||
@@ -2671,7 +2678,7 @@ def cmd_preprocess(
|
||||
# Always record the plain path; stitch shifts the base dir for low/tiny.
|
||||
entry["source_file"] = f"processed/{segment_id}_processed.mov"
|
||||
entry.setdefault("use_audio_channels", "auto")
|
||||
entry.setdefault("defer_loudnorm", True)
|
||||
entry.setdefault("defer_loudnorm", False)
|
||||
existing_narration[segment_id] = entry
|
||||
|
||||
with open(narration_json_path, "w", encoding="utf-8") as f:
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Deterministic narration scheduling for render-time segment stitching.
|
||||
|
||||
Instead of pre-stitching segments into narration_combined.mov, the render stage
|
||||
concatenates the processed segments directly. From narration.json + the cached
|
||||
per-segment transcripts this module computes two things:
|
||||
|
||||
1. an ordered segment schedule (processed file, skip, take, and offset in the
|
||||
combined timeline) — this drives the ffmpeg concat at render time; and
|
||||
2. the merged word-level transcript, with every word re-timed into the
|
||||
combined timeline — this drives slide alignment, exactly what
|
||||
re-transcribing narration_combined.mov used to produce, but derived
|
||||
deterministically (no re-transcription, no separate combined file).
|
||||
|
||||
The processed files share framerate and format and are uncompressed, so the
|
||||
computed offsets match the concatenated audio timeline sample-for-sample.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
|
||||
from .models import VideoSource
|
||||
from .preprocessor import get_preprocessed_path
|
||||
from .transcriber import TranscribedWord, load_transcript
|
||||
|
||||
|
||||
@dataclass
|
||||
class NarrationSegment:
|
||||
"""One narration segment placed on the combined render timeline."""
|
||||
|
||||
seg_id: str
|
||||
source_path: Path # processed file to concatenate
|
||||
skip: float # seconds trimmed from the segment's start
|
||||
take: Optional[float] # seconds kept from `skip` (None → to end)
|
||||
duration: float # effective seconds contributed to the timeline
|
||||
offset: float # start time of this segment in the combined timeline
|
||||
|
||||
|
||||
def segment_order(narration: dict) -> list[str]:
|
||||
"""Natural sort of segment ids (S2 before S10, s1-15 before s16-end)."""
|
||||
return sorted(
|
||||
narration.keys(),
|
||||
key=lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", s)],
|
||||
)
|
||||
|
||||
|
||||
def build_narration_schedule(
|
||||
narration: dict[str, VideoSource],
|
||||
narration_dir: Path,
|
||||
get_duration: Callable[[Path], float],
|
||||
transcripts_dir: Optional[Path] = None,
|
||||
verbose: bool = False,
|
||||
) -> tuple[list[NarrationSegment], list[TranscribedWord]]:
|
||||
"""Return (ordered segments with offsets, merged transcript in timeline).
|
||||
|
||||
Args:
|
||||
narration: seg_id -> VideoSource (from parse_narration).
|
||||
narration_dir: base dir the processed files resolve against.
|
||||
get_duration: callable(Path) -> float (ffprobe duration), used only
|
||||
when a segment has no explicit take.
|
||||
transcripts_dir: where per-segment {seg_id}.json transcripts live
|
||||
(defaults to narration_dir/transcripts).
|
||||
"""
|
||||
if transcripts_dir is None:
|
||||
transcripts_dir = narration_dir / "transcripts"
|
||||
|
||||
segments: list[NarrationSegment] = []
|
||||
merged: list[TranscribedWord] = []
|
||||
offset = 0.0
|
||||
|
||||
for seg_id in segment_order(narration):
|
||||
vs = narration[seg_id]
|
||||
source_path = get_preprocessed_path(narration_dir, vs)
|
||||
|
||||
skip = vs.skip or 0.0
|
||||
take = vs.take
|
||||
if take is not None:
|
||||
eff = max(0.0, take)
|
||||
else:
|
||||
full_dur = get_duration(source_path) if source_path.exists() else 0.0
|
||||
eff = max(0.0, full_dur - skip)
|
||||
seg_end = skip + eff # kept window end in the segment's own timeline
|
||||
|
||||
segments.append(
|
||||
NarrationSegment(
|
||||
seg_id=seg_id,
|
||||
source_path=source_path,
|
||||
skip=skip,
|
||||
take=take,
|
||||
duration=eff,
|
||||
offset=offset,
|
||||
)
|
||||
)
|
||||
|
||||
# Re-time this segment's transcript into the combined timeline: keep only
|
||||
# words inside [skip, seg_end], subtract skip, and add the running offset.
|
||||
tpath = transcripts_dir / f"{seg_id}.json"
|
||||
if tpath.exists():
|
||||
kept = 0
|
||||
for w in load_transcript(tpath):
|
||||
if w.end <= skip or w.start >= seg_end:
|
||||
continue # entirely outside the kept window
|
||||
new_start = max(w.start, skip) - skip + offset
|
||||
new_end = min(w.end, seg_end) - skip + offset
|
||||
merged.append(
|
||||
TranscribedWord(word=w.word, start=round(new_start, 3), end=round(new_end, 3))
|
||||
)
|
||||
kept += 1
|
||||
if verbose:
|
||||
print(f" {seg_id}: +{kept} words (skip={skip:.2f}s take={eff:.2f}s → offset {offset:.2f}s)")
|
||||
elif verbose:
|
||||
print(f" ⚠ {seg_id}: no transcript at {tpath} — words missing from merged transcript")
|
||||
|
||||
offset += eff
|
||||
|
||||
return segments, merged
|
||||
+37
-2
@@ -305,16 +305,51 @@ def ensure_proxy_files_exist(
|
||||
import selectors, time, sys, subprocess
|
||||
|
||||
|
||||
def run_ffmpeg_with_progress(cmd, duration, description="Processing"):
|
||||
# Module-level FFmpeg verbosity switch. When enabled (e.g. via `gnommo -v`),
|
||||
# run_ffmpeg_with_progress streams FFmpeg's full output at -loglevel verbose
|
||||
# instead of the quiet progress bar — useful when debugging filter graphs.
|
||||
_FFMPEG_VERBOSE = False
|
||||
|
||||
|
||||
def set_ffmpeg_verbose(enabled: bool) -> None:
|
||||
"""Enable/disable verbose FFmpeg logging for every ffmpeg run in this process."""
|
||||
global _FFMPEG_VERBOSE
|
||||
_FFMPEG_VERBOSE = bool(enabled)
|
||||
|
||||
|
||||
def run_ffmpeg_with_progress(cmd, duration, description="Processing", verbose=None, loglevel=None):
|
||||
cmd = cmd.copy()
|
||||
|
||||
if verbose is None:
|
||||
verbose = _FFMPEG_VERBOSE
|
||||
level = loglevel or ("verbose" if verbose else "warning")
|
||||
|
||||
# Verbose mode: stream FFmpeg's full output live (no progress bar), still
|
||||
# capturing it so the error path keeps the full log.
|
||||
if verbose:
|
||||
insert_pos = cmd.index("-y") + 1 if "-y" in cmd else 1
|
||||
cmd[insert_pos:insert_pos] = ["-loglevel", level, "-stats"]
|
||||
print(f" {description} — FFmpeg (-loglevel {level}):")
|
||||
print(f" $ {' '.join(cmd)}")
|
||||
p = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, bufsize=1, universal_newlines=True,
|
||||
)
|
||||
logs = []
|
||||
for line in iter(p.stdout.readline, ""):
|
||||
logs.append(line)
|
||||
sys.stdout.write(line)
|
||||
sys.stdout.flush()
|
||||
p.wait()
|
||||
return subprocess.CompletedProcess(cmd, p.returncode, stdout="", stderr="".join(logs))
|
||||
|
||||
insert_pos = cmd.index("-y") + 1 if "-y" in cmd else 1
|
||||
cmd[insert_pos:insert_pos] = [
|
||||
"-progress",
|
||||
"pipe:1",
|
||||
"-nostats",
|
||||
"-loglevel",
|
||||
"warning",
|
||||
level,
|
||||
]
|
||||
|
||||
p = subprocess.Popen(
|
||||
|
||||
Reference in New Issue
Block a user