File size: 13,129 Bytes
22a4c03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import streamlit as st
import torch
import numpy as np
from pydub import AudioSegment
from transformers import pipeline, WhisperProcessor
# webrtcvad: Bu kütüphane genel VAD için kullanılır ancak mevcut analiz akışınızda doğrudan bir işlevi yok.
# Eğer kullanmayacaksanız requirements.txt'den ve importtan kaldırabilirsiniz.
import webrtcvad 
from phonemizer.backend import EspeakBackend
from fastdtw import fastdtw
from scipy.signal import find_peaks
import librosa
import io
import os # Dosya yollarını yönetmek için

# Model ve tokenizer yolları (yerel klasörler için göreceli yollar)
# Hugging Face Spaces'te bu klasörler projenizin kök dizininde olacaktır.
model_path = './model_whisper-large-v3-turbo-aze-60hours(part15_aug)-lab/'
tokenizer_path = './tokenizer_whisper-large-v3-turbo-aze-60hours(part15_aug)-lab/'

# Model ve tokenizer klasörlerinin varlığını kontrol et
if not os.path.exists(model_path) or not os.path.exists(tokenizer_path):
    st.error(f"Hata: Model veya tokenizer klasörleri bulunamadı. Lütfen '{model_path}' ve '{tokenizer_path}' yollarını ve dosya yapısını kontrol edin.")
    st.stop() # Uygulamayı durdur

@st.cache_resource
def load_asr_components():
    """ASR modelini ve işlemcisini önbelleğe alır."""
    try:
        processor = WhisperProcessor.from_pretrained(tokenizer_path, language='aze', task='transcribe')
        
        pipeline_config = {
            "task": "automatic-speech-recognition",
            "model": model_path,
            "tokenizer": processor.tokenizer,
            "feature_extractor": processor.feature_extractor,
            "device": 0 if torch.cuda.is_available() else -1, # CUDA varsa GPU kullan
            "torch_dtype": torch.float16 if torch.cuda.is_available() else torch.float32,
            "generate_kwargs": {
                "task": "transcribe",
                "language": "azerbaijani",
                "num_beams": 2,
                "condition_on_prev_tokens": False,
                "compression_ratio_threshold": 2.4,
                "temperature": (0.0, 0.2, 0.4),
                "logprob_threshold": -1.0,
                "return_timestamps": True,
            }
        }
        asr_pipeline = pipeline(**pipeline_config)
        return processor, asr_pipeline
    except Exception as e:
        st.error(f"ASR bileşenleri yüklenirken hata oluştu: {e}. Lütfen model ve tokenizer yollarını kontrol edin.")
        return None, None

processor, asr_pipeline = load_asr_components()

