Download waveform.py from sandpies/ComfyUI-Hand-Tie-Clips: direct link, hf CLI and curl.
- Browser
- Download file 4.25 kB
-
https://huggingface.co/sandpies/ComfyUI-Hand-Tie-Clips/resolve/main/waveform.py
- Command line
-
hf download hf://sandpies/ComfyUI-Hand-Tie-Clips/waveform.py
-
curl -L -o waveform.py https://huggingface.co/sandpies/ComfyUI-Hand-Tie-Clips/resolve/main/waveform.py
4.25 kB
| """Peaks for the trim bar, computed here rather than in the browser. | |
| The obvious way to draw a waveform in a panel is to fetch the file and run it | |
| through `AudioContext.decodeAudioData`. That was the first design and it is | |
| wrong at this scale: a 173 s stereo file decodes to roughly 66 MB of Float32 in | |
| the tab, per control, for a picture 240 pixels wide. PromptMasterLD reaches the | |
| same conclusion from the other direction -- it has four separate trim controls | |
| and not one `decodeAudioData` call between them. | |
| So the server sends 240 numbers. The browser holds 240 numbers. The decode | |
| happens once, on a thread, behind an mtime-keyed cache. | |
| Three details are load-bearing: | |
| **Bucket by max, not by mean.** A mean flattens transients into a smooth | |
| sausage, and transients are the only landmarks you can trim against -- the | |
| whole point of looking at the picture is to find the downbeat or the start of | |
| the sentence. | |
| **`seconds` comes from the decoded sample count.** MP3 Xing/LAME headers | |
| routinely report double the real duration, and a duration that lies makes every | |
| position on the bar lie with it. We already decode, so the honest number is | |
| free. | |
| **No ComfyUI import.** Same rule as `music.py`, `plan.py` and `refs.py`: the DSP | |
| is testable on a CPU with no server, which is what `tools/check_waveform.py` | |
| does. | |
| """ | |
| from __future__ import annotations | |
| TAG = "HandTieClips" | |
| # 240 is PromptMasterLD's default and it is a good one: a trim bar is a few | |
| # hundred CSS pixels wide, so more buckets than this buys nothing you can see | |
| # and costs JSON on every panel open. | |
| DEFAULT_N = 240 | |
| MIN_N = 24 | |
| MAX_N = 600 | |
| def reduce_peaks(mono, n=DEFAULT_N): | |
| """A 1-D waveform -> `n` bucket maxima in 0..1. | |
| `mono` is any 1-D sequence of floats (a torch tensor, a numpy array, a | |
| list). Returns a plain list so the caller can hand it straight to | |
| `json_response` without a tensor library in the loop. | |
| """ | |
| n = max(MIN_N, min(MAX_N, int(n or DEFAULT_N))) | |
| total = len(mono) | |
| if total <= 0: | |
| return [0.0] * n | |
| import torch # noqa: PLC0415 | |
| x = mono if isinstance(mono, torch.Tensor) else torch.as_tensor(mono) | |
| x = x.detach().to(torch.float32).abs().flatten() | |
| # Pad up to a whole number of buckets rather than looping in Python. A | |
| # 173 s file is 8.3M samples and a per-bucket slice in the interpreter is | |
| # measurable; one reshape and one max over a dim is not. | |
| per = max(1, total // n) | |
| keep = per * n | |
| if keep < total: | |
| # Fold the remainder into the last bucket instead of dropping it, or a | |
| # transient in the final fraction of a second would vanish from the | |
| # picture while still being audible in the file. | |
| head = x[:keep].reshape(n, per).amax(dim=1) | |
| head[-1] = torch.maximum(head[-1], x[keep:].amax()) | |
| buckets = head | |
| else: | |
| buckets = x[:keep].reshape(n, per).amax(dim=1) | |
| top = float(buckets.max()) | |
| if top > 0: | |
| buckets = buckets / top | |
| # 3 dp is well past what a 240 px canvas can render, and it keeps the JSON | |
| # payload around 1.5 KB instead of 5 KB. | |
| return [round(float(v), 3) for v in buckets] | |
| def peaks(audio, n=DEFAULT_N): | |
| """A ComfyUI AUDIO dict -> `(peaks, seconds)`. | |
| `audio` is what `media.load_audio` returns: `{"waveform": [B,C,S], | |
| "sample_rate": int}`. A missing or empty take returns a flat line rather | |
| than raising, so a panel drawing a control for a file that has gone missing | |
| shows an empty bar instead of an error. | |
| """ | |
| if not audio: | |
| return [0.0] * max(MIN_N, min(MAX_N, int(n or DEFAULT_N))), 0.0 | |
| import torch # noqa: PLC0415 | |
| wav = audio.get("waveform") | |
| sr = int(audio.get("sample_rate") or 0) | |
| if wav is None or sr <= 0: | |
| return [0.0] * max(MIN_N, min(MAX_N, int(n or DEFAULT_N))), 0.0 | |
| x = wav if isinstance(wav, torch.Tensor) else torch.as_tensor(wav) | |
| while x.dim() > 1: | |
| # Mean across batch and channels, not sum: summing two correlated | |
| # channels doubles the amplitude and the normalise below would hide it, | |
| # but a mono file and its own stereo copy should draw identically. | |
| x = x.mean(dim=0) | |
| samples = int(x.shape[-1]) | |
| return reduce_peaks(x, n), samples / float(sr) | |