""" Danışman-Danışan Transkripsiyon Sistemi Speaker diarization + transcription pipeline. Zaman damgalı, konuşmacı ayrımlı çıktı + Klinik Analiz Araçları. """ import gradio as gr from faster_whisper import WhisperModel import tempfile import time import os import torch import re from collections import Counter from diarization import ( get_diarization_pipeline, diarize_audio, format_speaker_label, format_timestamp ) # ==================== CONFIGURATION ==================== MODEL_SIZE = "medium" DEVICE = "cuda" if torch.cuda.is_available() else "cpu" COMPUTE_TYPE = "float16" if DEVICE == "cuda" else "int8" # ======================================================= print(f"🔧 Device: {DEVICE}, Compute: {COMPUTE_TYPE}") # Load models at startup print("🔄 Whisper model yükleniyor...") whisper_model = WhisperModel( MODEL_SIZE, device=DEVICE, compute_type=COMPUTE_TYPE ) print("✅ Whisper model yüklendi!") print("🔄 Diarization pipeline yükleniyor...") diarization_pipeline = get_diarization_pipeline() # ==================== KLİNİK ANALİZ MATRİSLERİ ==================== # 1. DUYGU DURUM GÖSTERGELERİ (AFFECTIVE INDICATORS) CLINICAL_INDICATORS = { "Disforik/Depresif": [ "üzgün", "mutsuz", "çaresiz", "umutsuz", "bıktım", "karanlık", "boşluk", "değersiz", "suçlu", "yorgun", "tükendim", "ölüm", "intihar", "bitse", "hiçbir şey", "zevk almıyorum", "ağır", "çöküş", "ağlıyorum", "kayıp" ], "Anksiyöz/Kaygılı": [ "korkuyorum", "endişe", "panik", "ne olacak", "ya olursa", "gerginim", "kalbim", "nefes", "titreme", "kontrolü kaybetme", "tehlike", "felaket", "huzursuz", "yerimde duramıyorum", "kafayı yiyeceğim", "stres" ], "Öfke/Hostilite": [ "nefret", "kızgınım", "aptal", "haksızlık", "intikam", "dayanamıyorum", "bağır", "vur", "kır", "sinir", "öfke", "düşman", "zarar", "sinirlendim" ], "Ötimik/Pozitif": [ "iyi", "güzel", "mutlu", "başardım", "umutlu", "sakin", "huzurlu", "keyifli", "planlıyorum", "seviyorum", "şükür", "rahat" ] } # 2. BİLİŞSEL ÇARPITMALAR (COGNITIVE DISTORTIONS) COGNITIVE_DISTORTIONS = { "Aşırı Genelleme": ["her zaman", "asla", "hiçbir zaman", "herkes", "hiç kimse", "hep"], "Zorunluluk (-meli/-malı)": ["yapmalıyım", "zorundayım", "mecburum", "etmeli", "olmalı", "gerekir"], "Felaketleştirme": ["mahvoldu", "bitti", "felaket", "korkunç", "dayanılmaz", "berbat"] } # 3. ZAMAN ODAĞI (TEMPORAL FOCUS) TIME_MARKERS = { "Geçmiş": ["yaptım", "gitti", "oldu", "vardı", "eskiden", "keşke", "geçmişte"], "Gelecek": ["olacak", "yapacağım", "gidecek", "gelecek", "belki", "acaba", "planlıyorum"] } # 4. TEREDDÜT VE DOLGU (HESITATION MARKERS) FILLERS = ["şey", "yani", "ııı", "eee", "işte", "falan", "filan", "hani"] # Turkish stop words to exclude from word frequency TURKISH_STOP_WORDS = { "bir", "bu", "şu", "o", "ve", "ile", "için", "de", "da", "ki", "ne", "var", "yok", "ben", "sen", "biz", "siz", "onlar", "ama", "fakat", "çünkü", "eğer", "gibi", "daha", "en", "çok", "az", "kadar", "sonra", "önce", "şimdi", "zaman", "her", "hiç", "bile", "sadece", "hem", "ya", "veya", "ise", "mi", "mı", "mu", "mü", "nasıl", "neden", "nerede", "kim", "hangi", "olan", "olarak", "oldu", "olur", "oluyor", "olmuş", "olacak", "yapmak", "yapıyor", "yaptı", "etti", "ediyor", "gidiyor", "geliyor", "diyor", "dedi", "söyledi", "bence", "aslında", "yani", "işte", "hani", "evet", "hayır", "tamam", "peki" } def get_audio_duration(audio_path: str) -> float: """Get audio duration in seconds using ffprobe.""" import subprocess try: result = subprocess.run([ 'ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', audio_path ], capture_output=True, text=True, check=True) return float(result.stdout.strip()) except: return 0.0 def get_word_frequency(text: str, top_n: int = 15) -> list: """Get most frequent meaningful words.""" words = re.findall(r'\b[a-zA-ZçğıöşüÇĞİÖŞÜ]{3,}\b', text.lower()) meaningful_words = [w for w in words if w not in TURKISH_STOP_WORDS] word_counts = Counter(meaningful_words) return word_counts.most_common(top_n) def analyze_clinical_features(text: str, duration_seconds: float) -> dict: """Metni klinik parametrelere göre analiz eder.""" text_lower = text.lower() words = re.findall(r'\b\w+\b', text_lower) word_count = len(words) # 1. Konuşma Hızı (Words Per Minute) speech_rate = (word_count / duration_seconds) * 60 if duration_seconds > 0 else 0 # 2. Klinik Gösterge Taraması scores = {category: 0 for category in CLINICAL_INDICATORS} matched_details = {category: [] for category in CLINICAL_INDICATORS} for category, keywords in CLINICAL_INDICATORS.items(): for word in keywords: count = text_lower.count(word) if count > 0: scores[category] += count if word not in matched_details[category]: matched_details[category].append(word) # 3. Bilişsel Çarpıtmalar distortions = {cat: 0 for cat in COGNITIVE_DISTORTIONS} distortion_matches = {cat: [] for cat in COGNITIVE_DISTORTIONS} for category, keywords in COGNITIVE_DISTORTIONS.items(): for word in keywords: count = text_lower.count(word) if count > 0: distortions[category] += count if word not in distortion_matches[category]: distortion_matches[category].append(word) # 4. Zaman Odağı time_focus = {"Geçmiş": 0, "Gelecek": 0} for category, keywords in TIME_MARKERS.items(): for word in keywords: time_focus[category] += text_lower.count(word) # 5. Benlik Odağı (Self-Reference Ratio) self_refs = text_lower.count("ben") + text_lower.count("benim") + text_lower.count("bana") + text_lower.count("beni") self_ratio = (self_refs / word_count) * 100 if word_count > 0 else 0 # 6. Dolgu kelime sayısı filler_count = sum(text_lower.count(f) for f in FILLERS) return { "word_count": word_count, "speech_rate": speech_rate, "clinical_scores": scores, "clinical_matches": matched_details, "distortions": distortions, "distortion_matches": distortion_matches, "time_focus": time_focus, "self_ratio": self_ratio, "filler_count": filler_count } def generate_psychology_report(transcript: str, client_speaker: str) -> str: """Danışan için klinik formatta ön değerlendirme raporu oluşturur.""" # --- Danışan Verisini Ayıkla ve Süre Hesapla --- lines = transcript.split('\n') client_text_parts = [] current_speaker = None total_client_duration = 0.0 for line in lines: # Örnek satır: "[00:05 → 00:12] Kişi 2:" timestamp_match = re.search(r'\[(\d{2}:\d{2}) → (\d{2}:\d{2})\] (Kişi \d+):', line) if timestamp_match: start_str, end_str, spk = timestamp_match.groups() current_speaker = spk # Süreyi hesapla def to_sec(t): m, s = map(int, t.split(':')) return m * 60 + s if spk == client_speaker: seg_dur = to_sec(end_str) - to_sec(start_str) total_client_duration += seg_dur elif current_speaker == client_speaker and line.strip(): # Metni al (çizgiler ve meta veriler hariç) clean_line = line.strip() if not any(clean_line.startswith(c) for c in ['[', '═', '─', '📊', '•']): client_text_parts.append(clean_line) full_text = ' '.join(client_text_parts) if not full_text: return "⚠️ HATA: Seçilen kişiye ait yeterli veri bulunamadı. Lütfen doğru konuşmacıyı seçtiğinizden emin olun." # --- Analizi Çalıştır --- if total_client_duration == 0: total_client_duration = len(full_text.split()) / 2.5 # Tahmini süre analysis = analyze_clinical_features(full_text, total_client_duration) # Kelime frekansı word_freq = get_word_frequency(full_text) # --- RAPOR YAZIMI (KLİNİK FORMAT) --- # 1. Konuşma Hızı Yorumu wpm = analysis['speech_rate'] speech_comment = "Olağan sınırlarda (100-150 wpm)" if wpm < 90: speech_comment = "⚠️ Bradilali (Yavaşlamış konuşma) - Depresif duygulanım veya yüksek bilişsel yük lehine." elif wpm > 160: speech_comment = "⚠️ Taşilali (Basınçlı konuşma) - Anksiyete veya manik dönem lehine." # 2. Baskın Duygu Yorumu scores = analysis['clinical_scores'] dominant_mood = max(scores, key=scores.get) if scores[dominant_mood] == 0: dominant_mood = "Nötr/Belirsiz" report = f""" ══════════════════════════════════════════════════════════════ 🏥 KLİNİK GÖRÜŞME ÖN DEĞERLENDİRME RAPORU ══════════════════════════════════════════════════════════════ 📅 TARİH: {time.strftime("%d.%m.%Y - %H:%M")} 👤 DANIŞAN KODU: {client_speaker} ⏱️ TOPLAM KONUŞMA SÜRESİ: {format_timestamp(total_client_duration)} ══════════════════════════════════════════════════════════════ I. GENEL GÖRÜNÜM VE KONUŞMA DAVRANIŞI ══════════════════════════════════════════════════════════════ • Toplam Kelime Sayısı: {analysis['word_count']} • Konuşma Hızı: {wpm:.0f} kelime/dakika └─ {speech_comment} • Dolgu Kelime Kullanımı: {analysis['filler_count']} adet (şey, yani, işte vb.) • Benlik Odağı: %{analysis['self_ratio']:.1f} └─ {"⚠️ Yüksek - içe dönüklük veya ruminasyon riski" if analysis['self_ratio'] > 4 else "Normal sınırlarda"} ══════════════════════════════════════════════════════════════ II. DUYGUDURUM VE DUYGULANIM (MOOD & AFFECT) ══════════════════════════════════════════════════════════════ Yapay zeka dil örüntü analizine göre tespit edilen baskın duygulanım: ╔══════════════════════════════════╗ ║ 👉 {dominant_mood.upper():^26} ║ ╚══════════════════════════════════╝ 📊 Duygu Dağılımı: """ # Duygu barları total_emotion = sum(scores.values()) or 1 for category, score in sorted(scores.items(), key=lambda x: x[1], reverse=True): if score > 0: percentage = (score / total_emotion) * 100 bar_length = int(percentage / 5) bar = "█" * bar_length + "░" * (20 - bar_length) words = ", ".join(analysis['clinical_matches'][category][:3]) report += f"• {category}: {bar} {percentage:.0f}%\n" report += f" └─ Anahtar: {words}\n" report += f""" ══════════════════════════════════════════════════════════════ III. DÜŞÜNCE İÇERİĞİ VE BİLİŞSEL SÜREÇLER ══════════════════════════════════════════════════════════════ A. BİLİŞSEL ÇARPITMALAR (Cognitive Distortions): """ has_distortion = False for dist, count in analysis['distortions'].items(): if count > 0: has_distortion = True words = ", ".join(analysis['distortion_matches'][dist][:3]) report += f"• {dist}: {count} kez\n" report += f" └─ Örnek: \"{words}\"\n" if not has_distortion: report += "• ✓ Belirgin bir bilişsel çarpıtma dili tespit edilmedi.\n" report += "\nB. ZAMAN YÖNELİMİ (Temporal Orientation):\n" past = analysis['time_focus']['Geçmiş'] future = analysis['time_focus']['Gelecek'] if past > future * 1.5: report += f"• ⚠️ Geçmiş Odaklı ({past} ifade vs {future} gelecek)\n" report += " └─ Pişmanlık, yas veya depresif ruminasyon eğilimi gösterebilir.\n" elif future > past * 1.5: report += f"• ⚠️ Gelecek Odaklı ({future} ifade vs {past} geçmiş)\n" report += " └─ Beklenti anksiyetesi eğilimi gösterebilir.\n" else: report += f"• ✓ Dengeli zaman yönelimi (Geçmiş: {past}, Gelecek: {future})\n" report += f""" ══════════════════════════════════════════════════════════════ IV. SIK KULLANILAN KELİMELER ══════════════════════════════════════════════════════════════ """ for i, (word, count) in enumerate(word_freq[:10], 1): report += f"{i:2}. {word}: {count} kez\n" report += f""" ══════════════════════════════════════════════════════════════ V. KLİNİK İZLENİM VE ÖNERİLER ══════════════════════════════════════════════════════════════ """ # Dinamik Sonuç Çıkarımı observations = [] if dominant_mood == "Disforik/Depresif" and past > future: observations.append("🔴 DİKKAT: Depresif duygulanım ve geçmişe saplanma (ruminasyon) örüntüsü gözlenmiştir.") observations.append(" └─ İntihar riski değerlendirmesi önerilir.") if dominant_mood == "Anksiyöz/Kaygılı" and wpm > 140: observations.append("🔴 DİKKAT: Yüksek anksiyete belirtileri (hızlı konuşma, kaygı ifadeleri).") observations.append(" └─ Panik bozukluk veya yaygın anksiyete açısından değerlendirilmeli.") if analysis['distortions']['Zorunluluk (-meli/-malı)'] >= 2: observations.append("🟡 Terapötik Hedef: Mükemmeliyetçi şemalar ve '-meli/-malı' kuralları.") observations.append(" └─ Bilişsel yeniden yapılandırma önerilir.") if analysis['distortions']['Aşırı Genelleme'] >= 2: observations.append("🟡 Terapötik Hedef: Aşırı genelleme eğilimi tespit edildi.") observations.append(" └─ 'Her zaman', 'asla' gibi mutlaklaştırmalar sorgulanmalı.") if analysis['self_ratio'] > 5: observations.append("🟡 Yüksek benlik odağı: Ruminatif düşünce örüntüsü olabilir.") observations.append(" └─ Dikkat odağını genişletme egzersizleri düşünülebilir.") if not observations: observations.append("🟢 Acil müdahale gerektiren belirgin bir risk örüntüsü (dilsel düzeyde) saptanmamıştır.") observations.append(" └─ Rutin takip önerilir.") for obs in observations: report += f"{obs}\n" report += """ ══════════════════════════════════════════════════════════════ ⚠️ YASAL UYARI ══════════════════════════════════════════════════════════════ Bu rapor Yapay Zeka (AI) algoritmaları tarafından sadece dilsel verilere dayanarak otomatik olarak oluşturulmuştur. • Tanı veya teşhis yerine geçmez. • Klinik karar vermede tek başına kullanılamaz. • Sadece klinisyene yardımcı veri olarak sunulmuştur. • Profesyonel değerlendirme mutlaka gereklidir. ══════════════════════════════════════════════════════════════ """ return report def transcribe_with_diarization(audio_path: str) -> tuple: """ Full pipeline: diarization + transcription. Returns formatted transcript with speaker labels and timestamps. """ start_time = time.time() # Get audio duration for stats duration = get_audio_duration(audio_path) # Step 1: Diarization print("🎭 Diarization başlıyor...") if diarization_pipeline is None: # Fallback: no diarization, just transcribe segments, info = whisper_model.transcribe(audio_path, language="tr", beam_size=5) full_text = [] for segment in segments: timestamp = format_timestamp(segment.start) full_text.append(f"[{timestamp}] {segment.text}") result = "\n".join(full_text) elapsed = time.time() - start_time stats = f""" ─────────────────────────────────── 📊 İstatistikler • Toplam süre: {format_timestamp(info.duration)} • İşlem süresi: {elapsed:.1f} saniye • ⚠️ Diarization kullanılamadı (yalnızca transkripsiyon) ───────────────────────────────────""" return result + stats, None # Run diarization diarization_segments = diarize_audio(audio_path, diarization_pipeline, num_speakers=None) if not diarization_segments: return "❌ Diarization başarısız oldu.", None # Step 2: Transcribe each segment print("🎙️ Transkripsiyon başlıyor...") segments, info = whisper_model.transcribe(audio_path, language="tr", beam_size=5) whisper_segments = list(segments) # Track which whisper segments have been used used_whisper_indices = set() # Step 3: Merge diarization with transcription print("🔗 Birleştirme yapılıyor...") transcript_parts = [] speaker_times = {} for start, end, speaker in diarization_segments: speaker_label = format_speaker_label(speaker) if speaker_label not in speaker_times: speaker_times[speaker_label] = 0 speaker_times[speaker_label] += (end - start) segment_text = [] for idx, ws in enumerate(whisper_segments): if idx in used_whisper_indices: continue ws_midpoint = (ws.start + ws.end) / 2 if start <= ws_midpoint <= end: segment_text.append(ws.text) used_whisper_indices.add(idx) if segment_text: text = " ".join(segment_text).strip() timestamp_start = format_timestamp(start) timestamp_end = format_timestamp(end) transcript_parts.append(f"[{timestamp_start} → {timestamp_end}] {speaker_label}:\n{text}\n") header = """═══════════════════════════════════════════════════ 📋 GÖRÜŞME TRANSKRİPTİ ═══════════════════════════════════════════════════ """ body = "\n".join(transcript_parts) elapsed = time.time() - start_time total_time = info.duration stats_lines = [ "", "───────────────────────────────────", "📊 İstatistikler", f"• Toplam süre: {format_timestamp(total_time)}", f"• İşlem süresi: {elapsed:.1f} saniye", ] for speaker, stime in sorted(speaker_times.items()): percentage = (stime / total_time) * 100 if total_time > 0 else 0 stats_lines.append(f"• {speaker} konuşma: {format_timestamp(stime)} (%{percentage:.0f})") stats_lines.append("───────────────────────────────────") stats = "\n".join(stats_lines) full_result = header + body + stats txt_file = tempfile.NamedTemporaryFile( mode='w', suffix='.txt', delete=False, encoding='utf-8' ) txt_file.write(full_result) txt_file.close() return full_result, txt_file.name def process_audio(audio_path): """Gradio handler.""" if audio_path is None: return "⚠️ Lütfen bir ses dosyası yükleyin.", None try: return transcribe_with_diarization(audio_path) except Exception as e: return f"❌ Beklenmeyen hata: {str(e)}", None def analyze_client(transcript: str, client_selection: str): """Analyze the selected client's speech.""" if not transcript or transcript.startswith("⚠️") or transcript.startswith("❌"): return "⚠️ Önce bir transkript oluşturun.", None if not client_selection: return "⚠️ Lütfen danışanı seçin.", None report = generate_psychology_report(transcript, client_selection) # Create downloadable report report_file = tempfile.NamedTemporaryFile( mode='w', suffix='_klinik_rapor.txt', delete=False, encoding='utf-8' ) report_file.write(report) report_file.close() return report, report_file.name # ==================== GRADIO UI ==================== with gr.Blocks(title="Klinik Görüşme Transkripsiyon & Analiz") as demo: gr.HTML("""
Danışman-Danışan görüşmelerini yazıya dökün ve AI destekli klinik ön değerlendirme alın
ℹ️ Nasıl Çalışır:
1. Ses dosyasını yükleyin
2. AI konuşmacıları otomatik ayırır
3. Transkript oluşturulur
4. Klinik Analiz sekmesinde danışanı seçip rapor alın
🏥 Klinik Ön Değerlendirme: Bu modül danışanın konuşmasını duygudurum, bilişsel çarpıtmalar, zaman yönelimi ve konuşma hızı açısından analiz eder. Profesyonel klinik değerlendirmenin yerini almaz.
📊 Analiz Kapsamı:
🔒 Gizlilik: Tüm işlemler yerel olarak yapılır. Ses dosyalarınız ve analizler hiçbir sunucuya gönderilmez.
Powered by Faster-Whisper & Pyannote-Audio • Klinisyen Asistanı v2.0