class DictionAnalyzer:
    def __init__(self):
        # phonemizer backend'i de önbelleğe alınabilir veya burada başlatılabilir
        # EspeakBackend'in başlatılması biraz zaman alabilir, ilk yüklemede sorun olmaması için burada.
        self.phonemizer = EspeakBackend("az")

    def analyze_prosody(self, y, sr):
        try:
            pitch, _, _ = librosa.pyin(y, fmin=librosa.note_to_hz('C2'), fmax=librosa.note_to_hz('C7'))
        except Exception:
            pitch = np.array([np.nan]) # Handle cases where pyin might fail
        
        energy = librosa.feature.rms(y=y)[0]
        peaks, _ = find_peaks(energy, height=np.mean(energy) if energy.size > 0 else 0)
        
        return {
            "average_pitch": np.nanmean(pitch) if not np.all(np.isnan(pitch)) else 0,
            "pitch_variance": np.nanvar(pitch) if not np.all(np.isnan(pitch)) else 0,
            "energy_peaks": len(peaks)
        }

    def analyze_speed_and_fluency(self, transcript, audio_duration_seconds):
        words = len(transcript.split())
        if audio_duration_seconds > 0:
            words_per_second = words / audio_duration_seconds
        else:
            words_per_second = 0
        return {"words_per_second": words_per_second, "word_count": words}

    def compare_pronunciation(self, transcript, reference_text):
        phonetic_transcript = self.phonemizer.phonemize([transcript], strip=True)
        phonetic_reference = self.phonemizer.phonemize([reference_text], strip=True)
        
        def text_to_numeric(phonetic_text):
            if not phonetic_text or not phonetic_text[0]:
                return np.array([])
            return np.array([ord(char) for char in phonetic_text[0]])

        numeric_transcript = text_to_numeric(phonetic_transcript)
        numeric_reference = text_to_numeric(phonetic_reference)
        
        if numeric_transcript.size == 0 or numeric_reference.size == 0:
            return {"phonetic_distance": float('inf')} 

        try:
            dtw_distance, _ = fastdtw(numeric_transcript, numeric_reference, dist=lambda x, y: np.linalg.norm(x - y))
        except ValueError: # Handle empty sequences for DTW
             dtw_distance = float('inf')
        return {"phonetic_distance": dtw_distance}

    def evaluate_score(self, value, good_range, medium_range, is_lower_better=False):
        """
        Dəyəri yaxşı, orta, pis kateqoriyasına uyğun olaraq 1-10 arası bal verir.
        is_lower_better: Əgər dəyər nə qədər aşağı olarsa, o qədər yaxşıdırsa True qeyd edin.
        """
        if value is None or np.isnan(value) or value == float('inf'): # Handle invalid values
            return "Hesaplanamadı", 0

        if is_lower_better:
            if value <= good_range[1]:
                return "Yaxşı", 10
            elif good_range[1] < value <= medium_range[1]:
                return "Orta", 6
            else:
                return "Pis", 2
        else:
            if value >= good_range[0]:
                return "Yaxşı", 10
            elif medium_range[0] <= value < good_range[0]:
                return "Orta", 6
            else:
                return "Pis", 2

    def full_analysis(self, audio_data, reference_text):
        # audio_data dictionary olarak gelmeli: {"raw": np.array, "sampling_rate": int}
        y = audio_data["raw"]
        sr = audio_data["sampling_rate"]

        audio_duration = librosa.get_duration(y=y, sr=sr)
        
        if asr_pipeline is None:
            st.error("ASR pipeline yüklenemedi. Analiz yapılamıyor.")
            return None

        try:
            transcript_result = asr_pipeline({"raw": y, "sampling_rate": sr})
            transcript = transcript_result['text']
        except Exception as e:
            st.error(f"Transkripsiyon sırasında hata oluştu: {e}")
            transcript = "" # Boş transkript ile devam et
        
        prosody = self.analyze_prosody(y, sr)
        fluency = self.analyze_speed_and_fluency(transcript, audio_duration)
        pronunciation = self.compare_pronunciation(transcript, reference_text)
        
        # Değerlendirme aralıkları: Bu aralıklar, modelinizin performansı ve istediğiniz metriklere göre ayarlanmalıdır.
        # Bunlar sadece örneklerdir.
        prosody_avg_pitch_eval = self.evaluate_score(prosody["average_pitch"], (100, 250), (70, 300), is_lower_better=False)
        prosody_pitch_variance_eval = self.evaluate_score(prosody["pitch_variance"], (500, 3000), (200, 5000), is_lower_better=False)
        # energy_peaks için audio_duration'a göre bir oran düşünebilirsiniz. Örneğin: 0.1-0.5 peak/saniye iyi olabilir.
        prosody_energy_peaks_eval = self.evaluate_score(prosody["energy_peaks"] / audio_duration if audio_duration > 0 else 0, (0.1, 0.5), (0.05, 0.7), is_lower_better=False)

        fluency_wps_eval = self.evaluate_score(fluency["words_per_second"], (2.0, 3.0), (1.5, 3.5), is_lower_better=False)

        pronunciation_phonetic_distance_eval = self.evaluate_score(pronunciation["phonetic_distance"], (0, 200), (201, 1000), is_lower_better=True)

        return {
            "transcript": transcript,
            "prosody": {
                "average_pitch": prosody["average_pitch"],
                "average_pitch_score": prosody_avg_pitch_eval,
                "pitch_variance": prosody["pitch_variance"],
                "pitch_variance_score": prosody_pitch_variance_eval,
                "energy_peaks": prosody["energy_peaks"],
                "energy_peaks_score": prosody_energy_peaks_eval
            },
            "fluency": {
                "words_per_second": fluency["words_per_second"],
                "words_per_second_score": fluency_wps_eval,
                "word_count": fluency["word_count"]
            },
            "pronunciation": {
                "phonetic_distance": pronunciation["phonetic_distance"],
                "phonetic_distance_score": pronunciation_phonetic_distance_eval
            }
        }

# Streamlit UI
st.title("Diksiyon Analiz Uygulaması")
st.write("Ses dosyanızı yükleyin ve referans metin girerek diksiyonunuzu analiz edin.")

