API Reference

Complete guide to using FLAC Detective as a Python library.

Table of Contents

Installation

pip install flac-detective

Quick Start

Analyze a Single File

from flac_detective import FLACAnalyzer
from pathlib import Path

# Create analyzer
analyzer = FLACAnalyzer()

# Analyze a file
result = analyzer.analyze_file(Path("song.flac"))

# Check verdict
print(f"Verdict: {result['verdict']}")
print(f"Score: {result['score']}/150")
print(f"Reason: {result['reason']}")

Analyze Multiple Files

from flac_detective import FLACAnalyzer
from pathlib import Path

analyzer = FLACAnalyzer()
music_dir = Path("/path/to/music")

# Find all FLAC files
flac_files = list(music_dir.rglob("*.flac"))

# Analyze each
results = []
for flac_file in flac_files:
    result = analyzer.analyze_file(flac_file)
    results.append(result)
    print(f"{result['filename']}: {result['verdict']}")

# Filter suspicious files
suspicious = [r for r in results if r["score"] >= 55]
print(f"Found {len(suspicious)} suspicious files")

Core API

FLACAnalyzer

Main analysis class.

Constructor

FLACAnalyzer(sample_duration: float = 30.0)

Parameters:

  • sample_duration (float): Seconds of audio read per window, three windows per file (default: 30.0)

    • Every published accuracy figure was measured at 30 s; the thresholds were tuned against that reading.

    • A longer sample reads different audio, not the same audio better. Measured 30 s against 120 s on files with known provenance, verdicts moved in both directions, about one in forty — see ml/exchange/SAMPLE_DURATION_MEASUREMENT_2026-09-07.md.

    • A verdict that changes with this number is sitting on a reading boundary. That is worth knowing about the file; it is not a reason to prefer either number.

Example:

# Default (30 seconds) — the calibrated reading
analyzer = FLACAnalyzer()

# Shorter windows: faster, reads less of each file
analyzer = FLACAnalyzer(sample_duration=15.0)

Methods

analyze_file()
analyze_file(filepath: str | Path) -> dict

Analyzes a single lossless audio file. Despite the name, it handles FLAC, WAV, AIFF, ALAC (.m4a), APE (.ape), and lossless audio inside the archival video containers (LPCM in .mxf/.mov, PCM or FLAC in .mkv/.mka, TrueHD, DTS-HD MA) — everything but FLAC/WAV/AIFF is demuxed and decoded via ffmpeg (a lossy stream, such as an AAC .m4a or an AC-3 .mkv, is rejected, not analysed). The argument accepts a str or a pathlib.Path (since v1.0.1).

Parameters:

  • filepath (str | Path): Path to a FLAC / WAV / AIFF / ALAC / APE file, or a video container holding lossless audio

Returns: Dictionary with analysis results (see Result Objects)

Example:

from pathlib import Path

result = analyzer.analyze_file("song.flac")  # str works
result = analyzer.analyze_file(Path("album.wav"))  # Path works; WAV/ALAC/APE too

Raises:

  • FileNotFoundError: File doesn’t exist

  • ValueError: Not a valid FLAC file

  • RuntimeError: Analysis failed

Batch Analysis

Using find_flac_files()

from flac_detective.utils import find_flac_files
from pathlib import Path

# Find all FLAC files recursively
flac_files = find_flac_files(Path("/music"))

# Returns list of Path objects
print(f"Found {len(flac_files)} files")

find_flac_files() discovers .flac files only (as its name says). To also analyse WAV/ALAC/APE programmatically, gather those paths yourself (e.g. Path('/music').rglob('*.wav')) and pass each to analyze_file(), which handles every supported format.

Progress Tracking with ProgressTracker

from flac_detective import FLACAnalyzer, ProgressTracker
from flac_detective.utils import find_flac_files
from pathlib import Path

# Setup
analyzer = FLACAnalyzer()
tracker = ProgressTracker(progress_file=Path("progress.json"))

