phasecheck 0.1.0__py3-none-any.whl

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.
phasecheck/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """phasecheck: do the two channels of a stereo file cancel each other when mixed down to mono?"""
2
+ from ._version import __version__
3
+ from .check import Verdict, check_file, judge
4
+
5
+ __all__ = ["__version__", "Verdict", "check_file", "judge"]
phasecheck/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
phasecheck/analysis.py ADDED
@@ -0,0 +1,106 @@
1
+ """Stereo audio -> how much of it survives a mono downmix.
2
+
3
+ Mixing a stereo file to mono (the average of the two channels) keeps sound that is the same in both channels and
4
+ cancels sound that is opposite in the two channels. A file whose channels are partly or wholly out of phase can lose
5
+ level or vanish when a training pipeline converts it to mono. phasecheck measures that loss.
6
+
7
+ downmix_loss_db: 0 dB when the channels are identical (nothing is lost), about 3 dB when they are unrelated (the normal
8
+ case for wide stereo), and large when they are in opposite phase (the mono mix is much quieter than the channels).
9
+ Light on purpose: numpy, scipy, soundfile only.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass, field
14
+ from typing import Dict, List, Optional
15
+
16
+ import numpy as np
17
+ import soundfile as sf
18
+ from scipy.signal import stft
19
+
20
+ MAX_SECONDS = 120.0 # long files: only the middle 2 minutes are used
21
+ MIN_SECONDS = 2.0
22
+ ACTIVE_FLOOR_DB = -30.0 # frames more than 30 dB below the loudest frame are ignored
23
+ MIN_ACTIVE_FRAMES = 4
24
+ NPERSEG = 2048
25
+ BANDS_HZ = ((20.0, 250.0), (250.0, 2000.0), (2000.0, 8000.0), (8000.0, 1e9))
26
+ BAND_NAMES = ("20-250 Hz", "250 Hz-2 kHz", "2-8 kHz", "above 8 kHz")
27
+ MIN_BAND_SHARE_DB = -20.0 # a band counts only if it holds at least 1% of the energy
28
+ SILENT_CHANNEL_DB = -60.0 # a channel this far below the other is treated as silent (wavewash finds dead channels)
29
+ LOSS_CAP_DB = 60.0
30
+ IDENTICAL_DB = 0.2 # channels are "identical" if the mono mix loses less than this and their correlation is above 0.9999
31
+
32
+
33
+ @dataclass
34
+ class PhaseResult:
35
+ sample_rate: int
36
+ seconds: float
37
+ active_seconds: float
38
+ loss_db: float # full-band downmix loss
39
+ correlation: float # full-band, -1 (opposite) .. +1 (identical)
40
+ band_loss_db: Dict[str, Optional[float]] = field(default_factory=dict) # None for a band that is empty or holds too little energy
41
+ dual_mono: bool = False # both channels identical (mono stored as stereo)
42
+ silent_channel: Optional[int] = None # 0 or 1 if one channel is silent next to the other
43
+
44
+
45
+ def load_stereo(path, max_seconds: float = MAX_SECONDS):
46
+ """Reads the file at its own sample rate -> (array of shape (samples, channels), sample rate). Long files: the middle part."""
47
+ x, sr = sf.read(str(path), dtype="float64", always_2d=True)
48
+ n = int(max_seconds * sr)
49
+ if len(x) > n:
50
+ s = (len(x) - n) // 2
51
+ x = x[s:s + n]
52
+ return x, int(sr)
53
+
54
+
55
+ def _loss_db(p_mono: float, p_l: float, p_r: float) -> float:
56
+ mean_ch = (p_l + p_r) / 2
57
+ if mean_ch <= 0:
58
+ return 0.0
59
+ return float(min(LOSS_CAP_DB, -10 * np.log10(max(p_mono, mean_ch * 10 ** (-LOSS_CAP_DB / 10)) / mean_ch)))
60
+
61
+
62
+ def analyse(left: np.ndarray, right: np.ndarray, sr: int) -> Optional[PhaseResult]:
63
+ """Downmix loss of a stereo pair, measured over its loud parts. None if too short, silent or too few loud frames."""
64
+ n = min(len(left), len(right))
65
+ if n < MIN_SECONDS * sr:
66
+ return None
67
+ left, right = left[:n], right[:n]
68
+ if not (np.any(np.abs(left) > 1e-9) or np.any(np.abs(right) > 1e-9)):
69
+ return None
70
+ nper = min(NPERSEG, 2 ** int(np.log2(max(n // 4, 256))))
71
+ f, _, ZL = stft(left, fs=sr, nperseg=nper, noverlap=nper // 2, boundary=None, padded=False)
72
+ _, _, ZR = stft(right, fs=sr, nperseg=nper, noverlap=nper // 2, boundary=None, padded=False)
73
+ PL, PR = np.abs(ZL) ** 2, np.abs(ZR) ** 2
74
+ PM = np.abs(ZL + ZR) ** 2 / 4
75
+ cross = np.real(ZL * np.conj(ZR))
76
+ energy = PL.sum(axis=0) + PR.sum(axis=0)
77
+ if energy.max() <= 0:
78
+ return None
79
+ active = energy > energy.max() * 10 ** (ACTIVE_FLOOR_DB / 10)
80
+ if active.sum() < MIN_ACTIVE_FRAMES:
81
+ return None
82
+ PL, PR, PM, cross = PL[:, active], PR[:, active], PM[:, active], cross[:, active]
83
+ hop = nper // 2
84
+ tl, tr, tm = PL.sum(), PR.sum(), PM.sum()
85
+ silent = None
86
+ if tl > 0 and 10 * np.log10(max(tr, 1e-30) / tl) < SILENT_CHANNEL_DB:
87
+ silent = 1
88
+ elif tr > 0 and 10 * np.log10(max(tl, 1e-30) / tr) < SILENT_CHANNEL_DB:
89
+ silent = 0
90
+ denom = np.sqrt(tl * tr)
91
+ corr = float(cross.sum() / denom) if denom > 0 else 0.0
92
+ loss = _loss_db(tm, tl, tr)
93
+ total = tl + tr
94
+ bands: Dict[str, Optional[float]] = {}
95
+ for (lo, hi), name in zip(BANDS_HZ, BAND_NAMES):
96
+ sel = (f >= lo) & (f < min(hi, sr / 2 + 1))
97
+ if not sel.any():
98
+ bands[name] = None
99
+ continue
100
+ bl, br, bm = PL[sel].sum(), PR[sel].sum(), PM[sel].sum()
101
+ if total <= 0 or 10 * np.log10(max(bl + br, 1e-30) / total) < MIN_BAND_SHARE_DB:
102
+ bands[name] = None
103
+ continue
104
+ bands[name] = _loss_db(bm, bl, br)
105
+ dual = loss < IDENTICAL_DB and corr > 0.9999 and abs(10 * np.log10(max(tl, 1e-30) / max(tr, 1e-30))) < 0.5
106
+ return PhaseResult(sr, n / sr, float(active.sum() * hop / sr), loss, float(np.clip(corr, -1, 1)), bands, bool(dual), silent)
phasecheck/check.py ADDED
@@ -0,0 +1,71 @@
1
+ """Turn a downmix measurement into a verdict."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Dict, List, Optional
7
+
8
+ from .analysis import PhaseResult, analyse, load_stereo
9
+
10
+ WARN_DB = 6.0 # full-band downmix loss at or above this: WARN (provisional; not yet tuned on real recordings)
11
+ FAIL_DB = 10.0 # at or above this: FAIL
12
+ BAND_FAIL_DB = 10.0 # one frequency band (holding at least 1% of the energy) at or above this: WARN
13
+
14
+ OK, LIMITED, FAKE, UNSURE, SKIPPED, ERROR = "OK", "LIMITED", "FAKE", "UNSURE", "SKIPPED", "ERROR"
15
+
16
+
17
+ @dataclass
18
+ class Verdict:
19
+ file: str
20
+ sample_rate: Optional[int]
21
+ channels: int
22
+ status: str
23
+ reason: str
24
+ loss_db: Optional[float] = None
25
+ correlation: Optional[float] = None
26
+ worst_band: str = ""
27
+ worst_band_loss_db: Optional[float] = None
28
+ dual_mono: bool = False
29
+ seconds: float = 0.0
30
+
31
+ @property
32
+ def level(self) -> str:
33
+ return {OK: "PASS", LIMITED: "WARN", UNSURE: "REVIEW", FAKE: "FAIL", SKIPPED: "SKIP", ERROR: "ERROR"}[self.status]
34
+
35
+
36
+ def judge(res: Optional[PhaseResult], file: str = "", sample_rate: Optional[int] = None, channels: int = 2,
37
+ warn_db: float = WARN_DB, fail_db: float = FAIL_DB) -> Verdict:
38
+ if res is None:
39
+ return Verdict(file, sample_rate, channels, UNSURE, "too short, silent or too few loud parts to measure")
40
+ base = dict(file=file, sample_rate=res.sample_rate, channels=channels, loss_db=round(res.loss_db, 1),
41
+ correlation=round(res.correlation, 3), seconds=res.active_seconds)
42
+ present = {k: v for k, v in res.band_loss_db.items() if v is not None}
43
+ wb, wl = max(present.items(), key=lambda kv: kv[1]) if present else ("", None)
44
+ base.update(worst_band=wb, worst_band_loss_db=None if wl is None else round(wl, 1), dual_mono=res.dual_mono)
45
+ if res.silent_channel is not None:
46
+ side = "left" if res.silent_channel == 0 else "right"
47
+ return Verdict(status=UNSURE, reason=f"the {side} channel is silent next to the other, so there is nothing to cancel (a dead channel is wavewash's job)", **base)
48
+ if res.dual_mono:
49
+ return Verdict(status=OK, reason="both channels are identical (mono stored as stereo); the mono mix loses nothing", **base)
50
+ if res.loss_db >= fail_db:
51
+ why = " The channels are close to opposite in phase." if res.correlation <= -0.5 else ""
52
+ return Verdict(status=FAKE, reason=f"a mono mix is {res.loss_db:.1f} dB quieter than the channels (correlation {res.correlation:.2f}).{why}", **base)
53
+ if res.loss_db >= warn_db:
54
+ return Verdict(status=LIMITED, reason=f"a mono mix is {res.loss_db:.1f} dB quieter than the channels (correlation {res.correlation:.2f}); partly out of phase", **base)
55
+ if wl is not None and wl >= BAND_FAIL_DB:
56
+ return Verdict(status=LIMITED, reason=f"the {wb} band cancels in a mono mix ({wl:.1f} dB loss) although the whole file does not", **base)
57
+ return Verdict(status=OK, reason=f"a mono mix loses {res.loss_db:.1f} dB (correlation {res.correlation:.2f}); nothing cancels", **base)
58
+
59
+
60
+ def check_file(path, warn_db: float = WARN_DB, fail_db: float = FAIL_DB) -> Verdict:
61
+ p = Path(path)
62
+ try:
63
+ x, sr = load_stereo(p)
64
+ except Exception as e: # unreadable audio
65
+ return Verdict(p.name, None, 0, ERROR, f"could not read audio: {e}")
66
+ ch = x.shape[1]
67
+ if ch == 1:
68
+ return Verdict(p.name, sr, 1, SKIPPED, "mono file; nothing to check")
69
+ if ch > 2:
70
+ return Verdict(p.name, sr, ch, SKIPPED, f"{ch} channels; only two-channel files are checked")
71
+ return judge(analyse(x[:, 0], x[:, 1], sr), p.name, sr, 2, warn_db, fail_db)
phasecheck/cli.py ADDED
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from concurrent.futures import ProcessPoolExecutor
6
+ from functools import partial
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+ from .check import FAIL_DB, WARN_DB, check_file
11
+ from .report import text, to_json, write_csv
12
+
13
+ AUDIO_EXT = {".wav", ".flac", ".mp3", ".ogg", ".m4a", ".aiff", ".aif"}
14
+
15
+
16
+ def find_audio(source: Path):
17
+ if source.is_file():
18
+ return [source]
19
+ return sorted(p for p in source.rglob("*") if p.suffix.lower() in AUDIO_EXT and not p.name.startswith("._") and "__MACOSX" not in p.parts)
20
+
21
+
22
+ def main(argv=None) -> int:
23
+ ap = argparse.ArgumentParser(prog="phasecheck", description="Find stereo audio files whose channels cancel each other in a mono downmix.")
24
+ ap.add_argument("--version", action="version", version=f"phasecheck {__version__}")
25
+ sub = ap.add_subparsers(dest="cmd", required=True)
26
+ c = sub.add_parser("check", help="check one file or every audio file in a folder")
27
+ c.add_argument("source", type=Path)
28
+ c.add_argument("--warn-db", type=float, default=WARN_DB, help=f"downmix loss that earns a WARN (default {WARN_DB:g} dB)")
29
+ c.add_argument("--fail-db", type=float, default=FAIL_DB, help=f"downmix loss that earns a FAIL (default {FAIL_DB:g} dB)")
30
+ c.add_argument("--out", type=Path, help="write every verdict to this CSV")
31
+ c.add_argument("--json", action="store_true", help="print the full report as JSON (instead of text)")
32
+ c.add_argument("--show-pass", action="store_true", help="also list files that passed or were skipped")
33
+ c.add_argument("--jobs", type=int, default=1, help="files analysed in parallel (default 1)")
34
+ a = ap.parse_args(argv)
35
+
36
+ if not a.source.exists():
37
+ print(f"error: {a.source} not found", file=sys.stderr)
38
+ return 2
39
+ if a.warn_db > a.fail_db:
40
+ print("error: --warn-db must not be larger than --fail-db", file=sys.stderr)
41
+ return 2
42
+ files = find_audio(a.source)
43
+ if not files:
44
+ print("error: no audio files found", file=sys.stderr)
45
+ return 2
46
+ job = partial(check_file, warn_db=a.warn_db, fail_db=a.fail_db)
47
+ if a.jobs > 1:
48
+ with ProcessPoolExecutor(a.jobs) as ex:
49
+ vs = list(ex.map(job, files, chunksize=1))
50
+ else:
51
+ vs = [job(f) for f in files]
52
+ print(to_json(vs) if a.json else text(vs, a.show_pass))
53
+ if a.out:
54
+ write_csv(a.out, vs)
55
+ return 1 if any(v.level == "FAIL" for v in vs) else 0
56
+
57
+
58
+ if __name__ == "__main__":
59
+ sys.exit(main())
phasecheck/report.py ADDED
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import json
5
+ from collections import Counter
6
+ from typing import List
7
+
8
+ from . import __version__
9
+ from .check import Verdict
10
+
11
+ EVIDENCE = ("alpha. Tested on generated audio only; not yet measured on real recordings. The 6 dB and 10 dB limits are provisional. "
12
+ "See the README.")
13
+ LEVELS = ("PASS", "WARN", "FAIL", "REVIEW", "SKIP", "ERROR")
14
+
15
+
16
+ def summary_line(vs: List[Verdict]) -> str:
17
+ c = Counter(v.level for v in vs)
18
+ return (f"{len(vs)} files: {c['PASS']} pass, {c['WARN']} warn (partly out of phase), {c['FAIL']} fail (cancel in a mono mix), "
19
+ f"{c['REVIEW']} need a look, {c['SKIP']} skipped (not two-channel), {c['ERROR']} unreadable")
20
+
21
+
22
+ def text(vs: List[Verdict], show_pass: bool = False) -> str:
23
+ L = []
24
+ for v in vs:
25
+ if v.level in ("PASS", "SKIP") and not show_pass:
26
+ continue
27
+ L.append(f"{v.level:6} {v.file} {v.sample_rate or '-'} Hz {v.reason}")
28
+ return "\n".join(L + ([""] if L else []) + [summary_line(vs)])
29
+
30
+
31
+ def _row(v: Verdict) -> dict:
32
+ return {"file": v.file, "sample_rate": v.sample_rate, "channels": v.channels, "verdict": v.level, "reason": v.reason,
33
+ "downmix_loss_db": v.loss_db, "correlation": v.correlation, "worst_band": v.worst_band,
34
+ "worst_band_loss_db": v.worst_band_loss_db, "dual_mono": v.dual_mono, "seconds_analysed": round(v.seconds, 1)}
35
+
36
+
37
+ def write_csv(path, vs: List[Verdict]) -> None:
38
+ rows = [_row(v) for v in vs]
39
+ cols = list(_row(Verdict("", None, 0, "OK", "")).keys())
40
+ with open(path, "w", newline="", encoding="utf-8") as f:
41
+ w = csv.writer(f)
42
+ w.writerow(cols)
43
+ for r in rows:
44
+ w.writerow(["" if r[c] is None else r[c] for c in cols])
45
+
46
+
47
+ def to_dict(vs: List[Verdict]) -> dict:
48
+ c = Counter(v.level for v in vs)
49
+ return {"tool": "phasecheck", "version": __version__, "evidence": EVIDENCE,
50
+ "summary": {"files": len(vs), **{k: c[k] for k in LEVELS}}, "files": [_row(v) for v in vs]}
51
+
52
+
53
+ def to_json(vs: List[Verdict]) -> str:
54
+ return json.dumps(to_dict(vs), indent=2)
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: phasecheck
3
+ Version: 0.1.0
4
+ Summary: Find stereo audio files whose channels cancel each other when mixed down to mono (out-of-phase or inverted channels).
5
+ Author-email: Boobalan Arjunan <boobalan_arjunan@composermind.com>
6
+ Maintainer-email: Boobalan Arjunan <boobalan_arjunan@composermind.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/BoobalanArjunan/Phasecheck
9
+ Project-URL: Issues, https://github.com/BoobalanArjunan/Phasecheck/issues
10
+ Project-URL: Source, https://github.com/BoobalanArjunan/Phasecheck
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy>=1.23
18
+ Requires-Dist: scipy>=1.9
19
+ Requires-Dist: soundfile>=0.12
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # phasecheck
25
+
26
+ Finds stereo files that lose level or sound thin when played in mono, usually because one channel is polarity-inverted
27
+ or partly out of phase with the other.
28
+
29
+ > **Status: 0.1.0.** Checked on real stereo music (MUSDB18-HQ) with planted problems and untouched originals; results below.
30
+ > The thresholds (6 dB and 10 dB) were set on generated audio and are unchanged. They are provisional.
31
+
32
+ ```bash
33
+ pip install -e ".[dev]"
34
+ phasecheck check my_songs/ --out report.csv
35
+ phasecheck check song.wav --json
36
+ ```
37
+
38
+ ## What it measures
39
+ Over the loud parts of the file (frames within 30 dB of the loudest, middle 2 minutes of long files):
40
+
41
+ - **Downmix loss (dB):** how much quieter the mono sum is than the average of the two channels. 0 dB means identical channels,
42
+ about 3 dB means unrelated channels, large values mean the channels cancel.
43
+ - **Correlation** between the channels (-1 to 1), and the loss in four bands (20-250 Hz, 250 Hz-2 kHz, 2-8 kHz, above 8 kHz;
44
+ a band counts only if it holds at least 1% of the energy).
45
+ - **Dual mono** (identical channels) and a **silent channel**.
46
+
47
+ | Verdict | When |
48
+ |---|---|
49
+ | FAIL | full-band loss of 10 dB or more |
50
+ | WARN | loss of 6 dB or more, or one band at 10 dB or more |
51
+ | REVIEW | one channel is silent, or too little audio to judge |
52
+ | SKIP | mono, or more than two channels |
53
+ | PASS | otherwise (identical channels are noted) |
54
+
55
+ Exit code 1 only when a FAIL exists; 2 for a bad path or settings.
56
+
57
+ ## Real-data results (MUSDB18-HQ, 200 files per run)
58
+ Two runs on different songs: `test` split first, then `train`. The plan for each was fixed and tagged before the run
59
+ (`eval-spec-pc-v1`, `eval-spec-pc-v2`; see `docs/`). **The first run failed its plan** (inverted-channel copies were expected
60
+ to reach FAIL in 99% of cases; they reached 54%). The criterion was wrong, because how much an inversion loses depends on how
61
+ correlated the channels were; the v2 plan corrected it and was run once on new songs, with no code or threshold change.
62
+
63
+ | `train` split (v2 run) | result |
64
+ |---|---|
65
+ | Untouched originals flagged WARN or FAIL | 1 of 200 (range 0-3%) |
66
+ | Right channel inverted, original correlation 0.5 or more: flagged WARN or FAIL | 168 of 169 = 99% (97-100%) |
67
+ | Same copies reaching FAIL (10 dB or more) | 128 of 169 = 76% |
68
+ | Same inversion, original correlation under 0.5 | 1 of 11 flagged: **not reliably detectable** |
69
+ | Right channel inverted, all copies | 169 of 180 = 94% flagged, 128 of 180 = 71% FAIL |
70
+ | Only full mixtures | 39 of 39 flagged, 28 of 39 FAIL |
71
+ | 2-8 kHz band inverted | 62 of 71 = 87% (78-93%) |
72
+ | Dual mono recognised | 180 of 180 |
73
+
74
+ On the first (`test`) run: 0 of 199 originals flagged; all-copies detection 180 of 194 = 93% flagged, 104 of 194 = 54% FAIL; band 31 of 38 = 82%.
75
+
76
+ The one flagged original (train: a vocals stem, correlation -0.79) measures 9.7 dB quieter in a mono mix (bass about 19.5 dB quieter); not yet checked by ear.
77
+
78
+ Ranges are 95% Wilson intervals. Stems and mixtures of the same song are not independent, so the real uncertainty is wider.
79
+ The planted copies are made by one method; real polarity mistakes may differ. Only MUSDB18-HQ was used.
80
+
81
+ ## Limits (please read)
82
+ - **One dataset.** Tested on MUSDB18-HQ only (western-style studio stems and mixtures). Field recordings, spaced microphones and other styles may behave differently. Wide stereo can have low
83
+ correlation without any mono problem; that is why the verdict uses the downmix loss, not the correlation.
84
+ - **Swapped left and right channels cannot be detected.** Nothing in the audio says which side is which.
85
+ - **A passage that cancels is averaged into the whole file.** A short out-of-phase section in a long song may not reach WARN.
86
+ - A flagged file is not proof of a mistake: some recordings really cancel in mono. Listen to the mono mix.
87
+ - Only the middle 2 minutes of a long file are judged.
88
+ - Inverted channels whose correlation was under about 0.5 are not reliably caught: the mono mix hardly changes.
89
+ - An inverted mixture usually shows as WARN, not FAIL (about 72% of inverted mixtures reached FAIL).
90
+ - Thresholds (6 / 10 dB) are provisional and were chosen on generated audio.
91
+
92
+ ## Tests
93
+ `pytest -q` (57 tests). The checker itself was also tested by planting 21 deliberate bugs in the code and confirming
94
+ the tests catch them (the two that survived the first round now have their own tests).
95
+
96
+ ## License
97
+
98
+ MIT. Copyright (c) 2026 Boobalan Arjunan.
@@ -0,0 +1,12 @@
1
+ phasecheck/__init__.py,sha256=NU8jM3FGHirdNrMciCC8ZWgh15pDXl4ZpUCyNWJdkHc,239
2
+ phasecheck/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
3
+ phasecheck/analysis.py,sha256=p0jV7DwCJkgAAaX7Zxoh2wFx8Jd9VYQyui5YDmHmZ54,5065
4
+ phasecheck/check.py,sha256=br7d5bpWIM9eAzzA-1TbiE5n7ZkP4Q1AqgeOKUQXhOk,3712
5
+ phasecheck/cli.py,sha256=vfOcAytojpBBiGwbE6ZlEhEVxIYTztMZD2VEzYZpRKM,2518
6
+ phasecheck/report.py,sha256=gBf0eSlaVj84E41UlOMD-18rK4A4qRYeV0E-FpvVOcQ,2109
7
+ phasecheck-0.1.0.dist-info/licenses/LICENSE,sha256=brmceFF8Uw_1KeQBOshibuDU12xBGtsi2Y4mzSoRlgE,1462
8
+ phasecheck-0.1.0.dist-info/METADATA,sha256=IW7kNF1rT1jX7K6yOPWrCL2Hs-w8nwFtD9IhdDXP0YM,5383
9
+ phasecheck-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
10
+ phasecheck-0.1.0.dist-info/entry_points.txt,sha256=NStxGJbjeumUbBfrkE0neQmGptOwt-bfySbwKFPTQc0,51
11
+ phasecheck-0.1.0.dist-info/top_level.txt,sha256=zFXbCFvHByiGDVFqyoUcgXHkpj43SYGFTQWN2TQyG1Q,11
12
+ phasecheck-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ phasecheck = phasecheck.cli:main
@@ -0,0 +1,29 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Boobalan Arjunan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+ This project depends on third-party libraries (numpy, scipy, soundfile,
25
+ librosa, pyloudnorm, scikit-learn, tqdm, langdetect, matplotlib, and
26
+ optionally openai-whisper, torch/torchaudio). Each carries its own license
27
+ (mostly BSD/MIT/Apache-2.0) -- check requirements.txt and each package's own
28
+ license before redistributing, especially if your GTV program has its own
29
+ licensing rules.
@@ -0,0 +1 @@
1
+ phasecheck