build a track at its length instead of padding up to it

place_at padded a mix up to the scene length with apad and let atrim end the
stream. That handoff is a race. A bare apad loses it by generating silence for
ever: ffmpeg never reaches EOF and spins at 100% CPU with the output
half-written -- 4 hangs in 10 runs, and two orphans left on a dev host by
test_compose_dialogue had burned 14h and 10h of CPU unnoticed. apad=whole_dur
stops the hang and loses the same race the other way, emitting a track one clip
short (3s for 4s) in 3 of 14 runs with no error anywhere. Neither -threads 1
nor -filter_threads 1 helps.

A silent base input of exactly the target length, mixed under the placements
with amix=duration=first, has no handoff to lose: 0 bad in 20 runs, one output
size. The other apad sites feed no mix, but take whole_dur too since the length
is known there as well.

Also pass config/subfolder to the single-file component loader, the way the
GGUF branch already does and for the same reason: left to itself
from_single_file infers a base repo from the checkpoint's own metadata, so
Krea-2-RealDream reached for krea/Krea-2-Raw and died on a repo nobody named.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
parent 124941a4
......@@ -473,9 +473,21 @@ def silence(path: str, duration: float, cancel=None, log=None) -> str:
def pad_to(src: str, out_path: str, duration: float, cancel=None, log=None) -> str:
"""One scene's narration, padded (or trimmed) to the scene's exact length."""
"""One scene's narration, padded (or trimmed) to the scene's exact length.
The pad is bounded with ``whole_dur`` rather than left to ``atrim`` to stop.
A bare ``apad`` generates silence for ever and relies on something
downstream ending the stream; with more than one input feeding the pad
through ``amix`` that termination is a race, and when it is lost ffmpeg
never reaches EOF and spins at 100% CPU with the output half-written.
Measured on this exact graph (two 1 s inputs, ``amix``, 4 s trim): 4 hangs
in 10 runs, and two orphans on the host had burned 14 h and 10 h of CPU
before anyone noticed. ``-threads 1`` and ``-filter_threads 1`` do NOT fix
it (3/12 and 7/12); bounding the pad does (0/12).
"""
run(["-i", src, "-af",
f"aresample=48000,apad,atrim=0:{max(0.02, duration):.3f},asetpts=N/SR/TB",
f"aresample=48000,apad=whole_dur={max(0.02, duration):.3f},"
f"atrim=0:{max(0.02, duration):.3f},asetpts=N/SR/TB",
"-ac", "2", "-ar", "48000", "-c:a", "pcm_s16le", out_path],
cancel=cancel, log=log)
return out_path
......@@ -536,23 +548,33 @@ def place_at(placements: List[tuple], out_path: str, *, duration: float,
"""
if not placements:
return silence(out_path, duration, cancel=cancel, log=log)
inputs: List[str] = []
dur = max(0.02, float(duration))
# The track's length comes from a silent base input of exactly `dur`, mixed
# under the placements, and `amix=duration=first` ends the mix with it.
#
# It is NOT padded up to length with `apad`. A pad has to be ended by
# something downstream, and when it is fed through `amix` that handoff is a
# race: a bare `apad` loses it by generating silence for ever, so ffmpeg
# never reaches EOF and spins at 100% CPU with the output half-written (two
# orphans on a dev host had burned 14 h and 10 h of CPU before anyone
# noticed; 4 hangs in 10 runs of this graph). `apad=whole_dur` stops the
# hang but loses the same race the other way, silently emitting a track one
# clip short -- 3 s where 4 s was asked for, in 3 of 14 runs, with no error
# anywhere. Neither `-threads 1` nor `-filter_threads 1` helps (3/12, 7/12).
# A base input that is already the right length has no handoff to lose.
inputs: List[str] = ["-f", "lavfi", "-t", f"{dur:.3f}",
"-i", "anullsrc=channel_layout=stereo:sample_rate=48000"]
graph: List[str] = []
labels: List[str] = []
for i, (path, start) in enumerate(placements):
labels: List[str] = ["[0:a]"]
for i, (path, start) in enumerate(placements, start=1):
inputs += ["-i", path]
ms = max(0, int(round(float(start) * 1000)))
graph.append(f"[{i}:a]aresample=48000,adelay={ms}|{ms}[d{i}]")
labels.append(f"[d{i}]")
if len(labels) == 1:
graph.append(f"{labels[0]}apad,atrim=0:{max(0.02, float(duration)):.3f},"
f"asetpts=N/SR/TB[aout]")
else:
graph.append("".join(labels)
+ f"amix=inputs={len(labels)}:duration=longest:"
f"dropout_transition=0:normalize=0,"
f"apad,atrim=0:{max(0.02, float(duration)):.3f},"
f"asetpts=N/SR/TB[aout]")
graph.append("".join(labels)
+ f"amix=inputs={len(labels)}:duration=first:"
f"dropout_transition=0:normalize=0,"
f"atrim=0:{dur:.3f},asetpts=N/SR/TB[aout]")
run(inputs + ["-filter_complex", ";".join(graph), "-map", "[aout]",
"-ac", "2", "-ar", "48000", "-c:a", "pcm_s16le", out_path],
cancel=cancel, log=log)
......@@ -625,7 +647,8 @@ def clip_audio(src: str, out_path: str, *, duration: float, trim_start: float =
pre = ["-stream_loop", "-1"] if loop else []
if trim_start > 0:
pre += ["-ss", f"{float(trim_start):.3f}"]
af = ["aresample=48000", "apad",
af = ["aresample=48000",
f"apad=whole_dur={max(0.02, float(duration)):.3f}",
f"atrim=0:{max(0.02, float(duration)):.3f}", "asetpts=N/SR/TB"]
v = max(0.0, float(volume))
if abs(v - 1.0) > 1e-6:
......@@ -674,7 +697,8 @@ def mix_voice_clip(voice: Optional[str], clip: Optional[str], out_path: str, *,
return silence(out_path, duration, cancel=cancel, log=log)
if not voice:
run(["-i", clip, "-af",
f"aresample=48000,apad,atrim=0:{max(0.02, duration):.3f},asetpts=N/SR/TB",
f"aresample=48000,apad=whole_dur={max(0.02, duration):.3f},"
f"atrim=0:{max(0.02, duration):.3f},asetpts=N/SR/TB",
"-ac", "2", "-ar", "48000", "-c:a", "pcm_s16le", out_path],
cancel=cancel, log=log)
return out_path
......@@ -682,7 +706,8 @@ def mix_voice_clip(voice: Optional[str], clip: Optional[str], out_path: str, *,
# on the output: ffmpeg refuses simple and complex filtering on the same
# stream ("Filtergraph ... was specified for a stream fed from a complex
# filtergraph"), and the mix would never be written at all.
fit = (f"apad,atrim=0:{max(0.02, float(duration)):.3f},asetpts=N/SR/TB")
fit = (f"apad=whole_dur={max(0.02, float(duration)):.3f},"
f"atrim=0:{max(0.02, float(duration)):.3f},asetpts=N/SR/TB")
if duck:
graph = ("[1:a]asplit=2[c1][c2];"
"[0:a]asplit=2[v1][v2];"
......
......@@ -1043,7 +1043,19 @@ def build_component_models(model_name: str, cfg: Optional[Dict[str, Any]], dtype
# `.safetensors` component could only be named if somebody had
# published a converted directory tree for it.
_path = resolve_single_file(source)
components[name] = cls.from_single_file(_path, **kwargs)
# `config` names the architecture these tensors belong to, for
# the same reason the GGUF branch above passes it: left to
# itself from_single_file INFERS a base repo out of the
# checkpoint's own metadata, and a community fine-tune carries
# whatever its author trained from. Measured on Krea-2-RealDream
# (a Civitai Krea 2 checkpoint): it inferred `krea/Krea-2-Raw`
# and the load died with "is not a local folder and is not a
# valid model identifier" -- a missing-repo error for a repo
# nobody had named. The entry's own `path` is the repo supplying
# every other component, so it is the right config here too.
_base = str((c or {}).get('path') or '').strip() or model_name
components[name] = cls.from_single_file(
_path, config=_base, subfolder=name, **kwargs)
else:
components[name] = cls.from_pretrained(source, **kwargs)
descs.append(f"{name}:{source.split('/')[-1]}{'/' + mode if mode else ''}")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment