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}",
|
"source_file": f"processed/{video_file.name}",
|
||||||
}
|
}
|
||||||
narration_entry["use_audio_channels"] = "auto"
|
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
|
existing_narration[segment_id] = narration_entry
|
||||||
added_count += 1
|
added_count += 1
|
||||||
@@ -1455,7 +1458,10 @@ def _import_narration_segments(narration_dir: Path, config, verbose: bool) -> No
|
|||||||
narration_entry["filter"] = "talkinghead"
|
narration_entry["filter"] = "talkinghead"
|
||||||
|
|
||||||
narration_entry["use_audio_channels"] = "auto"
|
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
|
existing_narration[segment_id] = narration_entry
|
||||||
added_count += 1
|
added_count += 1
|
||||||
@@ -2567,7 +2573,8 @@ def cmd_preprocess(
|
|||||||
filter=filter_list,
|
filter=filter_list,
|
||||||
output_file=output_file,
|
output_file=output_file,
|
||||||
use_audio_channels=existing_entry.get("use_audio_channels", "auto"),
|
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))
|
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.
|
# Always record the plain path; stitch shifts the base dir for low/tiny.
|
||||||
entry["source_file"] = f"processed/{segment_id}_processed.mov"
|
entry["source_file"] = f"processed/{segment_id}_processed.mov"
|
||||||
entry.setdefault("use_audio_channels", "auto")
|
entry.setdefault("use_audio_channels", "auto")
|
||||||
entry.setdefault("defer_loudnorm", True)
|
entry.setdefault("defer_loudnorm", False)
|
||||||
existing_narration[segment_id] = entry
|
existing_narration[segment_id] = entry
|
||||||
|
|
||||||
with open(narration_json_path, "w", encoding="utf-8") as f:
|
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
|
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()
|
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
|
insert_pos = cmd.index("-y") + 1 if "-y" in cmd else 1
|
||||||
cmd[insert_pos:insert_pos] = [
|
cmd[insert_pos:insert_pos] = [
|
||||||
"-progress",
|
"-progress",
|
||||||
"pipe:1",
|
"pipe:1",
|
||||||
"-nostats",
|
"-nostats",
|
||||||
"-loglevel",
|
"-loglevel",
|
||||||
"warning",
|
level,
|
||||||
]
|
]
|
||||||
|
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(
|
||||||
|
|||||||
Reference in New Issue
Block a user