polysync 0.1.0__tar.gz → 0.2.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.1.0/src/polysync.egg-info → polysync-0.2.0}/PKG-INFO +1 -1
- {polysync-0.1.0 → polysync-0.2.0}/pyproject.toml +1 -1
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync/__init__.py +1 -1
- polysync-0.2.0/src/polysync/edit/grade.py +110 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync/edit/render_cuts.py +23 -10
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync/edit/render_pip.py +20 -8
- {polysync-0.1.0 → polysync-0.2.0/src/polysync.egg-info}/PKG-INFO +1 -1
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync.egg-info/SOURCES.txt +1 -0
- {polysync-0.1.0 → polysync-0.2.0}/LICENSE +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/README.md +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/setup.cfg +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync/audio.py +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync/cli.py +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync/edit/__init__.py +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync/edit/autoedit.py +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync/sidecar.py +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync/sync.py +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync/verify.py +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync.egg-info/dependency_links.txt +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync.egg-info/entry_points.txt +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync.egg-info/requires.txt +0 -0
- {polysync-0.1.0 → polysync-0.2.0}/src/polysync.egg-info/top_level.txt +0 -0
- {polysync-0.1.0 → polysync-0.2.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.2.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.2.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.2.0"
|
|
15
15
|
__all__ = [
|
|
16
16
|
"compute_sync", "SyncResult", "SyncError",
|
|
17
17
|
"read_sidecar", "write_sidecar", "sidecar_path", "SCHEMA_VERSION",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Color grading + orientation helpers for the renderers.
|
|
2
|
+
|
|
3
|
+
Raw camera footage almost never renders correctly straight off the card. Two
|
|
4
|
+
things bite every time and are handled here:
|
|
5
|
+
|
|
6
|
+
1. **Log color.** Sony cameras (FX3/FX6) shoot S-Log3 / S-Gamut3.Cine by default
|
|
7
|
+
— flat, grey, low-contrast. It MUST be converted to Rec.709 with a LUT or it
|
|
8
|
+
looks broken. Check the `.XML` sidecar's `CaptureGammaEquation` (`s-log3-cine`)
|
|
9
|
+
or run `ffprobe ... color_transfer`. `--log slog3` generates and applies the
|
|
10
|
+
conversion LUT for you.
|
|
11
|
+
2. **Orientation.** Phones / vertically-mounted cameras record rotated. Some
|
|
12
|
+
(FX3) write a rotation flag and ffmpeg auto-rotates; others (FX6 turned on its
|
|
13
|
+
side) write NO flag and come out lying down. `--rotate cam:deg` fixes those.
|
|
14
|
+
|
|
15
|
+
Performance note baked into `segment_filter`: the LUT is applied AFTER the
|
|
16
|
+
downscale, not before. A 3D LUT on 4K (8 MP) is ~4x slower than on 1080p — and
|
|
17
|
+
the result is visually identical. Always scale, then grade.
|
|
18
|
+
"""
|
|
19
|
+
import os
|
|
20
|
+
import tempfile
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_slog3_709_lut(path, size=33):
|
|
26
|
+
"""Write a Sony S-Log3 / S-Gamut3.Cine -> Rec.709 3D LUT (.cube) to `path`."""
|
|
27
|
+
def slog3_to_lin(n): # n in [0,1] == 10-bit code value / 1023
|
|
28
|
+
cv = n * 1023.0
|
|
29
|
+
return np.where(
|
|
30
|
+
cv >= 171.2102946929,
|
|
31
|
+
(10 ** ((cv - 420.0) / 261.5)) * 0.19 - 0.01,
|
|
32
|
+
(cv - 95.0) * 0.01125000 / (171.2102946929 - 95.0),
|
|
33
|
+
)
|
|
34
|
+
# S-Gamut3.Cine -> Rec.709 (linear) matrix, D65
|
|
35
|
+
M = np.array([[1.6269, -0.3576, -0.2693],
|
|
36
|
+
[-0.0928, 1.3478, -0.2550],
|
|
37
|
+
[0.0387, -0.1622, 1.1235]])
|
|
38
|
+
def oetf709(L):
|
|
39
|
+
L = np.clip(L, 0, 1)
|
|
40
|
+
return np.where(L < 0.018, 4.5 * L, 1.099 * np.power(L, 0.45) - 0.099)
|
|
41
|
+
lines = ["TITLE \"SLog3 SGamut3Cine to Rec709\"", f"LUT_3D_SIZE {size}",
|
|
42
|
+
"DOMAIN_MIN 0 0 0", "DOMAIN_MAX 1 1 1"]
|
|
43
|
+
for b in range(size):
|
|
44
|
+
for g in range(size):
|
|
45
|
+
for r in range(size):
|
|
46
|
+
lin = slog3_to_lin(np.array([r, g, b]) / (size - 1))
|
|
47
|
+
out = oetf709(M @ lin)
|
|
48
|
+
lines.append("%.6f %.6f %.6f" % (out[0], out[1], out[2]))
|
|
49
|
+
with open(path, "w") as f:
|
|
50
|
+
f.write("\n".join(lines) + "\n")
|
|
51
|
+
return path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Built-in log profiles -> on-the-fly LUT generators. Cached in tempdir so
|
|
55
|
+
# repeated render calls in one session don't regenerate.
|
|
56
|
+
_BUILTIN = {"slog3": make_slog3_709_lut}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_lut(lut=None, log=None):
|
|
60
|
+
"""Return a .cube path: explicit `lut` file wins; else generate from `log`."""
|
|
61
|
+
if lut:
|
|
62
|
+
return lut
|
|
63
|
+
if not log:
|
|
64
|
+
return None
|
|
65
|
+
key = log.lower()
|
|
66
|
+
if key not in _BUILTIN:
|
|
67
|
+
raise SystemExit("unknown --log %r (known: %s)" % (log, ", ".join(_BUILTIN)))
|
|
68
|
+
cache = os.path.join(tempfile.gettempdir(), "polysync_%s_709.cube" % key)
|
|
69
|
+
if not os.path.exists(cache):
|
|
70
|
+
_BUILTIN[key](cache)
|
|
71
|
+
return cache
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_rotate(values):
|
|
75
|
+
"""Parse repeatable `--rotate cam:deg` into {cam_index: degrees}. Degrees in
|
|
76
|
+
{90, 180, 270, -90}. 90 = clockwise."""
|
|
77
|
+
out = {}
|
|
78
|
+
for v in (values or []):
|
|
79
|
+
cam, _, deg = v.partition(":")
|
|
80
|
+
out[int(cam)] = int(deg)
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _transpose_chain(deg):
|
|
85
|
+
"""ffmpeg filter fragment to rotate `deg` clockwise (90/180/270/-90)."""
|
|
86
|
+
deg = deg % 360
|
|
87
|
+
if deg == 90:
|
|
88
|
+
return "transpose=1,"
|
|
89
|
+
if deg == 270:
|
|
90
|
+
return "transpose=2,"
|
|
91
|
+
if deg == 180:
|
|
92
|
+
return "transpose=1,transpose=1,"
|
|
93
|
+
return ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def segment_filter(cam, start, end, idx, W, H, fps, rotate_deg=0, lut=None,
|
|
97
|
+
pip=False):
|
|
98
|
+
"""Build one segment's video filter chain. Order: trim -> rotate -> scale ->
|
|
99
|
+
crop/pad -> LUT (after downscale, for speed) -> sar -> fps. With `pip=True`
|
|
100
|
+
the frame fills (crop) instead of pad — used for main/inset tiles."""
|
|
101
|
+
rot = _transpose_chain(rotate_deg)
|
|
102
|
+
if pip:
|
|
103
|
+
fit = ("scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d"
|
|
104
|
+
% (W, H, W, H))
|
|
105
|
+
else:
|
|
106
|
+
fit = ("scale=%d:%d:force_original_aspect_ratio=decrease,"
|
|
107
|
+
"pad=%d:%d:(ow-iw)/2:(oh-ih)/2" % (W, H, W, H))
|
|
108
|
+
grade = ("lut3d=%s," % lut) if lut else ""
|
|
109
|
+
return ("[%d:v]trim=start=%s:end=%s,setpts=PTS-STARTPTS,%s%s,%ssetsar=1,"
|
|
110
|
+
"fps=%d[v%d]" % (cam, start, end, rot, fit, grade, fps, idx))
|
|
@@ -2,21 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
Applies each input's `delta` via `ffmpeg -itsoffset` so EDL times (reference
|
|
4
4
|
timeline) work directly inside the filter graph — originals are read untouched.
|
|
5
|
+
|
|
6
|
+
Raw footage usually needs `--log slog3` (Sony S-Log3 -> Rec.709 grade) and, for
|
|
7
|
+
vertically-shot cameras with no rotation flag, `--rotate cam:90`. For vertical
|
|
8
|
+
delivery (小红书 / Reels / Shorts) pass `--width 1080 --height 1920 --fill`.
|
|
5
9
|
"""
|
|
6
10
|
import argparse
|
|
7
11
|
import json
|
|
8
12
|
import subprocess
|
|
9
13
|
from pathlib import Path
|
|
10
14
|
|
|
15
|
+
from .grade import resolve_lut, parse_rotate, segment_filter
|
|
16
|
+
|
|
11
17
|
|
|
12
18
|
def render_cuts(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
13
|
-
width=1920, height=1080, fps=30,
|
|
19
|
+
width=1920, height=1080, fps=30, lut=None, log=None,
|
|
20
|
+
rotate=None, fill=False, run=True):
|
|
14
21
|
plan = json.loads(Path(edl_path).read_text())
|
|
15
22
|
inputs = plan["inputs"]
|
|
16
23
|
deltas = plan.get("deltas", [0.0] * len(inputs))
|
|
17
24
|
edl = plan["edl"]
|
|
18
25
|
audio_src = plan["audio_source"]
|
|
19
26
|
W, H = width, height
|
|
27
|
+
lut_path = resolve_lut(lut, log)
|
|
28
|
+
rot = parse_rotate(rotate)
|
|
20
29
|
|
|
21
30
|
cmd = ["ffmpeg", "-nostdin", "-y"]
|
|
22
31
|
for src, dlt in zip(inputs, deltas):
|
|
@@ -24,14 +33,11 @@ def render_cuts(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
|
24
33
|
cmd.extend(["-itsoffset", "%.6f" % dlt])
|
|
25
34
|
cmd.extend(["-i", src])
|
|
26
35
|
|
|
27
|
-
filters = [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"pad=%d:%d:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=%d[v%d]"
|
|
33
|
-
% (row["cam"], row["start"], row["end"], W, H, W, H, fps, i)
|
|
34
|
-
)
|
|
36
|
+
filters = [
|
|
37
|
+
segment_filter(row["cam"], row["start"], row["end"], i, W, H, fps,
|
|
38
|
+
rotate_deg=rot.get(row["cam"], 0), lut=lut_path, pip=fill)
|
|
39
|
+
for i, row in enumerate(edl)
|
|
40
|
+
]
|
|
35
41
|
concat = "".join("[v%d]" % i for i in range(len(edl)))
|
|
36
42
|
filters.append("%sconcat=n=%d:v=1:a=0[vout]" % (concat, len(edl)))
|
|
37
43
|
fc = ";".join(filters)
|
|
@@ -63,9 +69,16 @@ def main(argv=None):
|
|
|
63
69
|
ap.add_argument("--width", type=int, default=1920)
|
|
64
70
|
ap.add_argument("--height", type=int, default=1080)
|
|
65
71
|
ap.add_argument("--fps", type=int, default=30)
|
|
72
|
+
ap.add_argument("--lut", help="3D LUT (.cube) applied after downscale")
|
|
73
|
+
ap.add_argument("--log", help="built-in log->Rec.709 grade (e.g. slog3)")
|
|
74
|
+
ap.add_argument("--rotate", action="append",
|
|
75
|
+
help="per-cam rotation CAM:DEG (90=CW), repeatable")
|
|
76
|
+
ap.add_argument("--fill", action="store_true",
|
|
77
|
+
help="crop to fill instead of letterbox-pad (use for vertical)")
|
|
66
78
|
args = ap.parse_args(argv)
|
|
67
79
|
render_cuts(args.edl, args.out, encoder=args.encoder, bitrate=args.bitrate,
|
|
68
|
-
width=args.width, height=args.height, fps=args.fps
|
|
80
|
+
width=args.width, height=args.height, fps=args.fps,
|
|
81
|
+
lut=args.lut, log=args.log, rotate=args.rotate, fill=args.fill)
|
|
69
82
|
|
|
70
83
|
|
|
71
84
|
if __name__ == "__main__":
|
|
@@ -11,6 +11,8 @@ import json
|
|
|
11
11
|
import subprocess
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
+
from .grade import resolve_lut, parse_rotate, _transpose_chain
|
|
15
|
+
|
|
14
16
|
POSITIONS = {
|
|
15
17
|
"bottom-right": ("W-w-{m}", "H-h-{m}"),
|
|
16
18
|
"top-right": ("W-w-{m}", "{m}"),
|
|
@@ -41,7 +43,7 @@ def pick_pip(row, K, coverage, mode="next"):
|
|
|
41
43
|
def render_pip(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
42
44
|
width=1920, height=1080, fps=30, pip="bottom-right",
|
|
43
45
|
pip_width=480, pip_margin=24, border_px=4, pip_pick="next",
|
|
44
|
-
run=True):
|
|
46
|
+
lut=None, log=None, rotate=None, run=True):
|
|
45
47
|
plan = json.loads(Path(edl_path).read_text())
|
|
46
48
|
inputs = plan["inputs"]
|
|
47
49
|
deltas = plan.get("deltas", [0.0] * len(inputs))
|
|
@@ -49,6 +51,9 @@ def render_pip(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
|
49
51
|
audio_src = plan["audio_source"]
|
|
50
52
|
K = len(inputs)
|
|
51
53
|
coverage = plan.get("coverage", [[0.0, plan["duration_sec"]]] * K)
|
|
54
|
+
lut_path = resolve_lut(lut, log)
|
|
55
|
+
rot = parse_rotate(rotate)
|
|
56
|
+
grade = ("lut3d=%s," % lut_path) if lut_path else ""
|
|
52
57
|
|
|
53
58
|
W, H = width, height
|
|
54
59
|
pw = pip_width
|
|
@@ -70,10 +75,11 @@ def render_pip(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
|
70
75
|
s, e = row["start"], row["end"]
|
|
71
76
|
main_label = "m%d" % i if K > 1 else "v%d" % i
|
|
72
77
|
filters.append(
|
|
73
|
-
"[%d:v]trim=start=%s:end=%s,setpts=PTS-STARTPTS
|
|
78
|
+
"[%d:v]trim=start=%s:end=%s,setpts=PTS-STARTPTS,%s"
|
|
74
79
|
"scale=%d:%d:force_original_aspect_ratio=decrease,"
|
|
75
|
-
"pad=%d:%d:(ow-iw)/2:(oh-ih)/2
|
|
76
|
-
% (cam, s, e,
|
|
80
|
+
"pad=%d:%d:(ow-iw)/2:(oh-ih)/2,%ssetsar=1,fps=%d[%s]"
|
|
81
|
+
% (cam, s, e, _transpose_chain(rot.get(cam, 0)),
|
|
82
|
+
W, H, W, H, grade, fps, main_label)
|
|
77
83
|
)
|
|
78
84
|
if K == 1:
|
|
79
85
|
continue
|
|
@@ -82,10 +88,11 @@ def render_pip(edl_path, out, encoder="hevc_videotoolbox", bitrate="12M",
|
|
|
82
88
|
filters.append("[m%d]copy[v%d]" % (i, i))
|
|
83
89
|
continue
|
|
84
90
|
chain = (
|
|
85
|
-
"[%d:v]trim=start=%s:end=%s,setpts=PTS-STARTPTS
|
|
91
|
+
"[%d:v]trim=start=%s:end=%s,setpts=PTS-STARTPTS,%s"
|
|
86
92
|
"scale=%d:%d:force_original_aspect_ratio=decrease,"
|
|
87
|
-
"pad=%d:%d:(ow-iw)/2:(oh-ih)/2
|
|
88
|
-
% (pip_cam, s, e,
|
|
93
|
+
"pad=%d:%d:(ow-iw)/2:(oh-ih)/2,%s"
|
|
94
|
+
% (pip_cam, s, e, _transpose_chain(rot.get(pip_cam, 0)),
|
|
95
|
+
pw, ph, pw, ph, grade)
|
|
89
96
|
)
|
|
90
97
|
if bw > 0:
|
|
91
98
|
chain += "pad=%d:%d:%d:%d:white," % (pw + 2 * bw, ph + 2 * bw, bw, bw)
|
|
@@ -130,11 +137,16 @@ def main(argv=None):
|
|
|
130
137
|
ap.add_argument("--pip-margin", type=int, default=24)
|
|
131
138
|
ap.add_argument("--border-px", type=int, default=4)
|
|
132
139
|
ap.add_argument("--pip-pick", choices=["next", "second-best"], default="next")
|
|
140
|
+
ap.add_argument("--lut", help="3D LUT (.cube) applied after downscale")
|
|
141
|
+
ap.add_argument("--log", help="built-in log->Rec.709 grade (e.g. slog3)")
|
|
142
|
+
ap.add_argument("--rotate", action="append",
|
|
143
|
+
help="per-cam rotation CAM:DEG (90=CW), repeatable")
|
|
133
144
|
args = ap.parse_args(argv)
|
|
134
145
|
render_pip(args.edl, args.out, encoder=args.encoder, bitrate=args.bitrate,
|
|
135
146
|
width=args.width, height=args.height, fps=args.fps, pip=args.pip,
|
|
136
147
|
pip_width=args.pip_width, pip_margin=args.pip_margin,
|
|
137
|
-
border_px=args.border_px, pip_pick=args.pip_pick
|
|
148
|
+
border_px=args.border_px, pip_pick=args.pip_pick,
|
|
149
|
+
lut=args.lut, log=args.log, rotate=args.rotate)
|
|
138
150
|
|
|
139
151
|
|
|
140
152
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polysync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -15,6 +15,7 @@ src/polysync.egg-info/requires.txt
|
|
|
15
15
|
src/polysync.egg-info/top_level.txt
|
|
16
16
|
src/polysync/edit/__init__.py
|
|
17
17
|
src/polysync/edit/autoedit.py
|
|
18
|
+
src/polysync/edit/grade.py
|
|
18
19
|
src/polysync/edit/render_cuts.py
|
|
19
20
|
src/polysync/edit/render_pip.py
|
|
20
21
|
tests/test_sync_synthetic.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
|