Audio-RAG-Platform / processor.py
aymanelgamal's picture
Upload 8 files
840ae20 verified
Raw
History Blame Contribute Delete
11.6 kB
from __future__ import annotations
import json
import os
import time
from pathlib import Path
from typing import Any, Dict, List
from dotenv import load_dotenv
from groq import Groq
from pydub import AudioSegment
from sentence_transformers import SentenceTransformer
from config import (
AUDIO_DIR,
DB_DIR,
DEFAULT_LANGUAGE,
EMBED_MODEL,
GROQ_ASR_MODEL,
GROQ_CHAT_MODEL,
HF_TOKEN,
MAX_CHUNK_SECONDS,
META_FILE,
MIN_CHUNK_SECONDS,
QUERY_LOG_FILE,
VECTOR_FILE,
)
load_dotenv()
api_key = os.getenv("GROQ_API_KEY")
if not api_key:
raise ValueError("🚨 GROQ_API_KEY not found! Please check your .env file.")
client = Groq(api_key=api_key)
embedder = SentenceTransformer(EMBED_MODEL)
def _ensure_dirs() -> None:
DB_DIR.mkdir(parents=True, exist_ok=True)
AUDIO_DIR.mkdir(parents=True, exist_ok=True)
for path in (META_FILE, VECTOR_FILE, QUERY_LOG_FILE):
if not path.exists():
path.write_text("[]", encoding="utf-8")
def _load_json(path: Path) -> list:
_ensure_dirs()
try:
with path.open("r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return []
def _save_json(path: Path, data: list) -> None:
_ensure_dirs()
with path.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def _seconds_to_hhmmss(seconds: float) -> str:
total = max(0, int(seconds))
h = total // 3600
m = (total % 3600) // 60
s = total % 60
return f"{h:02d}:{m:02d}:{s:02d}"
def _safe_transcribe(file_path: str, language: str = DEFAULT_LANGUAGE) -> str:
with open(file_path, "rb") as file:
transcript = client.audio.transcriptions.create(
file=(os.path.basename(file_path), file.read()),
model=GROQ_ASR_MODEL,
language=language,
response_format="text",
)
return transcript.strip()
def _safe_chat(prompt: str, temperature: float = 0.2) -> str:
response = client.chat.completions.create(
model=GROQ_CHAT_MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=temperature,
)
return response.choices[0].message.content.strip()
def _get_diarization_segments(audio_path: str, duration_sec: float) -> List[Dict[str, Any]]:
"""
Returns speaker segments. Falls back to a single-speaker segment if pyannote or HF token is unavailable.
"""
if not HF_TOKEN:
return [{"speaker": "Speaker 1", "start": 0.0, "end": float(duration_sec)}]
try:
from pyannote.audio import Pipeline
except Exception:
return [{"speaker": "Speaker 1", "start": 0.0, "end": float(duration_sec)}]
try:
pipeline = Pipeline.from_pretrained(
"pyannote/speaker-diarization-3.1",
use_auth_token=HF_TOKEN,
)
diarization = pipeline(audio_path)
segments: List[Dict[str, Any]] = []
speaker_map: Dict[str, str] = {}
speaker_counter = 1
for turn, _, speaker_label in diarization.itertracks(yield_label=True):
mapped = speaker_map.get(speaker_label)
if mapped is None:
mapped = f"Speaker {speaker_counter}"
speaker_map[speaker_label] = mapped
speaker_counter += 1
segments.append(
{
"speaker": mapped,
"start": float(turn.start),
"end": float(turn.end),
}
)
segments = [s for s in segments if s["end"] > s["start"]]
segments.sort(key=lambda x: (x["start"], x["end"]))
return segments or [{"speaker": "Speaker 1", "start": 0.0, "end": float(duration_sec)}]
except Exception:
return [{"speaker": "Speaker 1", "start": 0.0, "end": float(duration_sec)}]
def _normalize_segments(segments: List[Dict[str, Any]], duration_sec: float) -> List[Dict[str, Any]]:
if not segments:
return [{"speaker": "Speaker 1", "start": 0.0, "end": float(duration_sec)}]
normalized = []
last_end = 0.0
for seg in segments:
start = max(0.0, min(float(seg["start"]), duration_sec))
end = max(0.0, min(float(seg["end"]), duration_sec))
if end <= start:
continue
if start > last_end and start - last_end > 0.75:
normalized.append({"speaker": "Speaker 1", "start": last_end, "end": start})
normalized.append({"speaker": seg["speaker"], "start": start, "end": end})
last_end = max(last_end, end)
if not normalized:
return [{"speaker": "Speaker 1", "start": 0.0, "end": float(duration_sec)}]
return normalized
def _split_segment(start_sec: float, end_sec: float, max_len_sec: int = MAX_CHUNK_SECONDS) -> List[tuple[float, float]]:
parts = []
current = start_sec
while current < end_sec:
nxt = min(current + max_len_sec, end_sec)
if nxt - current >= MIN_CHUNK_SECONDS:
parts.append((current, nxt))
current = nxt
return parts
def extract_chunk_facts(chunk_text: str, speaker: str = "Speaker") -> str:
prompt = f"""
أنت مساعد دقيق جداً. استخرج أهم المعلومات، الأرقام، والأسماء والحقائق من هذا المقطع الصوتي القصير.
تجاهل الحشو والكلام الجانبي، وركز فقط على الجوهر.
إذا كان المقطع لا يحتوي على معلومات مهمة، اكتب: لا توجد معلومات جوهرية.
اكتب الإجابة في شكل نقاط قصيرة ومباشرة.
المتحدث: {speaker}
النص:
{chunk_text}
"""
return _safe_chat(prompt, temperature=0.1)
def merge_extracted_facts(all_facts: str) -> tuple[str, str]:
prompt = f"""
أنت خبير في التلخيص المتقدم.
إليك قائمة بالنقاط التي تم استخراجها من تسجيل صوتي.
مهمتك هي التوليف وليس النسخ.
الشروط:
1) اكتب من 5 إلى 7 نقاط رئيسية كحد أقصى.
2) ادمج الحقائق المترابطة في نقطة واحدة كثيفة.
3) احذف التكرار والحشو.
4) استخرج عنواناً دقيقاً يعبر عن جوهر التسجيل بالكامل.
أخرج النتيجة بهذا الشكل فقط:
العنوان: ...
النقاط:
- ...
- ...
النقاط الخام:
{all_facts}
"""
output = _safe_chat(prompt, temperature=0.2)
lines = output.splitlines()
title = "بدون عنوان"
points = output.strip()
for i, line in enumerate(lines):
if line.startswith("العنوان:"):
title = line.replace("العنوان:", "").strip()
if line.startswith("النقاط:"):
points = "\\n".join(lines[i + 1:]).strip()
return title or "بدون عنوان", points or output.strip()
def process_and_vectorize_audio(audio_path: str) -> Dict[str, Any]:
"""
End-to-end ingestion:
- speaker diarization (optional)
- chunking
- transcription
- fact extraction
- summary synthesis
- vector indexing
"""
_ensure_dirs()
start_time = time.time()
audio = AudioSegment.from_file(audio_path).set_channels(1).set_frame_rate(16000)
duration_sec = len(audio) / 1000.0
base_name = Path(audio_path).stem
diarized_segments = _normalize_segments(_get_diarization_segments(audio_path, duration_sec), duration_sec)
full_raw_text_parts: List[str] = []
all_chunks_facts_parts: List[str] = []
new_vectors: List[Dict[str, Any]] = []
chunk_records: List[Dict[str, Any]] = []
chunk_counter = 0
turn_counter = 0
for turn in diarized_segments:
turn_counter += 1
speaker = turn["speaker"]
for start_sec, end_sec in _split_segment(turn["start"], turn["end"], MAX_CHUNK_SECONDS):
start_ms = int(start_sec * 1000)
end_ms = int(end_sec * 1000)
if end_ms <= start_ms:
continue
chunk_audio = audio[start_ms:end_ms]
if len(chunk_audio) < MIN_CHUNK_SECONDS * 1000:
continue
chunk_counter += 1
chunk_filename = f"{base_name}_spk{speaker.replace(' ', '_')}_{start_ms}_{end_ms}.mp3"
chunk_path = AUDIO_DIR / chunk_filename
chunk_audio.export(chunk_path, format="mp3")
try:
text = _safe_transcribe(str(chunk_path), language=DEFAULT_LANGUAGE)
except Exception as exc:
text = ""
print(f"[WARN] transcription failed for {chunk_filename}: {exc}")
if not text:
continue
full_raw_text_parts.append(text)
chunk_facts = extract_chunk_facts(text, speaker=speaker)
all_chunks_facts_parts.append(
f"- ({speaker} | {_seconds_to_hhmmss(start_sec)}{_seconds_to_hhmmss(end_sec)}) {chunk_facts}"
)
embedding = embedder.encode(text, normalize_embeddings=True).tolist()
record = {
"file_link": audio_path,
"title": base_name,
"speaker": speaker,
"segment_index": turn_counter,
"chunk_index": chunk_counter,
"chunk_order": chunk_counter,
"audio_chunk_path": str(chunk_path),
"start_time": round(start_sec, 3),
"end_time": round(end_sec, 3),
"start_time_hms": _seconds_to_hhmmss(start_sec),
"end_time_hms": _seconds_to_hhmmss(end_sec),
"turn_start_time": round(turn["start"], 3),
"turn_end_time": round(turn["end"], 3),
"turn_start_hms": _seconds_to_hhmmss(turn["start"]),
"turn_end_hms": _seconds_to_hhmmss(turn["end"]),
"text_chunk": text,
"facts": chunk_facts,
"embedding": embedding,
"chunk_duration_sec": round(end_sec - start_sec, 3),
}
new_vectors.append(record)
chunk_records.append(record)
title, final_summary_points = merge_extracted_facts("".join(all_chunks_facts_parts))
metadata = {
"file_link": audio_path,
"title": title,
"summary": final_summary_points,
"full_text": " ".join(full_raw_text_parts).strip(),
"audio_duration_sec": round(duration_sec, 3),
"audio_duration_hms": _seconds_to_hhmmss(duration_sec),
"chunk_count": len(chunk_records),
"speaker_turn_count": len(diarized_segments),
"speakers": sorted({item["speaker"] for item in chunk_records}) or ["Speaker 1"],
"processing_time_sec": round(time.time() - start_time, 3),
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
"chunk_records": [
{
"speaker": item["speaker"],
"start_time": item["start_time"],
"end_time": item["end_time"],
"start_time_hms": item["start_time_hms"],
"end_time_hms": item["end_time_hms"],
"audio_chunk_path": item["audio_chunk_path"],
"text_chunk": item["text_chunk"],
}
for item in chunk_records
],
}
all_meta = _load_json(META_FILE)
all_meta.append(metadata)
_save_json(META_FILE, all_meta)
all_vectors = _load_json(VECTOR_FILE)
all_vectors.extend(new_vectors)
_save_json(VECTOR_FILE, all_vectors)
return metadata