mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-22 08:38:13 +02:00
feat: add full document mode in knowledge base
This commit is contained in:
parent
c085398933
commit
87c8c5e2c8
26 changed files with 1144 additions and 351 deletions
220
api/services/pipecat/audio_file_cache.py
Normal file
220
api/services/pipecat/audio_file_cache.py
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
"""Shared utilities for downloading, converting, and caching audio files.
|
||||
|
||||
Provides helpers used by both the recording audio cache and the ambient
|
||||
noise cache to avoid duplicating download / ffmpeg / disk-cache logic.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from typing import Literal, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from api.constants import APP_ROOT_DIR
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Filesystem cache directory (shared by all audio caches)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
CACHE_DIR = os.path.join(os.path.dirname(APP_ROOT_DIR), "dograh_pcm_cache")
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Download helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def download_storage_file(
|
||||
storage_key: str,
|
||||
storage_backend: str,
|
||||
get_storage_fn,
|
||||
) -> Optional[str]:
|
||||
"""Download a file from object storage to a local temp file.
|
||||
|
||||
Returns the temp file path on success, or None on failure.
|
||||
The caller is responsible for cleaning up the temp file.
|
||||
"""
|
||||
ext = ext_from_key(storage_key)
|
||||
fd, tmp_path = tempfile.mkstemp(suffix=ext, prefix="dograh_dl_")
|
||||
os.close(fd)
|
||||
|
||||
try:
|
||||
storage = get_storage_fn(storage_backend)
|
||||
success = await storage.adownload_file(storage_key, tmp_path)
|
||||
if not success:
|
||||
logger.error(f"Failed to download {storage_key}")
|
||||
_safe_unlink(tmp_path)
|
||||
return None
|
||||
return tmp_path
|
||||
except Exception:
|
||||
logger.exception(f"Error downloading {storage_key}")
|
||||
_safe_unlink(tmp_path)
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Audio conversion via ffmpeg
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def convert_audio_file(
|
||||
file_path: str,
|
||||
target_sample_rate: int,
|
||||
output_format: Literal["pcm", "wav"] = "pcm",
|
||||
) -> Optional[bytes]:
|
||||
"""Convert an audio file via ffmpeg.
|
||||
|
||||
Args:
|
||||
file_path: Path to the source audio file.
|
||||
target_sample_rate: Desired output sample rate.
|
||||
output_format: ``"pcm"`` for raw s16le bytes, ``"wav"`` for a
|
||||
complete WAV file (16-bit mono).
|
||||
|
||||
Returns:
|
||||
Converted audio bytes, or None on failure.
|
||||
"""
|
||||
ffmpeg = shutil.which("ffmpeg")
|
||||
if not ffmpeg:
|
||||
logger.error("ffmpeg not found on PATH - cannot convert audio")
|
||||
return None
|
||||
|
||||
if output_format == "pcm":
|
||||
fmt_args = ["-f", "s16le", "-acodec", "pcm_s16le"]
|
||||
else:
|
||||
fmt_args = ["-f", "wav", "-acodec", "pcm_s16le"]
|
||||
|
||||
cmd = [
|
||||
ffmpeg,
|
||||
"-i",
|
||||
file_path,
|
||||
*fmt_args,
|
||||
"-ac",
|
||||
"1",
|
||||
"-ar",
|
||||
str(target_sample_rate),
|
||||
"-loglevel",
|
||||
"error",
|
||||
"pipe:1",
|
||||
]
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
logger.error(f"ffmpeg failed (rc={proc.returncode}): {stderr.decode()}")
|
||||
return None
|
||||
if not stdout:
|
||||
logger.error("ffmpeg produced no output")
|
||||
return None
|
||||
|
||||
return stdout
|
||||
except Exception:
|
||||
logger.exception("ffmpeg subprocess error")
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# File I/O helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def read_cached_file(path: str) -> bytes:
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write_cache_file(path: str, data: bytes) -> None:
|
||||
"""Atomically write *data* to *path* (write-to-tmp then rename)."""
|
||||
fd, tmp = tempfile.mkstemp(dir=CACHE_DIR, suffix=".tmp")
|
||||
os.close(fd)
|
||||
with open(tmp, "wb") as f:
|
||||
f.write(data)
|
||||
os.replace(tmp, path)
|
||||
|
||||
|
||||
def ext_from_key(storage_key: str) -> str:
|
||||
"""Extract file extension from a storage key, defaulting to .wav."""
|
||||
_, ext = os.path.splitext(storage_key)
|
||||
return ext if ext else ".wav"
|
||||
|
||||
|
||||
def _safe_unlink(path: str) -> None:
|
||||
try:
|
||||
if os.path.exists(path):
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ambient noise file cache
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _ambient_noise_cache_path(storage_key: str, sample_rate: int) -> str:
|
||||
"""Return the on-disk path for a cached ambient noise WAV file."""
|
||||
# Use a stable hash of the storage key so different uploads get different cache entries
|
||||
import hashlib
|
||||
|
||||
key_hash = hashlib.sha256(storage_key.encode()).hexdigest()[:16]
|
||||
return os.path.join(CACHE_DIR, f"ambient_{key_hash}_{sample_rate}.wav")
|
||||
|
||||
|
||||
async def get_cached_ambient_noise_path(
|
||||
storage_key: str,
|
||||
storage_backend: str,
|
||||
target_sample_rate: int,
|
||||
) -> Optional[str]:
|
||||
"""Return a local WAV file path for a custom ambient noise file.
|
||||
|
||||
Downloads from object storage and converts to mono WAV at
|
||||
*target_sample_rate* on the first call; subsequent calls return the
|
||||
cached path immediately.
|
||||
|
||||
Args:
|
||||
storage_key: Object storage key for the uploaded audio file.
|
||||
storage_backend: Storage backend identifier (e.g. ``"minio"``, ``"s3"``).
|
||||
target_sample_rate: Target sample rate for the output WAV.
|
||||
|
||||
Returns:
|
||||
Absolute path to the cached WAV file, or None on failure.
|
||||
"""
|
||||
from api.services.storage import get_storage_for_backend
|
||||
|
||||
cached = _ambient_noise_cache_path(storage_key, target_sample_rate)
|
||||
if os.path.exists(cached):
|
||||
logger.debug(f"Ambient noise served from cache: {cached}")
|
||||
return cached
|
||||
|
||||
logger.info(f"Downloading custom ambient noise: {storage_key}")
|
||||
|
||||
def _get_storage(backend: str):
|
||||
return get_storage_for_backend(backend)
|
||||
|
||||
tmp_path = await download_storage_file(storage_key, storage_backend, _get_storage)
|
||||
if not tmp_path:
|
||||
return None
|
||||
|
||||
try:
|
||||
wav_data = await convert_audio_file(
|
||||
tmp_path, target_sample_rate, output_format="wav"
|
||||
)
|
||||
if wav_data is None:
|
||||
return None
|
||||
|
||||
write_cache_file(cached, wav_data)
|
||||
logger.info(f"Cached custom ambient noise: {cached} ({len(wav_data)} bytes)")
|
||||
return cached
|
||||
except Exception:
|
||||
logger.exception("Error caching ambient noise file")
|
||||
return None
|
||||
finally:
|
||||
_safe_unlink(tmp_path)
|
||||
Loading…
Add table
Add a link
Reference in a new issue