Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

| Version | Supported |
| -------- | ------------------ |
| 1.2.x | :white_check_mark: |
| < 1.2.0 | :x: |
| 1.3.x | :white_check_mark: |
| < 1.3.0 | :x: |

## Reporting a Vulnerability

Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
## v1.3.0 (2026-06-05) — Visual HTML report: see the spectral cliff

Until now the reports told you *which* files were suspect (text/csv) or handed the
raw numbers to a script (json). This release adds a way to **see why** — a single,
self-contained HTML page you double-click to open.

- **New `--format html`.** Writes one `.html` file (no external assets, no extra
dependency) with two parts: a **sortable, filterable triage table** (click a column
to sort, click a verdict to filter), and — for every flagged file — a **detail card
with an inline spectrum plot**. The plot is the real FFT magnitude (dB, peak-normalised)
of a 10 s middle segment, with the detected cutoff marked, so the MP3 **"cliff"** (a
sharp drop well below Nyquist) is visible to the eye rather than inferred from a score.
- **Lightweight by design.** The curve is computed with numpy (already a core dependency)
and drawn as a hand-rolled inline `<svg>` polyline — no matplotlib, no PNGs, no base64
blobs. The core analysis path is **untouched**: the spectrum is recomputed at report
time and **only for flagged files** (typically a handful), so the per-file result dict
carries no extra payload and the json/csv reports stay lean.
- **Graceful degradation.** A file that isn't natively readable (e.g. ALAC/APE without
ffmpeg, or an unreadable file) simply shows no plot — its table row and facts are still
there. The report never fails because one curve couldn't render.

New `reporting/html_reporter.py` (`HTMLReporter`, exported from
`flac_detective.reporting`) + tests in `tests/test_html_reporter.py`. Backward
compatible; no detection-logic change — `text`, `json` and `csv` are unchanged.

## v1.2.0 (2026-06-04) — Deep mode: catching high-bitrate AAC/Vorbis transcodes