# Find files
flac_files = find_flac_files(Path("/music"))
tracker.set_total(len(flac_files))

# Analyze with resume capability
for flac_file in flac_files:
    if not tracker.is_processed(str(flac_file)):
        result = analyzer.analyze_file(flac_file)
        tracker.add_result(result)

        # Save every 10 files
        if len(tracker.get_results()) % 10 == 0:
            tracker.save()

# Final save
tracker.save()

# Get results
results = tracker.get_results()
processed, total = tracker.get_progress()
print(f"Processed {processed}/{total} files")

# Cleanup when done
tracker.cleanup()

Parallel Processing

from concurrent.futures import ProcessPoolExecutor, as_completed
from flac_detective import FLACAnalyzer
from flac_detective.utils import find_flac_files
from pathlib import Path


def analyze_file(filepath):
    """Worker function for parallel processing."""
    analyzer = FLACAnalyzer()
    return analyzer.analyze_file(filepath)


# Find files
flac_files = find_flac_files(Path("/music"))

# Parallel analysis with 4 workers
results = []
with ProcessPoolExecutor(max_workers=4) as executor:
    futures = {executor.submit(analyze_file, f): f for f in flac_files}

    for future in as_completed(futures):
        result = future.result()
        results.append(result)
        print(f"Processed: {result['filename']}")

print(f"Complete: {len(results)} files analyzed")

Result Objects

Analysis Result Dictionary

Each analyze_file() call returns a dictionary:

{
    # File information
    "filepath": str,  # Full path: "/music/song.flac"
    "filename": str,  # Name only: "song.flac"
    # Verdict
    "score": int,  # 0-150 (higher = more likely fake)
    "verdict": str,  # "AUTHENTIC" | "WARNING" | "SUSPICIOUS" | "FAKE_CERTAIN"
    "confidence": str,  # Confidence level description
    "reason": str,  # Human-readable explanation
    # Audio metadata
    "cutoff_freq": int,  # Frequency cutoff in Hz (e.g., 22050)
    "sample_rate": int,  # Sample rate (e.g., 44100, 48000)
    "bit_depth": int,  # Bit depth (16, 24, etc.)
    "channels": int,  # Number of channels (1, 2)
    "duration": float,  # Duration in seconds
    "encoder": str,  # Encoder info from metadata
    # Quality indicators
    "has_clipping": bool,  # Audio clipping detected
    "clipping_severity": str,  # "light" | "moderate" | "severe"
    "has_dc_offset": bool,  # DC offset detected
    "dc_offset_severity": str,  # "light" | "moderate" | "severe"
    # Corruption detection
    "is_corrupted": bool,  # File integrity issues
    "corruption_error": str,  # Error message if corrupted
    # Transcode detection
    "estimated_mp3_bitrate": int,  # Estimated source bitrate (0 if unknown)
    # Fake hi-res axis (separate from the transcode verdict)
    "hires_verdict": str,  # GENUINE_HIRES | UPSAMPLED | PADDED_DEPTH |
    # UPSAMPLED_AND_PADDED | NOT_HIRES | UNKNOWN
    "hires_reason": str,  # The reading behind it, "" when none
    "is_fake_high_res": bool,  # Container declares more bits than the samples use
    "estimated_bit_depth": int,  # Bits the samples actually use (16, 24 or 32)
    "is_upsampled": bool,  # Spectral cliff at a lower rate's Nyquist, silence above
    "suspected_original_rate": int,
    # Rule details (verbose mode)
    "rule_scores": dict,  # Individual rule contributions
}

Verdict Values

# Possible verdict values
VERDICTS = [
    "AUTHENTIC",  # Score ≤ 30
    "WARNING",  # Score 31-54
    "SUSPICIOUS",  # Score 55-85
    "FAKE_CERTAIN",  # Score ≥ 86
]

Working with Results

result = analyzer.analyze_file(Path("song.flac"))

