suvradeepp's picture
Publish Tiny Hinglish Turn Detector development preview
35d483e verified
Raw
History Blame Contribute Delete
7.82 kB
"""Audio payload inspection and exact hashing without mandatory DSP packages."""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import io
import json
from pathlib import Path
import struct
from typing import Any, Mapping, Sequence
import wave
class AudioPayloadError(ValueError):
"""Raised when an audio value cannot be resolved to bytes or samples."""
@dataclass(frozen=True)
class AudioInfo:
"""Content fingerprint and inexpensive container metadata."""
sha256: str
num_bytes: int
format: str
sample_rate: int | None = None
num_channels: int | None = None
num_frames: int | None = None
bits_per_sample: int | None = None
duration_seconds: float | None = None
path: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"audio_sha256": self.sha256,
"audio_num_bytes": self.num_bytes,
"audio_format": self.format,
"sample_rate": self.sample_rate,
"num_channels": self.num_channels,
"num_frames": self.num_frames,
"bits_per_sample": self.bits_per_sample,
"duration_seconds": self.duration_seconds,
"audio_path": self.path,
}
def _resolve_path(path_value: str | Path, base_dir: str | Path | None) -> Path:
path = Path(path_value).expanduser()
if not path.is_absolute() and base_dir is not None:
path = Path(base_dir) / path
return path
def _read_path(path: Path) -> bytes:
try:
return path.read_bytes()
except OSError as exc:
raise AudioPayloadError(f"cannot read audio path {path}: {exc}") from exc
def extract_audio_bytes(
audio: Any,
*,
base_dir: str | Path | None = None,
) -> tuple[bytes, str | None]:
"""Resolve common Hugging Face/local audio representations to exact bytes.
Hugging Face Parquet rows normally contain ``{"bytes": ..., "path": ...}``.
Byte content is preferred over the often-virtual path. Decoded arrays are
represented canonically when no encoded payload is available.
"""
if isinstance(audio, bytes):
return audio, None
if isinstance(audio, (bytearray, memoryview)):
return bytes(audio), None
if isinstance(audio, (str, Path)):
path = _resolve_path(audio, base_dir)
return _read_path(path), str(audio)
if isinstance(audio, Mapping):
path_value = audio.get("path")
encoded = audio.get("bytes")
if encoded is not None:
if not isinstance(encoded, (bytes, bytearray, memoryview)):
raise AudioPayloadError("audio['bytes'] is not bytes-like")
return bytes(encoded), str(path_value) if path_value is not None else None
if path_value:
path = _resolve_path(str(path_value), base_dir)
return _read_path(path), str(path_value)
if audio.get("array") is not None:
return _canonical_array_bytes(audio["array"], audio.get("sampling_rate")), None
if hasattr(audio, "read"):
stream = audio
try:
position = stream.tell()
except (AttributeError, OSError):
position = None
payload = stream.read()
if position is not None:
try:
stream.seek(position)
except (AttributeError, OSError):
pass
if isinstance(payload, str):
payload = payload.encode("utf-8")
if isinstance(payload, (bytes, bytearray, memoryview)):
return bytes(payload), None
raise AudioPayloadError(
"unsupported audio value; expected bytes, a path, an Audio mapping, or a decoded array"
)
def _canonical_array_bytes(array: Any, sampling_rate: Any) -> bytes:
header: dict[str, Any] = {"sampling_rate": sampling_rate}
if hasattr(array, "dtype"):
header["dtype"] = str(array.dtype)
if hasattr(array, "shape"):
header["shape"] = tuple(int(value) for value in array.shape)
if hasattr(array, "tobytes"):
body = array.tobytes(order="C")
elif isinstance(array, Sequence) and not isinstance(array, (str, bytes, bytearray)):
# JSON is slower than ndarray.tobytes but deterministic and dependency-free.
try:
body = json.dumps(array, separators=(",", ":"), allow_nan=False).encode("utf-8")
except (TypeError, ValueError) as exc:
raise AudioPayloadError(f"cannot serialize decoded audio array: {exc}") from exc
else:
raise AudioPayloadError("audio['array'] cannot be serialized")
return b"TURN_ARRAY\0" + json.dumps(header, sort_keys=True).encode("utf-8") + b"\0" + body
def _infer_format(payload: bytes, path: str | None) -> str:
if payload.startswith(b"RIFF") and payload[8:12] == b"WAVE":
return "wav"
if payload.startswith(b"fLaC"):
return "flac"
if payload.startswith(b"OggS"):
return "ogg"
if payload.startswith(b"ID3") or payload[:2] in (b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"):
return "mp3"
if payload.startswith(b"TURN_ARRAY\0"):
return "decoded_array"
if path:
suffix = Path(path).suffix.lower().lstrip(".")
if suffix:
return suffix
return "unknown"
def _wav_metadata(payload: bytes) -> tuple[int, int, int, int, float] | None:
try:
with wave.open(io.BytesIO(payload), "rb") as wav_file:
sample_rate = wav_file.getframerate()
channels = wav_file.getnchannels()
frames = wav_file.getnframes()
bits = wav_file.getsampwidth() * 8
except (wave.Error, EOFError, OSError, struct.error):
return None
duration = frames / sample_rate if sample_rate else None
return sample_rate, channels, frames, bits, duration
def _flac_metadata(payload: bytes) -> tuple[int, int, int, int, float] | None:
# FLAC STREAMINFO is always the first metadata block and has a 34-byte body.
if len(payload) < 42 or payload[:4] != b"fLaC":
return None
block_type = payload[4] & 0x7F
block_length = int.from_bytes(payload[5:8], "big")
if block_type != 0 or block_length < 34 or len(payload) < 8 + block_length:
return None
streaminfo = payload[8 : 8 + block_length]
packed = int.from_bytes(streaminfo[10:18], "big")
sample_rate = packed >> 44
channels = ((packed >> 41) & 0x7) + 1
bits = ((packed >> 36) & 0x1F) + 1
frames = packed & ((1 << 36) - 1)
duration = frames / sample_rate if sample_rate else None
return sample_rate, channels, frames, bits, duration
def inspect_audio(audio: Any, *, base_dir: str | Path | None = None) -> AudioInfo:
"""Hash exact audio bytes and extract WAV/FLAC STREAMINFO metadata."""
payload, path = extract_audio_bytes(audio, base_dir=base_dir)
if not payload:
raise AudioPayloadError("audio payload is empty")
digest = hashlib.sha256(payload).hexdigest()
audio_format = _infer_format(payload, path)
metadata = _wav_metadata(payload) if audio_format == "wav" else None
if audio_format == "flac":
metadata = _flac_metadata(payload)
if metadata is None:
return AudioInfo(digest, len(payload), audio_format, path=path)
sample_rate, channels, frames, bits, duration = metadata
return AudioInfo(
sha256=digest,
num_bytes=len(payload),
format=audio_format,
sample_rate=sample_rate,
num_channels=channels,
num_frames=frames,
bits_per_sample=bits,
duration_seconds=duration,
path=path,
)
def exact_audio_sha256(audio: Any, *, base_dir: str | Path | None = None) -> str:
"""Return the SHA-256 digest of the exact encoded payload/canonical array."""
return inspect_audio(audio, base_dir=base_dir).sha256