diff --git a/arabic_phonemes.py b/arabic_phonemes.py new file mode 100644 index 0000000..6453b64 --- /dev/null +++ b/arabic_phonemes.py @@ -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] \ No newline at end of file diff --git a/config.py b/config.py index 26b7b57..cd95b98 100644 --- a/config.py +++ b/config.py @@ -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" @@ -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( @@ -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): @@ -87,4 +121,4 @@ def parse_disrespectful_language_patterns(cls, value): @lru_cache def get_settings() -> Settings: - return Settings() + return Settings() \ No newline at end of file diff --git a/corpus.py b/corpus.py index 53c24d6..a30b34c 100644 --- a/corpus.py +++ b/corpus.py @@ -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() @@ -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: @@ -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() @@ -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}") \ No newline at end of file diff --git a/phoneme_aligner.py b/phoneme_aligner.py new file mode 100644 index 0000000..d56ab5e --- /dev/null +++ b/phoneme_aligner.py @@ -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}} diff --git a/tajweed.py b/tajweed.py new file mode 100644 index 0000000..06c02bd --- /dev/null +++ b/tajweed.py @@ -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)] \ No newline at end of file