# Check verdict
if result["verdict"] == "AUTHENTIC":
    print("File is genuine!")
elif result["verdict"] in ["SUSPICIOUS", "FAKE_CERTAIN"]:
    print(f"Likely fake: {result['reason']}")

# Check score threshold
if result["score"] >= 86:
    print("High confidence fake")

# The hi-res axis, separate from the transcode verdict
if result["hires_verdict"] in ("UPSAMPLED", "PADDED_DEPTH", "UPSAMPLED_AND_PADDED"):
    print(f"Fake hi-res: {result['hires_reason']}")
if result["is_upsampled"]:
    print(f"Upsampled from about {result['suspected_original_rate']} Hz")

# Access metadata
print(f"Sample rate: {result['sample_rate']} Hz")
print(f"Bit depth: {result['bit_depth']} bit")

Advanced Usage

Custom Filtering

from flac_detective import FLACAnalyzer
from flac_detective.utils import find_flac_files
from pathlib import Path

analyzer = FLACAnalyzer()
results = [analyzer.analyze_file(f) for f in find_flac_files(Path("/music"))]

# Find all fake files (score >= 86)
fakes = [r for r in results if r["score"] >= 86]

# Find upsampled files
upsampled = [r for r in results if r["is_upsampled"]]

# Find 24-bit suspicious files
suspicious_24bit = [r for r in results if r["bit_depth"] == 24 and r["score"] >= 50]

# Find corrupted files
corrupted = [r for r in results if r["is_corrupted"]]

# Find authentic high-quality files
authentic_hq = [r for r in results if r["verdict"] == "AUTHENTIC" and r["sample_rate"] >= 48000]

Statistics Calculation

from flac_detective.reporting.statistics import calculate_statistics

# Analyze files
results = [analyzer.analyze_file(f) for f in flac_files]

# Calculate statistics
stats = calculate_statistics(results)

print(f"Total files: {stats['total']}")
print(f"Authentic: {stats['authentic']} ({stats['authentic_pct']})")
print(f"Warning: {stats['probably_authentic']} ({stats['probably_authentic_pct']})")
print(f"Suspicious: {stats['suspect']} ({stats['suspect_pct']})")
print(f"Fake: {stats['fake']} ({stats['fake_pct']})")
# Every counter has a "<name>_pct" twin, already formatted ("12.5%").

Report Generation

from flac_detective.reporting import TextReporter
from pathlib import Path

# Analyze files
analyzer = FLACAnalyzer()
results = [analyzer.analyze_file(f) for f in flac_files]

# Generate report
reporter = TextReporter()
report_file = Path("analysis_report.txt")

reporter.generate_report(results=results, output_file=report_file, scan_paths=[Path("/music")])

print(f"Report saved to {report_file}")

FLAC Repair

Repair is enabled by default inside the analysis pipeline. When FLACAnalyzer encounters a file it cannot read, it routes through repair_flac_file automatically, preserves metadata, and analyses the repaired copy from the temp directory. The user’s file is replaced only with FLACAnalyzer(repair_in_place=True) (the CLI’s --repair-in-place), never by default. You do not need to call the repair API in normal use.

For advanced cases where you want to run the duration-fix repair pass directly (e.g. on a known-bad file outside the analyser), use the FLACDurationFixer class:

from flac_detective.repair import FLACDurationFixer
from pathlib import Path

fixer = FLACDurationFixer()
fixed = fixer.fix_duration(Path("corrupted.flac"), output_path=Path("repaired.flac"))
print("Repair attempted, output at:", fixed)

Or, to access the lower-level repair function used by the analyser:

from flac_detective.analysis.new_scoring.audio_loader import repair_flac_file

repaired_path = repair_flac_file(corrupted_path=Path("corrupted.flac"))
# repaired_path is None on failure

Integration Examples

With Pandas

import pandas as pd
from flac_detective import FLACAnalyzer
from flac_detective.utils import find_flac_files
from pathlib import Path

