session-scribe / app.py
Jedi09's picture
Update app.py
cd6b204 verified
"""
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("""
<style>
footer { display: none !important; }
.gradio-container { max-width: 1100px !important; margin: auto !important; }
</style>
<div style="text-align: center; padding: 40px 20px 30px;
background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 100%);
border-radius: 20px; margin-bottom: 24px; color: white;">
<h1 style="font-size: 2.2rem; font-weight: 700; margin: 0 0 8px 0;">
🏥 Klinik Görüşme Transkripsiyon & Analiz
</h1>
<p style="font-size: 1rem; opacity: 0.95; margin: 0;">
Danışman-Danışan görüşmelerini yazıya dökün ve AI destekli klinik ön değerlendirme alın
</p>
</div>
""")
with gr.Tabs():
# Tab 1: Transkripsiyon
with gr.TabItem("📝 Transkripsiyon"):
with gr.Row():
with gr.Column():
gr.HTML('<div style="font-weight: 600; margin-bottom: 12px;">📤 Ses Dosyası</div>')
audio_input = gr.Audio(
label="Görüşme Kaydı",
type="filepath",
sources=["upload", "microphone"]
)
submit_btn = gr.Button(
"🚀 Transkripsiyon Başlat",
variant="primary",
size="lg"
)
gr.HTML("""
<div style="background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border: 1px solid #7dd3fc; border-radius: 12px;
padding: 16px 20px; margin-top: 16px;">
<p style="margin: 0; color: #0369a1; font-size: 14px;">
ℹ️ <strong>Nasıl Çalışır:</strong><br>
1. Ses dosyasını yükleyin<br>
2. AI konuşmacıları otomatik ayırır<br>
3. Transkript oluşturulur<br>
4. Klinik Analiz sekmesinde danışanı seçip rapor alın
</p>
</div>
""")
with gr.Row():
with gr.Column():
gr.HTML('<div style="font-weight: 600; margin-bottom: 12px;">📝 Transkript Sonucu</div>')
output_text = gr.Textbox(
label="",
placeholder="Transkript burada görünecek...",
lines=25,
interactive=False
)
download_file = gr.File(
label="📥 Transkripti İndir (.txt)"
)
# Tab 2: Klinik Analiz
with gr.TabItem("🏥 Klinik Analiz"):
gr.HTML("""
<div style="background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border: 1px solid #f59e0b; border-radius: 12px;
padding: 16px 20px; margin-bottom: 16px;">
<p style="margin: 0; color: #92400e; font-size: 14px;">
🏥 <strong>Klinik Ön Değerlendirme:</strong> 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.
</p>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
client_selector = gr.Radio(
label="🎯 Danışanı Seçin",
choices=["Kişi 1", "Kişi 2"],
value="Kişi 2",
info="Hangi kişi danışan?"
)
analyze_btn = gr.Button(
"🔍 Klinik Analiz Başlat",
variant="primary",
size="lg"
)
gr.HTML("""
<div style="background: #f9fafb; border-radius: 12px;
padding: 16px 20px; margin-top: 16px;">
<p style="margin: 0 0 8px 0; color: #374151; font-size: 13px; font-weight: 600;">
📊 Analiz Kapsamı:
</p>
<ul style="margin: 0; padding-left: 20px; color: #6b7280; font-size: 12px;">
<li>Duygudurum göstergeleri</li>
<li>Bilişsel çarpıtmalar</li>
<li>Zaman yönelimi</li>
<li>Konuşma hızı</li>
<li>Benlik odağı</li>
<li>Kelime frekansı</li>
</ul>
</div>
""")
with gr.Column(scale=2):
analysis_output = gr.Textbox(
label="📋 Klinik Ön Değerlendirme Raporu",
placeholder="Analiz raporu burada görünecek...",
lines=30,
interactive=False
)
report_download = gr.File(
label="📥 Raporu İndir (.txt)"
)
# Features
gr.HTML("""
<div style="display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; margin-top: 24px;">
<div style="text-align: center; padding: 14px; background: #f9fafb; border-radius: 12px;">
<div style="font-size: 22px; margin-bottom: 4px;">🎭</div>
<div style="font-size: 11px; color: #6b7280; font-weight: 500;">Konuşmacı Ayrımı</div>
</div>
<div style="text-align: center; padding: 14px; background: #f9fafb; border-radius: 12px;">
<div style="font-size: 22px; margin-bottom: 4px;">⏱️</div>
<div style="font-size: 11px; color: #6b7280; font-weight: 500;">Zaman Damgası</div>
</div>
<div style="text-align: center; padding: 14px; background: #f9fafb; border-radius: 12px;">
<div style="font-size: 22px; margin-bottom: 4px;">🔒</div>
<div style="font-size: 11px; color: #6b7280; font-weight: 500;">%100 Local</div>
</div>
<div style="text-align: center; padding: 14px; background: linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%); border-radius: 12px;">
<div style="font-size: 22px; margin-bottom: 4px;">🧠</div>
<div style="font-size: 11px; color: #5b21b6; font-weight: 500;">Duygudurum</div>
</div>
<div style="text-align: center; padding: 14px; background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); border-radius: 12px;">
<div style="font-size: 22px; margin-bottom: 4px;">💭</div>
<div style="font-size: 11px; color: #92400e; font-weight: 500;">Bilişsel Analiz</div>
</div>
<div style="text-align: center; padding: 14px; background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); border-radius: 12px;">
<div style="font-size: 22px; margin-bottom: 4px;">📋</div>
<div style="font-size: 11px; color: #065f46; font-weight: 500;">Klinik Rapor</div>
</div>
</div>
""")
gr.HTML("""
<div style="background: #ecfdf5; border: 1px solid #6ee7b7; border-radius: 8px;
padding: 12px 16px; margin-top: 16px;">
<p style="margin: 0; color: #047857; font-size: 13px;">
🔒 <strong>Gizlilik:</strong> Tüm işlemler yerel olarak yapılır.
Ses dosyalarınız ve analizler hiçbir sunucuya gönderilmez.
</p>
</div>
""")
gr.HTML("""
<div style="text-align: center; padding: 24px 0; color: #9ca3af; font-size: 13px;">
<p>Powered by Faster-Whisper & Pyannote-Audio • Klinisyen Asistanı v2.0</p>
</div>
""")
# Event handling
submit_btn.click(
fn=process_audio,
inputs=[audio_input],
outputs=[output_text, download_file]
)
analyze_btn.click(
fn=analyze_client,
inputs=[output_text, client_selector],
outputs=[analysis_output, report_download]
)
# Launch
if __name__ == "__main__":
demo.launch(share=False, show_error=True)