461bf6fd40
CI / lint (3.11) (push) Has been cancelled
CI / lint (3.12) (push) Has been cancelled
CI / lint (3.13) (push) Has been cancelled
CI / shellcheck (push) Has been cancelled
CI / shfmt (push) Has been cancelled
CI / setup (3.11) (push) Has been cancelled
CI / setup (3.12) (push) Has been cancelled
CI / setup (3.13) (push) Has been cancelled
CI / check-licenses (3.12) (push) Has been cancelled
CI / test_unit (3.11) (push) Has been cancelled
CI / test_unit (3.12) (push) Has been cancelled
CI / test_unit (3.13) (push) Has been cancelled
CI / test_unit_no_extras (3.11) (push) Has been cancelled
CI / test_unit_no_extras (3.12) (push) Has been cancelled
CI / test_json_to_html (3.12) (push) Has been cancelled
CI / test_unit_no_extras (3.13) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.12, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.11, --extra xlsx) (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.12, --extra xlsx) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.11, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (csv, 3.13, --extra csv) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.11, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.12, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (docx, 3.13, --extra docx) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.11, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.12, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (markdown, 3.13, --extra md) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.11, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.12, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (odt, 3.13, --extra odt) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.11, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.12, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pdf-image, 3.13, --extra pdf --extra image --extra paddleocr) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.11, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.12, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pptx, 3.13, --extra pptx) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.11, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.12, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
CI / test_unit_dependency_extras (pypandoc, 3.13, --extra epub --extra org --extra rtf --extra rst) (push) Has been cancelled
Build And Push Docker Image / set-short-sha (push) Has been cancelled
Partition Benchmark / setup (push) Has been cancelled
Partition Benchmark / Measure and compare partition() runtime (push) Has been cancelled
CI / test_unit_dependency_extras (xlsx, 3.13, --extra xlsx) (push) Has been cancelled
CI / test_ingest_src (3.12) (push) Has been cancelled
CI / test_json_to_markdown (3.12) (push) Has been cancelled
CI / changelog (push) Has been cancelled
CI / test_dockerfile (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Build And Push Docker Image / build-images (linux/amd64, opensource-linux-8core) (push) Has been cancelled
Build And Push Docker Image / build-images (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Build And Push Docker Image / publish-images (push) Has been cancelled
146 lines
5.4 KiB
Python
146 lines
5.4 KiB
Python
"""Partition audio files into elements using speech-to-text transcription."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import IO, Any
|
|
|
|
from unstructured.chunking import add_chunking_strategy
|
|
from unstructured.documents.elements import Element, ElementMetadata, NarrativeText
|
|
from unstructured.file_utils.filetype import detect_filetype
|
|
from unstructured.file_utils.model import FileType
|
|
from unstructured.partition.common.common import exactly_one
|
|
from unstructured.partition.common.metadata import apply_metadata, get_last_modified_date
|
|
from unstructured.partition.utils.speech_to_text.speech_to_text_interface import (
|
|
SpeechToTextAgent,
|
|
)
|
|
from unstructured.utils import is_temp_file_path
|
|
|
|
_AUDIO_MIME_TYPE_FALLBACK = FileType.WAV.mime_type
|
|
|
|
|
|
@apply_metadata()
|
|
@add_chunking_strategy
|
|
def partition_audio(
|
|
filename: str | None = None,
|
|
*,
|
|
file: IO[bytes] | None = None,
|
|
language: str | None = None,
|
|
stt_agent: str | None = None,
|
|
metadata_filename: str | None = None,
|
|
metadata_last_modified: str | None = None,
|
|
**kwargs: Any,
|
|
) -> list[Element]:
|
|
"""Partition an audio file into elements using speech-to-text transcription.
|
|
|
|
Supports any audio format handled by the configured STT agent. The default Whisper
|
|
agent accepts any format that ffmpeg supports (WAV, MP3, FLAC, M4A, OGG, OPUS, WEBM,
|
|
and more). Transcription produces one NarrativeText element per segment, preserving
|
|
segment-level timestamps in ``metadata.segment_start_seconds`` and
|
|
``metadata.segment_end_seconds``. Requires the optional ``audio`` extra:
|
|
``pip install "unstructured[audio]"``.
|
|
|
|
.. note::
|
|
**Chunking drops segment timestamps.** When elements are merged by a chunking
|
|
strategy, ``segment_start_seconds`` and ``segment_end_seconds`` are currently
|
|
discarded (consolidation strategy ``DROP``). If audio timeline alignment matters
|
|
for your use-case, consume the un-chunked elements directly.
|
|
|
|
Parameters
|
|
----------
|
|
filename
|
|
Path to the audio file.
|
|
file
|
|
File-like object opened in binary mode (e.g. ``open("audio.mp3", "rb")``).
|
|
language
|
|
Optional ISO 639-1 language code for the spoken language (e.g. ``"en"``).
|
|
When ``None``, the speech-to-text agent may auto-detect.
|
|
stt_agent
|
|
Optional fully-qualified class name of the SpeechToTextAgent implementation.
|
|
Defaults to the Whisper agent when the ``audio`` extra is installed.
|
|
metadata_filename
|
|
Filename to store in element metadata when partitioning from a file object.
|
|
metadata_last_modified
|
|
Last modified date to store in element metadata.
|
|
"""
|
|
exactly_one(filename=filename, file=file)
|
|
|
|
# Metadata from original input only (not the temp path); compute before any temp file lifecycle.
|
|
last_modified = get_last_modified_date(filename) if filename else None
|
|
mime_type = _audio_mime_type(filename, file, metadata_filename)
|
|
|
|
audio_path: str
|
|
if filename is not None:
|
|
audio_path = filename
|
|
else:
|
|
assert file is not None # guaranteed by exactly_one()
|
|
file.seek(0)
|
|
suffix = _audio_suffix(file, metadata_filename)
|
|
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
|
|
shutil.copyfileobj(file, tmp)
|
|
audio_path = tmp.name
|
|
|
|
try:
|
|
agent = SpeechToTextAgent.get_agent(stt_agent)
|
|
segments = agent.transcribe_segments(audio_path, language=language)
|
|
finally:
|
|
if filename is None and is_temp_file_path(audio_path):
|
|
Path(audio_path).unlink(missing_ok=True)
|
|
|
|
if not segments:
|
|
return []
|
|
|
|
elements: list[Element] = []
|
|
for seg in segments:
|
|
text = seg["text"].strip()
|
|
if not text:
|
|
continue
|
|
element = NarrativeText(text=text)
|
|
element.metadata = ElementMetadata(
|
|
filetype=mime_type,
|
|
last_modified=last_modified,
|
|
segment_start_seconds=seg["start"],
|
|
segment_end_seconds=seg["end"],
|
|
)
|
|
element.metadata.detection_origin = "speech_to_text"
|
|
elements.append(element)
|
|
|
|
return elements
|
|
|
|
|
|
def _audio_mime_type(
|
|
filename: str | None,
|
|
file: IO[bytes] | None,
|
|
metadata_filename: str | None,
|
|
) -> str:
|
|
"""Return the MIME type for the audio source, falling back to WAV when undetectable."""
|
|
detected: FileType | None = None
|
|
if filename is not None:
|
|
detected = detect_filetype(file_path=filename)
|
|
elif file is not None:
|
|
detected = detect_filetype(
|
|
file=file,
|
|
metadata_file_path=metadata_filename,
|
|
)
|
|
if detected is not None and detected not in (FileType.UNK, FileType.EMPTY):
|
|
return detected.mime_type
|
|
return _AUDIO_MIME_TYPE_FALLBACK
|
|
|
|
|
|
def _audio_suffix(file: IO[bytes], metadata_filename: str | None) -> str:
|
|
"""Return the file-extension suffix for a temp file that wraps `file`.
|
|
|
|
Preference order:
|
|
1. Extension of `metadata_filename` when provided (e.g. ".mp3" from "recording.mp3").
|
|
2. Extension of `file.name` when the file object exposes a name (e.g. a real opened file).
|
|
3. ".wav" as a safe fallback recognised by all STT backends.
|
|
"""
|
|
for name in (metadata_filename, getattr(file, "name", None)):
|
|
if name:
|
|
suffix = Path(name).suffix
|
|
if suffix:
|
|
return suffix
|
|
return ".wav"
|