# Analyze
analyzer = FLACAnalyzer()
flac_files = find_flac_files(Path("/music"))
results = [analyzer.analyze_file(f) for f in flac_files]

# Create DataFrame
df = pd.DataFrame(results)

# Analysis
print(df["verdict"].value_counts())
print(df.groupby("verdict")["score"].mean())

# Filter and sort
suspicious = df[df["score"] >= 55].sort_values("score", ascending=False)
print(suspicious[["filename", "score", "verdict", "reason"]])

# Export
df.to_csv("analysis.csv", index=False)
suspicious.to_csv("suspicious.csv", index=False)

With tqdm Progress Bar

from tqdm import tqdm
from flac_detective import FLACAnalyzer
from flac_detective.utils import find_flac_files
from pathlib import Path

analyzer = FLACAnalyzer()
flac_files = find_flac_files(Path("/music"))

results = []
for flac_file in tqdm(flac_files, desc="Analyzing FLAC files"):
    result = analyzer.analyze_file(flac_file)
    results.append(result)

Database Integration (SQLite)

import sqlite3
from flac_detective import FLACAnalyzer
from flac_detective.utils import find_flac_files
from pathlib import Path

# Create database
conn = sqlite3.connect("flac_analysis.db")
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE IF NOT EXISTS analysis (
        filepath TEXT PRIMARY KEY,
        filename TEXT,
        score INTEGER,
        verdict TEXT,
        sample_rate INTEGER,
        bit_depth INTEGER,
        is_upsampled BOOLEAN,
        analyzed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
""")

# Analyze and store
analyzer = FLACAnalyzer()
flac_files = find_flac_files(Path("/music"))

for flac_file in flac_files:
    result = analyzer.analyze_file(flac_file)

    cursor.execute(
        """
        INSERT OR REPLACE INTO analysis
        (filepath, filename, score, verdict, sample_rate, bit_depth, is_upsampled)
        VALUES (?, ?, ?, ?, ?, ?, ?)
    """,
        (
            result["filepath"],
            result["filename"],
            result["score"],
            result["verdict"],
            result["sample_rate"],
            result["bit_depth"],
            result["is_upsampled"],
        ),
    )

conn.commit()
conn.close()

# Query later
conn = sqlite3.connect("flac_analysis.db")
cursor = conn.cursor()

cursor.execute("SELECT * FROM analysis WHERE verdict = 'FAKE_CERTAIN'")
fakes = cursor.fetchall()
print(f"Found {len(fakes)} certain fakes")

Web API (Flask Example)

from flask import Flask, request, jsonify
from flac_detective import FLACAnalyzer
from pathlib import Path
import tempfile

app = Flask(__name__)
analyzer = FLACAnalyzer()


@app.route("/analyze", methods=["POST"])
def analyze():
    """Analyze uploaded FLAC file."""
    if "file" not in request.files:
        return jsonify({"error": "No file provided"}), 400

    file = request.files["file"]

    # Save temporarily
    with tempfile.NamedTemporaryFile(suffix=".flac", delete=False) as tmp:
        file.save(tmp.name)
        tmp_path = Path(tmp.name)

    try:
        # Analyze
        result = analyzer.analyze_file(tmp_path)
        return jsonify(result)
    finally:
        # Cleanup
        tmp_path.unlink()


if __name__ == "__main__":
    app.run(debug=True)

Error Handling

Basic Error Handling

from flac_detective import FLACAnalyzer
from pathlib import Path

analyzer = FLACAnalyzer()

try:
    result = analyzer.analyze_file(Path("song.flac"))
    print(f"Score: {result['score']}")

except FileNotFoundError:
    print("File not found")

except ValueError as e:
    print(f"Invalid file: {e}")

except RuntimeError as e:
    print(f"Analysis failed: {e}")

except Exception as e:
    print(f"Unexpected error: {e}")

Graceful Degradation

def safe_analyze(filepath, analyzer):
    """Analyze with error handling."""
    try:
        return analyzer.analyze_file(filepath)
    except Exception as e:
        return {
            "filepath": str(filepath),
            "filename": filepath.name,
            "score": 0,
            "verdict": "ERROR",
            "reason": f"Analysis failed: {str(e)}",
            "error": str(e),
        }


# Use in batch processing
analyzer = FLACAnalyzer()
results = [safe_analyze(f, analyzer) for f in flac_files]

# Check for errors
errors = [r for r in results if r["verdict"] == "ERROR"]
print(f"Errors: {len(errors)}")

Corrupted File Handling

result = analyzer.analyze_file(Path("song.flac"))

if result["is_corrupted"]:
    print(f"File is corrupted: {result['corruption_error']}")

    # FLACAnalyzer already attempted auto-repair internally before reporting the
    # corruption — if you see is_corrupted=True, repair failed too. For a manual
    # second pass you can call the lower-level repair directly:
    from flac_detective.analysis.new_scoring.audio_loader import repair_flac_file

    repaired = repair_flac_file(corrupted_path=Path(result["filepath"]))
    if repaired:
        new_result = analyzer.analyze_file(repaired)
        print(f"Repaired file score: {new_result['score']}")

Complete Example

Full pipeline with all features:

#!/usr/bin/env python3
"""Complete FLAC analysis pipeline with all features."""

from flac_detective import FLACAnalyzer, ProgressTracker
from flac_detective.utils import find_flac_files
from flac_detective.reporting import TextReporter
from flac_detective.reporting.statistics import calculate_statistics
from pathlib import Path
import sys


def main():
    # Configuration
    music_dir = Path(sys.argv[1] if len(sys.argv) > 1 else ".")
    output_dir = Path("flac_analysis")
    output_dir.mkdir(exist_ok=True)

    # Initialize
    analyzer = FLACAnalyzer(sample_duration=30.0)
    tracker = ProgressTracker(progress_file=output_dir / "progress.json")

    # Find files
    print(f"Scanning {music_dir}...")
    flac_files = find_flac_files(music_dir)
    print(f"Found {len(flac_files)} FLAC files\n")

    # Analyze
    tracker.set_total(len(flac_files))

    for flac_file in flac_files:
        if not tracker.is_processed(str(flac_file)):
            try:
                result = analyzer.analyze_file(flac_file)
                tracker.add_result(result)

                # Progress
                processed, total = tracker.get_progress()
                print(
                    f"[{processed}/{total}] {result['filename']}: "
                    f"{result['score']}/150 ({result['verdict']})"
                )

                # Save periodically
                if processed % 10 == 0:
                    tracker.save()

            except Exception as e:
                print(f"Error analyzing {flac_file}: {e}")

    # Save results
    tracker.save()
    results = tracker.get_results()

    # Generate report
    report_file = output_dir / "analysis_report.txt"
    reporter = TextReporter()
    reporter.generate_report(results, report_file, scan_paths=[music_dir])

    # Statistics
    stats = calculate_statistics(results)

    # Summary
    print(f"\n{'=' * 60}")
    print("Analysis Complete")
    print(f"{'=' * 60}")
    print(f"Total files: {stats['total_files']}")
    print(f"Authentic: {stats['authentic_count']} ({stats['authentic_percentage']:.1f}%)")
    print(f"Suspicious: {stats['suspicious_count']} ({stats['suspicious_percentage']:.1f}%)")
    print(f"Fake (certain): {stats['fake_count']} ({stats['fake_percentage']:.1f}%)")
    print(f"Average score: {stats['average_score']:.1f}")
    print(f"\nReport saved to: {report_file}")
    print(f"{'=' * 60}\n")

    # Cleanup
    tracker.cleanup()


if __name__ == "__main__":
    main()

Next Steps


For questions or issues, visit GitHub Issues.