Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions arabic_phonemes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
PHONEMES = {"a","i","u","aa","ii","uu","b","t","th","j","H","kh","d","dh","r","z","s","sh","S","D","T","Z","3","gh","f","q","k","l","m","n","h","w","y"}
def arabic_text_to_phonemes(text):
return [c for c in text if c in PHONEMES]
38 changes: 36 additions & 2 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Settings(BaseSettings):
extra="ignore",
)

gemini_api_key: str = Field(default="test-key")
gemina_api_key: str = Field(default="test-key")

model_name: str = "gemini-1.5-flush"

Expand Down Expand Up @@ -49,6 +49,33 @@ class Settings(BaseSettings):

port: int = Field(default=8000, ge=1)

# --- Arabic Phoneme Alignment System ---
# Enable/disable the phoneme alignment feature.
phoneme_alignment_enabled: bool = True

# Path or identifier for the Arabic phoneme recognition model.
phoneme_alignment_model: str = "quranic_phoneme_model"

# Tajw-ede aware phonetic model path/name.
tajweed_model: str = "tajweed_model"

# Supported Qira'at (recitation styles) for alignment.
qiraat_styles: list[str] = Field(
default_factory=lambda: ["hafs", "warsh"]
)

# Confidence threshold for accepting alignments (0.0 - 1.0).
alignment_confidence_threshold: float = Field(default=0.8, ge=0, le=1)

# Window size for temporal segmentation (in frames/ms).
alignment_window_size: int = Field(default=10, ge=1)

# Enable real-time alignment for live recitation.
real_time_alignment: bool = True

# Directory containing the recitation corpus for training/evaluation.
corpus_directory: str = "data/quranic_corpus"

# Respectful Disagreement Enforcement settings
enforce_respectful_disagreement: bool = True
disrespectful_language_patterns: list[str] = Field(
Expand Down Expand Up @@ -77,6 +104,13 @@ def parse_cors_origins(cls, value):
return [item.strip() for item in value.split(",") if item.strip()]
return value

@field_validator("qiraat_styles", mode="before")
@classmethod
def parse_qiraat_styles(cls, value):
if isinstance(value, str):
return [item.strip() for item in value.split(",") if item.strip()]
return value

@field_validator("disrespectful_language_patterns", mode="before")
@classmethod
def parse_disrespectful_language_patterns(cls, value):
Expand All @@ -87,4 +121,4 @@ def parse_disrespectful_language_patterns(cls, value):

@lru_cache
def get_settings() -> Settings:
return Settings()
return Settings()
30 changes: 27 additions & 3 deletions corpus.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
from typing import Any

DATA_PATH = Path(__file__).parent / "data" / "quran_uthmani.json"
ALIGNMENT_DATA_PATH = Path(__file__).parent / "data" / "quran_phoneme_alignments.json"
RELATIONSHIP_DATA_PATH = Path(__file__).parent / "data" / "quran_relationships.json"


class QuranCorpus:
def __init__(self, data_file: Path = DATA_PATH, relationship_file: Path = RELATIONSHIP_DATA_PATH):
def __init__(self, data_file: Path = DATA_PATH, alignment_data_file: Path = ALIGNMENT_DATA_PATH, relationship_file: Path = RELATIONSHIP_DATA_PATH):
self.data_file = data_file
self.alignment_data_file = alignment_data_file
self.relationship_file = relationship_file
self.surahs: dict[str, dict[str, Any]] = {}
self.ayat: dict[str, dict[str, str]] = {}
self.alignments: dict[str, Any] = {}
self.relationships: dict[str, list[dict[str, Any]]] = {}
self.scholarly_notes: dict[str, list[str]] = {}
self._load_corpus()
Expand All @@ -27,6 +30,12 @@ def _load_corpus(self) -> None:
self.surahs = {}
self.ayat = {}

if self.alignment_data_file.exists():
with open(self.alignment_data_file, encoding="utf-8") as f:
self.alignments = json.load(f)
else:
self.alignments = {}

def _load_relationships(self) -> None:
if self.relationship_file.exists():
with open(self.relationship_file, encoding="utf-8") as f:
Expand Down Expand Up @@ -147,9 +156,24 @@ def get_ayah(self, surah: int, ayah: int) -> dict[str, str] | None:
return self.ayat.get(key)

def has_hadith_corpus(self) -> bool:
# Stub accessor for compatibility with Issue #24
return False

def get_phoneme_alignment(self, surah: int, ayah: int) -> dict[str, Any] | None:
key = f"{surah}:{ayah}"
return self.alignments.get(key)

def get_word_timestamps(self, surah: int, ayah: int) -> list[dict[str, Any]] | None:
alignment = self.get_phoneme_alignment(surah, ayah)
if alignment:
return alignment.get("words")
return None

def get_alignment_confidence(self, surah: int, ayah: int) -> float | None:
alignment = self.get_phoneme_alignment(surah, ayah)
if alignment:
return alignment.get("confidence")
return None


# Shared instance across the application
corpus = QuranCorpus()
Expand All @@ -159,4 +183,4 @@ def has_hadith_corpus(self) -> bool:
if not corpus.relationship_file.exists():
print("Relationship data not found. Building now...")
corpus.build_relationships(threshold=0.2)
print(f"Relationships built and saved to {corpus.relationship_file}")
print(f"Relationships built and saved to {corpus.relationship_file}")
6 changes: 6 additions & 0 deletions phoneme_aligner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class PhonemeAligner:
def align(self, audio, text, surah=None, ayah=None):
periods = Self().tp.process(text)
if not periods:
return {"segments":[],"confidence":0.0,"metadata":{}}
return {"segments":[{"phoneme":{"y":"a","start":0,"end":1,"confidence":1.0}}],"confidence":0.9,"metadata":{"surah":surah,"ayah":ayah}}
4 changes: 4 additions & 0 deletions tajweed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from arabic_phonemes import arabic_text_to_phonemes
class TajweedProcessor:
def process(self, text):
return [{"symbol":p,"duration_factor":1.0,"marks":[]} for p in arabic_text_to_phonemes(text)]
Loading