The tool's documented blind spot — high-bitrate AAC, Opus and Vorbis transcodes —
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ authors:
repository-code: "https://github.com/Guillain-RDCDE/FLAC_Detective"
url: "https://github.com/Guillain-RDCDE/FLAC_Detective"
license: MIT
version: "1.2.0"
date-released: "2026-06-04"
version: "1.3.0"
date-released: "2026-06-05"
keywords:
- "FLAC"
- "lossless audio"
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![PyPI Downloads](https://img.shields.io/pypi/dm/flac-detective)](https://pypi.org/project/flac-detective/)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![CI](https://github.com/Guillain-RDCDE/FLAC_Detective/actions/workflows/ci.yml/badge.svg)](https://github.com/Guillain-RDCDE/FLAC_Detective/actions/workflows/ci.yml)
[![Status](https://img.shields.io/badge/status-stable%20(v1.2.0)-brightgreen)](https://github.com/Guillain-RDCDE/FLAC_Detective/releases)
[![Status](https://img.shields.io/badge/status-stable%20(v1.3.0)-brightgreen)](https://github.com/Guillain-RDCDE/FLAC_Detective/releases)
[![codecov](https://codecov.io/gh/Guillain-RDCDE/FLAC_Detective/branch/main/graph/badge.svg)](https://codecov.io/gh/Guillain-RDCDE/FLAC_Detective)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
Expand Down Expand Up @@ -93,6 +93,11 @@ enable the ML extra.

## 🆕 Latest releases

- **v1.3.0 — visual HTML report.** `--format html` writes a single self-contained page
(no external assets, no extra dependency) with a sortable/filterable triage table **and an
inline spectrum plot for every flagged file** — the MP3 "cliff" is visible to the eye, with
the detected cutoff marked. Computed with numpy and drawn as inline SVG; the core analysis
path is untouched (spectra are recomputed at report time, for flagged files only).
- **v1.2.0 — `--deep` mode + high-confidence WARNING floor.** The CNN (Rule 12) actually
*does* separate high-bitrate **AAC / Opus / Vorbis** transcodes from genuine FLAC on
full-range audio (ROC-AUC 0.94–0.99) — but the fast path used to skip Rule 12 on the very
Expand Down Expand Up @@ -204,6 +209,9 @@ flac-detective -v --format json --output report.json /music
# Triage a whole library: CSV ranked most-suspicious-first (opens in any spreadsheet)
flac-detective /music --format csv --output triage.csv

# Visual HTML report: a sortable triage table + a spectrum plot per flagged file
flac-detective /music --format html --output report.html

# Quick scan (15 s sample instead of default 30 s)
flac-detective --sample-duration 15 /music

Expand All @@ -216,6 +224,11 @@ flac-detective --deep /music
> **sorted by score (most suspicious at the top)** — sort/filter it in any spreadsheet
> to work through your library from the riskiest files down. The console summary also
> prints the top suspects so you see what to check first without opening anything.
>
> **Want to *see* why a file was flagged?** `--format html` writes a single self-contained
> page with a sortable triage table and an **inline spectrum plot for each flagged file** —
> the MP3 "cliff" (a sharp drop well below Nyquist) becomes visible at a glance, with the
> detected cutoff marked. No external assets, no extra dependency; just double-click to open.

**📖 See [User Guide](docs/user-guide.md) for detailed usage examples and command line options.**

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,4 @@ FLAC Detective is released under the MIT License. See [LICENSE](https://github.c

---

**Version**: 1.2.0 | **Last Updated**: June 2026
**Version**: 1.3.0 | **Last Updated**: June 2026
10 changes: 7 additions & 3 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,19 @@ flac-detective /music --format json --output results.json

# CSV — library triage: one row per file, ranked most-suspicious first
flac-detective /music --format csv --output triage.csv

# HTML — a visual report you double-click to open (spectrum plots for flagged files)
flac-detective /music --format html --output report.html
```

**Three formats, three jobs:**
**Four formats, four jobs:**

| `--format` | For | Notes |
|---|---|---|
| `text` (default) | reading | human-friendly report + console summary |
| `json` | automation | full result objects, parse with `jq` etc. |
| `csv` | **triaging a whole library** | one row per file, **sorted by score (most suspicious first)**; open in any spreadsheet to sort/filter. Columns: `rank, score, verdict, filename, cutoff_freq_hz, sample_rate, bit_depth, reason, filepath` |
| `html` | **seeing the evidence** | a single self-contained `.html` (no external assets) with a sortable/filterable triage table **and an inline spectrum plot for every flagged file** — the MP3 "cliff" is visible to the eye, with the detected cutoff marked. Plots are drawn for flagged files only; a file that isn't natively readable simply shows no plot |

When a scan finds suspicious files, the console summary also prints the **top suspects
ranked by score**, so you immediately see what to check first.
Expand Down Expand Up @@ -264,8 +268,8 @@ FLAC Detective saves a detailed text report:

```
FLAC AUTHENTICITY ANALYSIS REPORT
Generated: 2026-06-04 14:30:22
Analyzer Version: 1.2.0
Generated: 2026-06-05 14:30:22
Analyzer Version: 1.3.0
Sample Duration: 30.0s
Scan Path: /music/collection
======================================================================
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "flac-detective"
version = "1.2.0"
version = "1.3.0"
description = "Advanced FLAC authenticity analyzer - Detects MP3-to-FLAC transcodes with high precision"
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -189,7 +189,7 @@ disable = [

[tool.commitizen]
name = "cz_conventional_commits"
version = "1.2.0"
version = "1.3.0"
tag_format = "v$version"
update_changelog_on_bump = false
version_files = [
Expand Down
6 changes: 3 additions & 3 deletions src/flac_detective/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
All other files should reference this file.
"""

__version__ = "1.2.0"
__version__ = "1.3.0"
__version_info__ = tuple(int(x) for x in __version__.split("."))

# Release information
__release_date__ = "2026-06-04"
__release_name__ = "Deep mode — AAC/Vorbis transcode detection"
__release_date__ = "2026-06-05"
__release_name__ = "Visual HTML report — see the spectral cliff"

# Metadata
__author__ = "Guillain d'Erceville"
Expand Down
85 changes: 59 additions & 26 deletions src/flac_detective/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
from .analysis.diagnostic_tracker import get_tracker, reset_tracker
from .colors import Colors, colorize
from .config import analysis_config
from .reporting import CSVReporter, TextReporter
from .reporting import CSVReporter, HTMLReporter, TextReporter
from .tracker import ProgressTracker
from .utils import LOGO, find_flac_files, find_non_flac_audio_files

Expand Down Expand Up @@ -289,12 +289,13 @@ def parse_arguments() -> argparse.Namespace:
)
parser.add_argument(
"--format",
choices=["text", "json", "csv"],
choices=["text", "json", "csv", "html"],
default="text",
help=(
"Report format: 'text' (human-readable, default), 'json' (machine-readable), "
"or 'csv' (one row per file, ranked most-suspicious first — for triaging a "
"whole library in a spreadsheet)."
"'csv' (one row per file, ranked most-suspicious first — for triaging a "
"whole library in a spreadsheet), or 'html' (a single self-contained page "
"with a sortable triage table and a spectrum plot for each flagged file)."
),
)
args = parser.parse_args()
Expand Down Expand Up @@ -643,6 +644,50 @@ def _cleanup_console_log_if_empty(log_file: Path) -> bool:
return True


def _write_report(
results: list[dict],
output_file: Path,
report_format: str,
input_paths: list[Path],
all_flac_files: list[Path],
all_non_flac_files: list[Path],
) -> None:
"""Write ``results`` to ``output_file`` in the requested format.

Splits the format dispatch out of ``generate_final_report`` so that function
stays within the complexity budget as new formats are added.

Args:
results: Per-file analysis result dicts.
output_file: Destination path (extension already chosen by the caller).
report_format: "text", "json", "csv" or "html".
input_paths: Scan roots (passed to reporters for relative paths / scan_info).
all_flac_files: All FLAC files analyzed (json scan_info only).
all_non_flac_files: All non-FLAC files found (json scan_info only).
"""
if report_format == "json":
import json

payload = {
"scan_info": {
"timestamp": datetime.now().isoformat(),
"analyzer_version": __version__,
"scan_paths": [str(p) for p in input_paths],
"total_flac_files": len(all_flac_files),
"total_non_flac_files": len(all_non_flac_files),
},
"results": results,
}
with open(output_file, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False, default=str)
elif report_format == "csv":
CSVReporter().generate_report(results, output_file, scan_paths=input_paths)
elif report_format == "html":
HTMLReporter().generate_report(results, output_file, scan_paths=input_paths)
else:
TextReporter().generate_report(results, output_file, scan_paths=input_paths)


def generate_final_report(
results: list[dict],
output_dir: Path,
Expand All @@ -663,37 +708,25 @@ def generate_final_report(
log_file: Path to the console log file.
input_paths: List of user input paths (scan roots).
output_path: Explicit output path; if None, auto-named in `output_dir`.
report_format: "text" or "json".
report_format: "text", "json", "csv" or "html".
"""
logger.info("\nGenerating report...")

if output_path is not None:
output_file = output_path
output_file.parent.mkdir(parents=True, exist_ok=True)
else:
ext = {"json": "json", "csv": "csv"}.get(report_format, "txt")
ext = {"json": "json", "csv": "csv", "html": "html"}.get(report_format, "txt")
output_file = output_dir / f"flac_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{ext}"

if report_format == "json":
import json

payload = {
"scan_info": {
"timestamp": datetime.now().isoformat(),
"analyzer_version": __version__,
"scan_paths": [str(p) for p in input_paths],
"total_flac_files": len(all_flac_files),
"total_non_flac_files": len(all_non_flac_files),
},
"results": results,
}
with open(output_file, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False, default=str)
elif report_format == "csv":
CSVReporter().generate_report(results, output_file, scan_paths=input_paths)
else:
reporter = TextReporter()
reporter.generate_report(results, output_file, scan_paths=input_paths)
_write_report(
results,
output_file,
report_format,
input_paths,
all_flac_files,
all_non_flac_files,
)

# Generate diagnostic report if there were issues
tracker = get_tracker()
Expand Down
3 changes: 2 additions & 1 deletion src/flac_detective/reporting/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Report generation module."""

from .csv_reporter import CSVReporter
from .html_reporter import HTMLReporter
from .text_reporter import TextReporter

__all__ = ["CSVReporter", "TextReporter"]
__all__ = ["CSVReporter", "HTMLReporter", "TextReporter"]
Loading
Loading