uploaded_file = st.file_uploader("Ses Dosyası Yükleyin (.wav, .mp3)", type=["wav", "mp3"])
reference_text_input = st.text_area("Referans Metin Girin", 
                                     "Salam, hər vaxtınız xeyr, mənim adım Əlidir. Salam, Əli bəy. Sizə necə kömək edə bilərəm? Kartımın şifrəsini dəyişmək istəyirəm və şifrəmi unutmuşam. Şifrənizi dəyişdirmək üçün sizdən bəzi məlumatlar istəyəcəm. Zəhmət olmasa, adınızı, soyadınızı və bir də vəsigənin fin kodunu deyə bilərsiniz. Əlbəttə, deyə bilərəm. Mənim adım Ələl Əkbəlli. Fin kodum isə 7CRH7UE. Zəhmət olmasa, yenə təkrar bilərsiniz fin kodu? 7-C-R-H-7-U-E Təşəkkür edirəm, Əli bəy. Şifrənizi sıfırlamaq üçün sizə indi kod gəlməlidir telefona, gəldi, hal-hazırda? Bəli, bəli, kod gəldi. İndi parolu dəyişə bilərəm. Bəli, dəyişə bilərsiniz? Oldu, hazırda şifrəmi dəyişdirdim. Aha, hal-hazırda əməliyyatınız uğurla tamamlanıb və şifrəniz dəyişilib. Yeni şifrədən istifadə edə bilərsiniz, kart hesabınıza daxil olmaq üçün. Başqa sualınız varmı? Xeyr, çox təşəkkür edəm sizə. Buyurun, gününüz xoş geçsin. Sağ olun.")

if uploaded_file is not None and reference_text_input:
    st.audio(uploaded_file, format='audio/wav') # Kullanıcının yüklediği sesi oynat
    
    with st.spinner('Ses dosyası işleniyor...'):
        try:
            audio_bytes = uploaded_file.read()
            audio_segment = AudioSegment.from_file(io.BytesIO(audio_bytes))

            audio_array = np.array(audio_segment.get_array_of_samples(), dtype=np.float32)
            
            # Ses stereo ise mono'ya çevir
            if audio_segment.channels == 2:
                audio_array = audio_array.reshape((-1, 2)).mean(axis=1)

            # Normalizasyon
            audio_array /= np.iinfo(audio_segment.array_type).max

            # Eğer sample rate 16000 değilse, yeniden örnekle
            if audio_segment.frame_rate != 16000:
                st.info(f"Ses dosyası {audio_segment.frame_rate} Hz. 16000 Hz'e dönüştürülüyor...")
                audio_array = librosa.resample(y=audio_array, orig_sr=audio_segment.frame_rate, target_sr=16000)
            
            audio_input_for_analysis = {"raw": audio_array, "sampling_rate": 16000}

        except Exception as e:
            st.error(f"Ses dosyası okunurken veya işlenirken hata oluştu: {e}")
            audio_input_for_analysis = None

    if audio_input_for_analysis:
        analyzer = DictionAnalyzer()
        
        st.write("Analiz ediliyor, lütfen bekleyin...")
        with st.spinner('Analiz tamamlanıyor...'):
            report = analyzer.full_analysis(audio_input_for_analysis, reference_text_input)

        if report:
            st.subheader("Analiz Sonuçları")
            st.write(f"**Transkript:** {report['transcript']}")

            st.subheader("Prosoya Analizi")
            st.write(f"Ortalama Ton: {report['prosody']['average_pitch']:.2f} Hz - **Kategori: {report['prosody']['average_pitch_score'][0]} ({report['prosody']['average_pitch_score'][1]}/10)**")
            st.write(f"Ton Farkı: {report['prosody']['pitch_variance']:.2f} - **Kategori: {report['prosody']['pitch_variance_score'][0]} ({report['prosody']['pitch_variance_score'][1]}/10)**")
            st.write(f"Enerji Pikleri: {report['prosody']['energy_peaks']} - **Kategori: {report['prosody']['energy_peaks_score'][0]} ({report['prosody']['energy_peaks_score'][1]}/10)** (Saniyedeki tepe noktası oranı)")

            st.subheader("Sürat ve Akıcılık Analizi")
            st.write(f"Saniyedeki Kelime Sayısı: {report['fluency']['words_per_second']:.2f} - **Kategori: {report['fluency']['words_per_second_score'][0]} ({report['fluency']['words_per_second_score'][1]}/10)**")
            st.write(f"Toplam Kelime Sayısı: {report['fluency']['word_count']}")

            st.subheader("Tələffüz Müqayisəsi")
            st.write(f"Fonetik Məsafə (DTW): {report['pronunciation']['phonetic_distance']:.2f} - **Kategori: {report['pronunciation']['phonetic_distance_score'][0]} ({report['pronunciation']['phonetic_distance_score'][1]}/10)**")
        else:
            st.error("Analiz tamamlanamadı.")
elif uploaded_file is None and st.button("Analiz Et"):
    st.warning("Lütfen bir ses dosyası yükleyin ve referans metin girin.")