polysync 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {polysync-0.2.0/src/polysync.egg-info → polysync-0.3.0}/PKG-INFO +1 -1
- {polysync-0.2.0 → polysync-0.3.0}/pyproject.toml +1 -1
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/__init__.py +1 -1
- polysync-0.3.0/src/polysync/edit/audiomix.py +124 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/edit/render_cuts.py +35 -8
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/edit/render_pip.py +34 -9
- {polysync-0.2.0 → polysync-0.3.0/src/polysync.egg-info}/PKG-INFO +1 -1
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync.egg-info/SOURCES.txt +1 -0
- {polysync-0.2.0 → polysync-0.3.0}/LICENSE +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/README.md +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/setup.cfg +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/audio.py +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/cli.py +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/edit/__init__.py +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/edit/autoedit.py +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/edit/grade.py +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/sidecar.py +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/sync.py +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync/verify.py +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync.egg-info/dependency_links.txt +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync.egg-info/entry_points.txt +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync.egg-info/requires.txt +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/src/polysync.egg-info/top_level.txt +0 -0
- {polysync-0.2.0 → polysync-0.3.0}/tests/test_sync_synthetic.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polysync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Multicam audio sync and director-style auto-edit — align N angles of one event by audio cross-correlation, then cut/PiP them into one MP4. Reversible sidecars, never re-encodes the originals.
|
|
5
5
|
Author: 王建硕 (Jian Shuo Wang)
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "polysync"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Multicam audio sync and director-style auto-edit — align N angles of one event by audio cross-correlation, then cut/PiP them into one MP4. Reversible sidecars, never re-encodes the originals."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -11,7 +11,7 @@ Public API:
|
|
|
11
11
|
from .sync import compute_sync, SyncResult, SyncError
|
|
12
12
|
from .sidecar import read_sidecar, write_sidecar, sidecar_path, SCHEMA_VERSION
|
|
13
13
|
|
|
14
|
-
__version__ = "0.
|
|
14
|
+
__version__ = "0.3.0"
|
|
15
15
|
__all__ = [
|
|
16
16
|
"compute_sync", "SyncResult", "SyncError",
|
|
17
17
|
"read_sidecar", "write_sidecar", "sidecar_path", "SCHEMA_VERSION",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Speaker-gated ("ducked") audio mix for multicam interviews.
|
|
2
|
+
|
|
3
|
+
The default render takes a single camera's mic as the soundtrack. With close,
|
|
4
|
+
bleeding mics that's noisy: every mic also picks up the *other* speaker plus room
|
|
5
|
+
tone, so a constant sum sounds muddy and loudness-normalization pumps the bleed
|
|
6
|
+
up during pauses. This builds a cleaner track instead: per moment, keep only the
|
|
7
|
+
ACTIVE speaker's mic at full level and duck the rest.
|
|
8
|
+
|
|
9
|
+
Who's active is decided by each mic's energy relative to ITS OWN baseline (not
|
|
10
|
+
absolute level) — that's what tracks the talker despite a louder close mic
|
|
11
|
+
bleeding. Far/room mics (e.g. a wide establishing cam) are auto-excluded: any
|
|
12
|
+
mic whose overall level is >`exclude_db` below the loudest is dropped as an
|
|
13
|
+
audio candidate so the reverby room mic is never selected.
|
|
14
|
+
|
|
15
|
+
`build_ducked_audio` returns a finished wav (gated → high-pass → light denoise →
|
|
16
|
+
loudness-normalized). The renderers use it in place of the single-cam audio when
|
|
17
|
+
`--duck-audio` is passed.
|
|
18
|
+
"""
|
|
19
|
+
import subprocess
|
|
20
|
+
import tempfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
from .. import audio
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _aligned_mic(path, delta, sr, n):
|
|
29
|
+
"""Extract a cam's loudest mic at `sr`, shifted into the reference timeline
|
|
30
|
+
(so index t corresponds to reference second t/sr), length `n` samples."""
|
|
31
|
+
with tempfile.NamedTemporaryFile(suffix=".pcm", delete=False) as tf:
|
|
32
|
+
tmp = tf.name
|
|
33
|
+
audio.extract_pcm(path, tmp, sr) # loudest stream, mono
|
|
34
|
+
x = audio.read_pcm(tmp)
|
|
35
|
+
Path(tmp).unlink(missing_ok=True)
|
|
36
|
+
pad = int(round(delta * sr))
|
|
37
|
+
if pad > 0:
|
|
38
|
+
x = np.concatenate([np.zeros(pad, np.float32), x])
|
|
39
|
+
elif pad < 0:
|
|
40
|
+
x = x[-pad:]
|
|
41
|
+
if len(x) < n:
|
|
42
|
+
x = np.pad(x, (0, n - len(x)))
|
|
43
|
+
return x[:n]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_ducked_audio(inputs, deltas, coverage, duration, out_path, sr=48000,
|
|
47
|
+
duck_db=-18.0, frame_ms=100.0, margin=0.20,
|
|
48
|
+
exclude_db=14.0, audio_cams=None, verbose=True):
|
|
49
|
+
"""Write a speaker-gated, cleaned wav to `out_path`. Returns out_path.
|
|
50
|
+
|
|
51
|
+
`audio_cams` (list of cam indices) explicitly picks which mics to gate among
|
|
52
|
+
— use it to exclude a wide/room mic that sits at a similar LEVEL to a real
|
|
53
|
+
speaker mic (level alone can't tell a close lav from a near room mic). If
|
|
54
|
+
None, fall back to dropping any mic >`exclude_db` below the loudest.
|
|
55
|
+
"""
|
|
56
|
+
n = int(duration * sr)
|
|
57
|
+
mics = [_aligned_mic(p, d, sr, n) for p, d in zip(inputs, deltas)]
|
|
58
|
+
|
|
59
|
+
lvl = np.array([20 * np.log10(np.sqrt(np.mean(m ** 2)) + 1e-6) for m in mics])
|
|
60
|
+
if audio_cams:
|
|
61
|
+
keep = [k for k in audio_cams if 0 <= k < len(mics)]
|
|
62
|
+
else:
|
|
63
|
+
keep = [k for k in range(len(mics)) if lvl[k] >= lvl.max() - exclude_db]
|
|
64
|
+
if verbose:
|
|
65
|
+
print(" mic levels(dB): %s; audio candidates: %s"
|
|
66
|
+
% ([round(float(x), 1) for x in lvl], keep))
|
|
67
|
+
|
|
68
|
+
hop = int(frame_ms / 1000 * sr)
|
|
69
|
+
nf = n // hop
|
|
70
|
+
|
|
71
|
+
def frame_logE(x):
|
|
72
|
+
return np.array([np.log(np.sqrt(np.mean(x[i*hop:(i+1)*hop] ** 2) + 1) + 1)
|
|
73
|
+
for i in range(nf)])
|
|
74
|
+
|
|
75
|
+
# coverage mask per cam (frames where the cam has valid footage in ref time)
|
|
76
|
+
cov = np.zeros((len(mics), nf), dtype=bool)
|
|
77
|
+
for k in range(len(mics)):
|
|
78
|
+
s, e = coverage[k] if k < len(coverage) else (0.0, duration)
|
|
79
|
+
cov[k, max(0, int(s/ (frame_ms/1000))): int(e/(frame_ms/1000))] = True
|
|
80
|
+
|
|
81
|
+
# baseline-normalized energy per candidate
|
|
82
|
+
norm = np.full((len(mics), nf), -1e9)
|
|
83
|
+
for k in keep:
|
|
84
|
+
E = frame_logE(mics[k])
|
|
85
|
+
base = np.median(E[cov[k]]) if cov[k].any() else np.median(E)
|
|
86
|
+
norm[k] = np.where(cov[k], E - base, -1e9)
|
|
87
|
+
|
|
88
|
+
# active cam per frame = argmax normalized among covered candidates
|
|
89
|
+
active = np.full(nf, keep[0], dtype=int)
|
|
90
|
+
for f in range(nf):
|
|
91
|
+
vals = [(norm[k, f], k) for k in keep if cov[k, f]]
|
|
92
|
+
if vals:
|
|
93
|
+
active[f] = max(vals)[1]
|
|
94
|
+
|
|
95
|
+
# gain mask per cam (active=1 else duck), smoothed to crossfade
|
|
96
|
+
duck = 10 ** (duck_db / 20.0)
|
|
97
|
+
out = np.zeros(n, dtype=np.float32)
|
|
98
|
+
ker = np.ones(int(0.2 * sr)) / int(0.2 * sr)
|
|
99
|
+
for k in range(len(mics)):
|
|
100
|
+
if k not in keep:
|
|
101
|
+
continue
|
|
102
|
+
gf = np.where(active == k, 1.0, duck)
|
|
103
|
+
gs = np.repeat(gf, hop)
|
|
104
|
+
gs = np.pad(gs, (0, n - len(gs)), mode="edge")
|
|
105
|
+
gs = np.convolve(gs, ker, "same")
|
|
106
|
+
out += mics[k] * gs
|
|
107
|
+
|
|
108
|
+
pk = np.max(np.abs(out))
|
|
109
|
+
if pk > 0:
|
|
110
|
+
out *= 0.95 * 32767 / pk
|
|
111
|
+
with tempfile.NamedTemporaryFile(suffix=".pcm", delete=False) as tf:
|
|
112
|
+
raw = tf.name
|
|
113
|
+
out.astype(np.int16).tofile(raw)
|
|
114
|
+
|
|
115
|
+
# high-pass rumble, light FFT denoise, loudness-normalize -> finished wav
|
|
116
|
+
subprocess.run(
|
|
117
|
+
["ffmpeg", "-nostdin", "-y", "-hide_banner", "-loglevel", "error",
|
|
118
|
+
"-f", "s16le", "-ar", str(sr), "-ac", "1", "-i", raw,
|
|
119
|
+
"-af", "highpass=f=70,afftdn=nr=10,loudnorm=I=-16:TP=-1.5:LRA=11",
|
|
120
|
+
"-ar", str(sr), "-ac", "2", str(out_path)],
|
|
121
|
+
check=True,
|
|
122
|
+
)
|
|
123
|
+
Path(raw).unlink(missing_ok=True)
|
|
124
|
+
return out_path
|
|
@@ -10,28 +10,42 @@ delivery (小红书 / Reels / Shorts) pass `--width 1080 --height 1920 --fill`.
|
|
|
10
10
|
import argparse
|
|
11
11
|
import json
|
|
12
12
|
import subprocess
|
|
13
|
+
import tempfile
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
from .grade import resolve_lut, parse_rotate, segment_filter
|
|
17
|
+
from .audiomix import build_ducked_audio
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
def render_cuts(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
19
21
|
width=1920, height=1080, fps=30, lut=None, log=None,
|
|
20
|
-
rotate=None, fill=False,
|
|
22
|
+
rotate=None, fill=False, duck_audio=False, duck_db=-18.0,
|
|
23
|
+
audio_cams=None, run=True):
|
|
21
24
|
plan = json.loads(Path(edl_path).read_text())
|
|
22
25
|
inputs = plan["inputs"]
|
|
23
26
|
deltas = plan.get("deltas", [0.0] * len(inputs))
|
|
24
27
|
edl = plan["edl"]
|
|
25
28
|
audio_src = plan["audio_source"]
|
|
29
|
+
duration = plan["duration_sec"]
|
|
26
30
|
W, H = width, height
|
|
27
31
|
lut_path = resolve_lut(lut, log)
|
|
28
32
|
rot = parse_rotate(rotate)
|
|
29
33
|
|
|
34
|
+
# Speaker-gated soundtrack: build a cleaned wav up front, use it as the audio.
|
|
35
|
+
ducked_wav = None
|
|
36
|
+
if duck_audio:
|
|
37
|
+
coverage = plan.get("coverage", [[0.0, duration]] * len(inputs))
|
|
38
|
+
ducked_wav = str(Path(tempfile.mkdtemp()) / "ducked.wav")
|
|
39
|
+
build_ducked_audio(inputs, deltas, coverage, duration, ducked_wav,
|
|
40
|
+
duck_db=duck_db, audio_cams=audio_cams)
|
|
41
|
+
|
|
30
42
|
cmd = ["ffmpeg", "-nostdin", "-y"]
|
|
31
43
|
for src, dlt in zip(inputs, deltas):
|
|
32
44
|
if abs(dlt) > 1e-9:
|
|
33
45
|
cmd.extend(["-itsoffset", "%.6f" % dlt])
|
|
34
46
|
cmd.extend(["-i", src])
|
|
47
|
+
if ducked_wav:
|
|
48
|
+
cmd.extend(["-i", ducked_wav]) # extra input, already ref-aligned
|
|
35
49
|
|
|
36
50
|
filters = [
|
|
37
51
|
segment_filter(row["cam"], row["start"], row["end"], i, W, H, fps,
|
|
@@ -42,13 +56,16 @@ def render_cuts(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
|
42
56
|
filters.append("%sconcat=n=%d:v=1:a=0[vout]" % (concat, len(edl)))
|
|
43
57
|
fc = ";".join(filters)
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
cmd.extend(["-filter_complex", fc, "-map", "[vout]"])
|
|
60
|
+
if ducked_wav:
|
|
61
|
+
cmd.extend(["-map", "%d:a:0" % len(inputs)])
|
|
62
|
+
else:
|
|
63
|
+
audio_offset = edl[0]["start"] if edl else 0.0
|
|
64
|
+
fc2 = ("[%d:a:0]atrim=start=%s:duration=%s,asetpts=PTS-STARTPTS[aout]"
|
|
65
|
+
% (audio_src, audio_offset, duration))
|
|
66
|
+
cmd[cmd.index("-filter_complex") + 1] = fc + ";" + fc2
|
|
67
|
+
cmd.extend(["-map", "[aout]"])
|
|
49
68
|
cmd.extend([
|
|
50
|
-
"-filter_complex", fc,
|
|
51
|
-
"-map", "[vout]", "-map", "[aout]",
|
|
52
69
|
"-t", str(duration),
|
|
53
70
|
"-c:v", encoder, "-b:v", bitrate, "-tag:v", "hvc1",
|
|
54
71
|
"-c:a", "aac", "-b:a", "192k",
|
|
@@ -75,10 +92,20 @@ def main(argv=None):
|
|
|
75
92
|
help="per-cam rotation CAM:DEG (90=CW), repeatable")
|
|
76
93
|
ap.add_argument("--fill", action="store_true",
|
|
77
94
|
help="crop to fill instead of letterbox-pad (use for vertical)")
|
|
95
|
+
ap.add_argument("--duck-audio", action="store_true",
|
|
96
|
+
help="speaker-gated soundtrack: keep the active speaker's mic, "
|
|
97
|
+
"duck the rest (cleaner than a single-cam mic for interviews)")
|
|
98
|
+
ap.add_argument("--duck-db", type=float, default=-18.0,
|
|
99
|
+
help="level of ducked (inactive) mics, dB (default -18)")
|
|
100
|
+
ap.add_argument("--audio-cams",
|
|
101
|
+
help="comma-separated cam indices to gate among (e.g. 0,1) — "
|
|
102
|
+
"exclude wide/room mics; default = auto by level")
|
|
78
103
|
args = ap.parse_args(argv)
|
|
104
|
+
cams = [int(x) for x in args.audio_cams.split(",")] if args.audio_cams else None
|
|
79
105
|
render_cuts(args.edl, args.out, encoder=args.encoder, bitrate=args.bitrate,
|
|
80
106
|
width=args.width, height=args.height, fps=args.fps,
|
|
81
|
-
lut=args.lut, log=args.log, rotate=args.rotate, fill=args.fill
|
|
107
|
+
lut=args.lut, log=args.log, rotate=args.rotate, fill=args.fill,
|
|
108
|
+
duck_audio=args.duck_audio, duck_db=args.duck_db, audio_cams=cams)
|
|
82
109
|
|
|
83
110
|
|
|
84
111
|
if __name__ == "__main__":
|
|
@@ -9,9 +9,11 @@ Per-segment EDL rows may carry a `pip` field (cam index) to override the picker.
|
|
|
9
9
|
import argparse
|
|
10
10
|
import json
|
|
11
11
|
import subprocess
|
|
12
|
+
import tempfile
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
from .grade import resolve_lut, parse_rotate, _transpose_chain
|
|
16
|
+
from .audiomix import build_ducked_audio
|
|
15
17
|
|
|
16
18
|
POSITIONS = {
|
|
17
19
|
"bottom-right": ("W-w-{m}", "H-h-{m}"),
|
|
@@ -43,18 +45,26 @@ def pick_pip(row, K, coverage, mode="next"):
|
|
|
43
45
|
def render_pip(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
44
46
|
width=1920, height=1080, fps=30, pip="bottom-right",
|
|
45
47
|
pip_width=480, pip_margin=24, border_px=4, pip_pick="next",
|
|
46
|
-
lut=None, log=None, rotate=None,
|
|
48
|
+
lut=None, log=None, rotate=None, duck_audio=False, duck_db=-18.0,
|
|
49
|
+
audio_cams=None, run=True):
|
|
47
50
|
plan = json.loads(Path(edl_path).read_text())
|
|
48
51
|
inputs = plan["inputs"]
|
|
49
52
|
deltas = plan.get("deltas", [0.0] * len(inputs))
|
|
50
53
|
edl = plan["edl"]
|
|
51
54
|
audio_src = plan["audio_source"]
|
|
55
|
+
duration = plan["duration_sec"]
|
|
52
56
|
K = len(inputs)
|
|
53
|
-
coverage = plan.get("coverage", [[0.0,
|
|
57
|
+
coverage = plan.get("coverage", [[0.0, duration]] * K)
|
|
54
58
|
lut_path = resolve_lut(lut, log)
|
|
55
59
|
rot = parse_rotate(rotate)
|
|
56
60
|
grade = ("lut3d=%s," % lut_path) if lut_path else ""
|
|
57
61
|
|
|
62
|
+
ducked_wav = None
|
|
63
|
+
if duck_audio:
|
|
64
|
+
ducked_wav = str(Path(tempfile.mkdtemp()) / "ducked.wav")
|
|
65
|
+
build_ducked_audio(inputs, deltas, coverage, duration, ducked_wav,
|
|
66
|
+
duck_db=duck_db, audio_cams=audio_cams)
|
|
67
|
+
|
|
58
68
|
W, H = width, height
|
|
59
69
|
pw = pip_width
|
|
60
70
|
ph = round(pw * 9 / 16)
|
|
@@ -68,6 +78,8 @@ def render_pip(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
|
68
78
|
if abs(dlt) > 1e-9:
|
|
69
79
|
cmd.extend(["-itsoffset", "%.6f" % dlt])
|
|
70
80
|
cmd.extend(["-i", src])
|
|
81
|
+
if ducked_wav:
|
|
82
|
+
cmd.extend(["-i", ducked_wav]) # extra input, already ref-aligned
|
|
71
83
|
|
|
72
84
|
filters = []
|
|
73
85
|
for i, row in enumerate(edl):
|
|
@@ -103,14 +115,19 @@ def render_pip(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
|
103
115
|
|
|
104
116
|
concat = "".join("[v%d]" % i for i in range(len(edl)))
|
|
105
117
|
filters.append("%sconcat=n=%d:v=1:a=0[vout]" % (concat, len(edl)))
|
|
106
|
-
|
|
107
|
-
dur = plan["duration_sec"]
|
|
118
|
+
dur = duration
|
|
108
119
|
fc = ";".join(filters)
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
cmd.extend(["-filter_complex", None, "-map", "[vout]"]) # fc filled below
|
|
121
|
+
if ducked_wav:
|
|
122
|
+
cmd[cmd.index("-filter_complex") + 1] = fc
|
|
123
|
+
cmd.extend(["-map", "%d:a:0" % K])
|
|
124
|
+
else:
|
|
125
|
+
audio_offset = edl[0]["start"] if edl else 0.0
|
|
126
|
+
fc += (";[%d:a:0]atrim=start=%s:duration=%s,asetpts=PTS-STARTPTS[aout]"
|
|
127
|
+
% (audio_src, audio_offset, dur))
|
|
128
|
+
cmd[cmd.index("-filter_complex") + 1] = fc
|
|
129
|
+
cmd.extend(["-map", "[aout]"])
|
|
111
130
|
cmd.extend([
|
|
112
|
-
"-filter_complex", fc,
|
|
113
|
-
"-map", "[vout]", "-map", "[aout]",
|
|
114
131
|
"-t", str(dur),
|
|
115
132
|
"-c:v", encoder, "-b:v", bitrate, "-tag:v", "hvc1",
|
|
116
133
|
"-c:a", "aac", "-b:a", "192k",
|
|
@@ -141,12 +158,20 @@ def main(argv=None):
|
|
|
141
158
|
ap.add_argument("--log", help="built-in log->Rec.709 grade (e.g. slog3)")
|
|
142
159
|
ap.add_argument("--rotate", action="append",
|
|
143
160
|
help="per-cam rotation CAM:DEG (90=CW), repeatable")
|
|
161
|
+
ap.add_argument("--duck-audio", action="store_true",
|
|
162
|
+
help="speaker-gated soundtrack (keep active speaker's mic, duck rest)")
|
|
163
|
+
ap.add_argument("--duck-db", type=float, default=-18.0,
|
|
164
|
+
help="level of ducked (inactive) mics, dB (default -18)")
|
|
165
|
+
ap.add_argument("--audio-cams",
|
|
166
|
+
help="comma-separated cam indices to gate among (e.g. 0,1)")
|
|
144
167
|
args = ap.parse_args(argv)
|
|
168
|
+
cams = [int(x) for x in args.audio_cams.split(",")] if args.audio_cams else None
|
|
145
169
|
render_pip(args.edl, args.out, encoder=args.encoder, bitrate=args.bitrate,
|
|
146
170
|
width=args.width, height=args.height, fps=args.fps, pip=args.pip,
|
|
147
171
|
pip_width=args.pip_width, pip_margin=args.pip_margin,
|
|
148
172
|
border_px=args.border_px, pip_pick=args.pip_pick,
|
|
149
|
-
lut=args.lut, log=args.log, rotate=args.rotate
|
|
173
|
+
lut=args.lut, log=args.log, rotate=args.rotate,
|
|
174
|
+
duck_audio=args.duck_audio, duck_db=args.duck_db, audio_cams=cams)
|
|
150
175
|
|
|
151
176
|
|
|
152
177
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polysync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Multicam audio sync and director-style auto-edit — align N angles of one event by audio cross-correlation, then cut/PiP them into one MP4. Reversible sidecars, never re-encodes the originals.
|
|
5
5
|
Author: 王建硕 (Jian Shuo Wang)
|
|
6
6
|
License: MIT
|
|
@@ -14,6 +14,7 @@ src/polysync.egg-info/entry_points.txt
|
|
|
14
14
|
src/polysync.egg-info/requires.txt
|
|
15
15
|
src/polysync.egg-info/top_level.txt
|
|
16
16
|
src/polysync/edit/__init__.py
|
|
17
|
+
src/polysync/edit/audiomix.py
|
|
17
18
|
src/polysync/edit/autoedit.py
|
|
18
19
|
src/polysync/edit/grade.py
|
|
19
20
|
src/polysync/edit/render_cuts.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|