Rthur2003 commited on
Commit
4e0bd69
·
1 Parent(s): d20e581

feat: add local demo for AURIS AI music detection with Gradio interface

Browse files
Files changed (1) hide show
  1. local_demo.py +488 -0
local_demo.py ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AURIS Local Demo — AI Music Detection
3
+ Gradio arayüzü ile eğitilmiş modeli doğrudan test et.
4
+ Backend'e gerek yok, model local'de çalışır.
5
+
6
+ Çalıştır:
7
+ python local_demo.py
8
+ """
9
+
10
+ import io
11
+ import json
12
+ import pickle
13
+ import time
14
+ from pathlib import Path
15
+
16
+ import gradio as gr
17
+ import numpy as np
18
+
19
+ # ── Model yükleme ──────────────────────────────────────────────────
20
+
21
+ MODELS_DIR = Path(__file__).parent / "models"
22
+ FIGURES_DIR = Path(__file__).parent.parent / "docs" / "academic" / "figures"
23
+
24
+ with open(MODELS_DIR / "auris_classifier_v1.pkl", "rb") as f:
25
+ model = pickle.load(f)
26
+
27
+ with open(MODELS_DIR / "feature_scaler_v1.pkl", "rb") as f:
28
+ scaler = pickle.load(f)
29
+
30
+ with open(MODELS_DIR / "feature_columns_v1.json", "r") as f:
31
+ feature_cols = json.load(f)
32
+
33
+ with open(MODELS_DIR / "training_results.json", "r") as f:
34
+ training_results = json.load(f)
35
+
36
+ best_model_name = training_results.get("_best_model", "Gradient Boosting")
37
+ n_features = training_results.get("_n_features", 47)
38
+ importance = training_results.get("_feature_importance", {})
39
+ top_features = sorted(importance.items(), key=lambda x: x[1], reverse=True)[:10]
40
+
41
+ print(f"Model: {best_model_name} | Features: {n_features}")
42
+ print(f"Figures: {FIGURES_DIR}")
43
+
44
+
45
+ # ── Feature extraction (simplified — same as training pipeline) ────
46
+
47
+ def extract_features_from_audio(audio_path: str) -> dict:
48
+ """Extract 47 features from audio file using librosa."""
49
+ import librosa
50
+ from scipy import stats as sp_stats
51
+
52
+ y, sr = librosa.load(audio_path, sr=22050, mono=True, duration=60.0)
53
+ duration_sec = len(y) / sr
54
+
55
+ # RMS energy
56
+ rms = librosa.feature.rms(y=y, hop_length=512)[0]
57
+ rms_mean = float(np.mean(rms))
58
+ rms_std = float(np.std(rms))
59
+ rms_dynamic_range = float(np.max(rms) - np.min(rms))
60
+
61
+ # Spectral features
62
+ cent = librosa.feature.spectral_centroid(y=y, sr=sr, hop_length=512)[0]
63
+ flat = librosa.feature.spectral_flatness(y=y, hop_length=512)[0]
64
+ bw = librosa.feature.spectral_bandwidth(y=y, sr=sr, hop_length=512)[0]
65
+ rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr, hop_length=512)[0]
66
+ contrast = librosa.feature.spectral_contrast(y=y, sr=sr, hop_length=512)
67
+
68
+ # MFCCs
69
+ mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13, hop_length=512)
70
+ mfcc_delta = librosa.feature.delta(mfcc)
71
+ mfcc_delta2 = librosa.feature.delta(mfcc, order=2)
72
+
73
+ # Zero crossing
74
+ zcr = librosa.feature.zero_crossing_rate(y, hop_length=512)[0]
75
+
76
+ # Tempo
77
+ tempo, beats = librosa.beat.beat_track(y=y, sr=sr, hop_length=512)
78
+ tempo_val = float(np.atleast_1d(tempo)[0])
79
+ beat_times = librosa.frames_to_time(beats, sr=sr, hop_length=512)
80
+ if len(beat_times) > 1:
81
+ ibi = np.diff(beat_times)
82
+ tempo_stability = float(np.std(ibi))
83
+ tempo_cv = float(np.std(ibi) / np.mean(ibi)) if np.mean(ibi) > 0 else 0.0
84
+ else:
85
+ tempo_stability = 0.0
86
+ tempo_cv = 0.0
87
+
88
+ # Chroma
89
+ chroma = librosa.feature.chroma_stft(y=y, sr=sr, hop_length=512)
90
+ chroma_std = float(np.mean(np.std(chroma, axis=1)))
91
+ chroma_entropy = float(-np.sum(
92
+ np.mean(chroma, axis=1) * np.log2(np.mean(chroma, axis=1) + 1e-10)
93
+ ))
94
+ chroma_diff = np.diff(chroma, axis=1)
95
+ chroma_transition_rate = float(np.mean(np.abs(chroma_diff)))
96
+
97
+ # Tonnetz
98
+ tonnetz = librosa.feature.tonnetz(y=y, sr=sr)
99
+ tonnetz_std = float(np.mean(np.std(tonnetz, axis=1)))
100
+
101
+ # Harmonic ratio
102
+ y_harm, y_perc = librosa.effects.hpss(y)
103
+ harm_energy = float(np.sum(y_harm ** 2))
104
+ perc_energy = float(np.sum(y_perc ** 2))
105
+ total_energy = harm_energy + perc_energy + 1e-10
106
+ harmonic_ratio = harm_energy / total_energy
107
+
108
+ # Mel features
109
+ mel = librosa.feature.melspectrogram(y=y, sr=sr, hop_length=512)
110
+ mel_db = librosa.power_to_db(mel)
111
+ mel_flatness = float(np.mean(librosa.feature.spectral_flatness(S=mel)))
112
+
113
+ # Onset
114
+ onset_env = librosa.onset.onset_strength(y=y, sr=sr, hop_length=512)
115
+
116
+ # Pitch
117
+ pitches, magnitudes = librosa.piptrack(y=y, sr=sr, hop_length=512)
118
+ pitch_vals = []
119
+ for t in range(pitches.shape[1]):
120
+ idx = magnitudes[:, t].argmax()
121
+ p = pitches[idx, t]
122
+ if p > 50:
123
+ pitch_vals.append(p)
124
+ pitch_mean_hz = float(np.mean(pitch_vals)) if pitch_vals else 0.0
125
+ if len(pitch_vals) > 1 and pitch_mean_hz > 0:
126
+ cents = 1200 * np.log2(np.array(pitch_vals) / pitch_mean_hz + 1e-10)
127
+ pitch_std_cents = float(np.std(cents))
128
+ else:
129
+ pitch_std_cents = 0.0
130
+
131
+ # Heuristic scores (same sigmoid as training)
132
+ def _sigmoid(x, center=0.5, steepness=6.0):
133
+ return 1.0 / (1.0 + np.exp(-steepness * (x - center)))
134
+
135
+ spectral_regularity = float(_sigmoid(1.0 - float(np.std(flat)), 0.5, 4))
136
+ temporal_patterns = float(_sigmoid(1.0 - tempo_cv, 0.6, 5) if tempo_cv > 0 else 0.5)
137
+ harmonic_structure = float(_sigmoid(harmonic_ratio, 0.5, 4))
138
+
139
+ # Build feature dict matching feature_columns_v1.json order
140
+ feats = {
141
+ "rms_energy": rms_mean,
142
+ "rms_std": rms_std,
143
+ "spectral_centroid_mean": float(np.mean(cent)),
144
+ "spectral_centroid_std": float(np.std(cent)),
145
+ "spectral_flatness_mean": float(np.mean(flat)),
146
+ "spectral_flatness_std": float(np.std(flat)),
147
+ "spectral_bandwidth_mean": float(np.mean(bw)),
148
+ "spectral_bandwidth_std": float(np.std(bw)),
149
+ "spectral_rolloff_mean": float(np.mean(rolloff)),
150
+ "spectral_rolloff_std": float(np.std(rolloff)),
151
+ "spectral_contrast_mean": float(np.mean(contrast)),
152
+ "spectral_contrast_std": float(np.std(contrast)),
153
+ "mfcc_variance": float(np.mean(np.var(mfcc, axis=1))),
154
+ "mfcc_delta_var": float(np.mean(np.var(mfcc_delta, axis=1))),
155
+ "mfcc_delta2_var": float(np.mean(np.var(mfcc_delta2, axis=1))),
156
+ "zero_crossing_rate": float(np.mean(zcr)),
157
+ "zero_crossing_std": float(np.std(zcr)),
158
+ "tempo_bpm": tempo_val,
159
+ "tempo_stability": tempo_stability,
160
+ "tempo_cv": tempo_cv,
161
+ "beat_count": float(len(beats)),
162
+ "rms_dynamic_range": rms_dynamic_range,
163
+ "chroma_std": chroma_std,
164
+ "chroma_entropy": chroma_entropy,
165
+ "chroma_transition_rate": chroma_transition_rate,
166
+ "tonnetz_std": tonnetz_std,
167
+ "harmonic_ratio": harmonic_ratio,
168
+ "mel_flatness": mel_flatness,
169
+ "onset_strength_mean": float(np.mean(onset_env)),
170
+ "onset_strength_std": float(np.std(onset_env)),
171
+ "pitch_mean_hz": pitch_mean_hz,
172
+ "pitch_std_cents": pitch_std_cents,
173
+ "spectral_regularity": spectral_regularity,
174
+ "temporal_patterns": temporal_patterns,
175
+ "harmonic_structure": harmonic_structure,
176
+ "vocal_confidence": 0.0,
177
+ "vocal_ai_score": 0.0,
178
+ "vocal_energy_ratio": 0.0,
179
+ "vocal_harmonic_ratio": 0.0,
180
+ "vocal_texture_score": 0.0,
181
+ "has_vocals": 0.0,
182
+ "pitch_stability_score": float(_sigmoid(1.0 - min(pitch_std_cents / 200, 1.0), 0.5, 4)),
183
+ "vibrato_rate_hz": 0.0,
184
+ "vibrato_extent_cents": 0.0,
185
+ "vibrato_regularity_score": 0.0,
186
+ "formant_consistency_score": 0.0,
187
+ "breath_pattern_score": float(_sigmoid(rms_dynamic_range, 0.3, 5)),
188
+ }
189
+
190
+ return feats, duration_sec
191
+
192
+
193
+ # ── Prediction ──────────────────────────────────────────────────
194
+
195
+ def predict(audio_file):
196
+ """Run AURIS model on uploaded audio."""
197
+ if audio_file is None:
198
+ return (
199
+ "Dosya yükleyin / Upload a file",
200
+ None, None, None, None, None
201
+ )
202
+
203
+ t0 = time.time()
204
+
205
+ # Handle Gradio audio input (can be tuple or path)
206
+ if isinstance(audio_file, tuple):
207
+ audio_path = audio_file[0] if isinstance(audio_file[0], str) else None
208
+ if audio_path is None:
209
+ return ("Geçersiz dosya", None, None, None, None, None)
210
+ else:
211
+ audio_path = audio_file
212
+
213
+ try:
214
+ feats, duration = extract_features_from_audio(audio_path)
215
+ except Exception as e:
216
+ return (f"Hata: {e}", None, None, None, None, None)
217
+
218
+ # Build feature vector in correct column order
219
+ X = np.array([[feats.get(col, 0.0) for col in feature_cols]], dtype=np.float32)
220
+ X = np.nan_to_num(X, nan=0.0, posinf=1.0, neginf=-1.0)
221
+ X_scaled = scaler.transform(X)
222
+
223
+ elapsed = time.time() - t0
224
+
225
+ # Get probability
226
+ prob = model.predict_proba(X_scaled)[0]
227
+ ai_prob = float(prob[1])
228
+ human_prob = float(prob[0])
229
+ is_ai = ai_prob > 0.5
230
+
231
+ # Verdict
232
+ if ai_prob > 0.8:
233
+ verdict = f"AI Üretimi Müzik Tespit Edildi — %{ai_prob*100:.1f} güven"
234
+ color = "#a64b3c"
235
+ elif ai_prob > 0.5:
236
+ verdict = f"Muhtemelen AI Üretimi — %{ai_prob*100:.1f} güven"
237
+ color = "#c99347"
238
+ elif ai_prob > 0.3:
239
+ verdict = f"Muhtemelen İnsan Yapımı — %{human_prob*100:.1f} güven"
240
+ color = "#c99347"
241
+ else:
242
+ verdict = f"İnsan Yapımı Müzik — %{human_prob*100:.1f} güven"
243
+ color = "#7fb069"
244
+
245
+ # Feature scores display
246
+ sr_pct = feats["spectral_regularity"] * 100
247
+ tp_pct = feats["temporal_patterns"] * 100
248
+ hs_pct = feats["harmonic_structure"] * 100
249
+
250
+ details_md = f"""
251
+ ## Sonuç / Result
252
+
253
+ | | |
254
+ |---|---|
255
+ | **Karar** | {'AI Üretimi' if is_ai else 'İnsan Yapımı'} |
256
+ | **AI Olasılığı** | %{ai_prob*100:.1f} |
257
+ | **İnsan Olasılığı** | %{human_prob*100:.1f} |
258
+ | **Model** | {best_model_name} |
259
+ | **Süre** | {duration:.1f}s |
260
+ | **İşlem Süresi** | {elapsed:.2f}s |
261
+
262
+ ## Ses Özellik Analizi
263
+
264
+ | Özellik | Skor | Yorum |
265
+ |---------|------|-------|
266
+ | Spektral Düzenlilik | %{sr_pct:.0f} | {'AI benzeri düzenlilik' if sr_pct > 60 else 'Doğal varyasyon'} |
267
+ | Zamansal Örüntüler | %{tp_pct:.0f} | {'Metronomik hassasiyet' if tp_pct > 60 else 'Doğal zamanlama'} |
268
+ | Harmonik Yapı | %{hs_pct:.0f} | {'Tahmin edilebilir paternler' if hs_pct > 60 else 'Organik harmonik yapı'} |
269
+
270
+ ## En Önemli 10 Özellik (Bu Dosya İçin)
271
+
272
+ | Özellik | Değer | Global Önem |
273
+ |---------|-------|-------------|
274
+ """
275
+ for fname, imp in top_features:
276
+ val = feats.get(fname, 0.0)
277
+ details_md += f"| {fname} | {val:.4f} | {imp:.4f} |\n"
278
+
279
+ # Gauge plot
280
+ import matplotlib
281
+ matplotlib.use("Agg")
282
+ import matplotlib.pyplot as plt
283
+ import matplotlib.patches as mpatches
284
+
285
+ fig, ax = plt.subplots(figsize=(6, 3), subplot_kw={"projection": "polar"})
286
+ fig.patch.set_facecolor("#1a1207")
287
+
288
+ theta = np.linspace(np.pi, 0, 100)
289
+ r = np.ones(100)
290
+ # Background arc
291
+ ax.plot(theta, r, color="#3d2817", linewidth=20, alpha=0.3)
292
+ # Score arc
293
+ score_end = int(ai_prob * 100)
294
+ if score_end > 0:
295
+ c = "#7fb069" if ai_prob < 0.4 else "#c99347" if ai_prob < 0.7 else "#a64b3c"
296
+ ax.plot(theta[:score_end], r[:score_end], color=c, linewidth=20)
297
+
298
+ # Needle
299
+ needle_angle = np.pi - ai_prob * np.pi
300
+ ax.plot([needle_angle, needle_angle], [0, 0.85], color="#faf6ed", linewidth=2)
301
+ ax.scatter([needle_angle], [0.85], color="#faf6ed", s=30, zorder=5)
302
+
303
+ ax.set_ylim(0, 1.2)
304
+ ax.set_yticklabels([])
305
+ ax.set_xticklabels([])
306
+ ax.spines["polar"].set_visible(False)
307
+ ax.grid(False)
308
+
309
+ ax.text(0, -0.3, f"%{ai_prob*100:.0f}", ha="center", va="center",
310
+ fontsize=28, fontweight="bold", color="#faf6ed",
311
+ transform=ax.transAxes)
312
+ ax.text(0, -0.45, "AI Olasılığı", ha="center", va="center",
313
+ fontsize=10, color="#c99347", transform=ax.transAxes)
314
+
315
+ plt.tight_layout()
316
+ gauge_path = str(Path(__file__).parent / "_gauge_temp.png")
317
+ plt.savefig(gauge_path, dpi=100, bbox_inches="tight",
318
+ facecolor="#1a1207", edgecolor="none")
319
+ plt.close()
320
+
321
+ # Feature bars plot
322
+ fig2, ax2 = plt.subplots(figsize=(6, 2.5))
323
+ fig2.patch.set_facecolor("#1a1207")
324
+ ax2.set_facecolor("#1a1207")
325
+
326
+ bars_data = [
327
+ ("Spektral Düzenlilik", sr_pct),
328
+ ("Zamansal Örüntüler", tp_pct),
329
+ ("Harmonik Yapı", hs_pct),
330
+ ]
331
+ y_pos = np.arange(len(bars_data))
332
+ vals = [v for _, v in bars_data]
333
+ colors = ["#c99347" if v > 60 else "#7fb069" for v in vals]
334
+
335
+ ax2.barh(y_pos, vals, color=colors, edgecolor="#3d2817", height=0.6)
336
+ ax2.set_yticks(y_pos)
337
+ ax2.set_yticklabels([n for n, _ in bars_data], color="#faf6ed", fontsize=10)
338
+ ax2.set_xlim(0, 100)
339
+ ax2.set_xlabel("Skor (%)", color="#c99347")
340
+ ax2.tick_params(colors="#c99347")
341
+ ax2.spines["top"].set_visible(False)
342
+ ax2.spines["right"].set_visible(False)
343
+ ax2.spines["bottom"].set_color("#3d2817")
344
+ ax2.spines["left"].set_color("#3d2817")
345
+
346
+ for i, v in enumerate(vals):
347
+ ax2.text(v + 1, i, f"%{v:.0f}", va="center", color="#faf6ed", fontsize=10)
348
+
349
+ plt.tight_layout()
350
+ bars_path = str(Path(__file__).parent / "_bars_temp.png")
351
+ plt.savefig(bars_path, dpi=100, bbox_inches="tight",
352
+ facecolor="#1a1207", edgecolor="none")
353
+ plt.close()
354
+
355
+ return verdict, gauge_path, bars_path, details_md
356
+
357
+
358
+ # ── Figures gallery ─────────────────────────────────────────────
359
+
360
+ def get_figure_paths():
361
+ """Get all academic figure paths."""
362
+ if FIGURES_DIR.exists():
363
+ return sorted(str(p) for p in FIGURES_DIR.glob("*.png"))
364
+ return []
365
+
366
+
367
+ # ── Gradio UI ───────────────────────────────────────────────────
368
+
369
+ AURIS_CSS = """
370
+ .gradio-container {
371
+ background: linear-gradient(135deg, #1a1207 0%, #2a1f10 50%, #1a1207 100%) !important;
372
+ font-family: 'Segoe UI', sans-serif;
373
+ }
374
+ .dark { background: #1a1207 !important; }
375
+ h1, h2, h3 { color: #c99347 !important; }
376
+ p, span, label { color: #faf6ed !important; }
377
+ .gr-button-primary {
378
+ background: linear-gradient(135deg, #c99347, #e7c77a) !important;
379
+ color: #1a1207 !important;
380
+ border: none !important;
381
+ font-weight: bold !important;
382
+ }
383
+ footer { display: none !important; }
384
+ """
385
+
386
+ HEADER_MD = """
387
+ # AURIS — AI Music Detection System
388
+
389
+ **Yapay Zeka Müzik Tespit Platformu**
390
+
391
+ Model: **{model}** | Özellikler: **{n_feat}** | Veri: **{n_samples}** örnek | AUC: **{auc}**
392
+ """.format(
393
+ model=best_model_name,
394
+ n_feat=n_features,
395
+ n_samples=training_results.get("_n_samples", "?"),
396
+ auc=training_results.get(best_model_name, {}).get("roc_auc", "?"),
397
+ )
398
+
399
+ ALL_MODELS_MD = "## Tüm Model Sonuçları\n\n| Model | Accuracy | F1 | ROC-AUC | Süre |\n|-------|----------|-----|---------|------|\n"
400
+ for name, data in sorted(
401
+ ((k, v) for k, v in training_results.items()
402
+ if not k.startswith("_") and isinstance(v, dict)),
403
+ key=lambda x: -x[1].get("roc_auc", 0),
404
+ ):
405
+ ALL_MODELS_MD += (
406
+ f"| {name} | {data.get('accuracy', 0):.4f} | "
407
+ f"{data.get('f1', 0):.4f} | {data.get('roc_auc', 0):.4f} | "
408
+ f"{data.get('train_time_sec', 0):.1f}s |\n"
409
+ )
410
+
411
+
412
+ with gr.Blocks(css=AURIS_CSS, title="AURIS — AI Music Detection", theme=gr.themes.Base()) as demo:
413
+
414
+ gr.Markdown(HEADER_MD)
415
+
416
+ with gr.Tabs():
417
+ # ── Tab 1: Analysis ──
418
+ with gr.Tab("Analiz / Analysis"):
419
+ with gr.Row():
420
+ with gr.Column(scale=1):
421
+ audio_input = gr.Audio(
422
+ label="Audio Dosyası Yükle",
423
+ type="filepath",
424
+ )
425
+ analyze_btn = gr.Button(
426
+ "Analiz Et / Analyze",
427
+ variant="primary",
428
+ size="lg",
429
+ )
430
+
431
+ with gr.Column(scale=1):
432
+ verdict_text = gr.Textbox(
433
+ label="Sonuç / Verdict",
434
+ interactive=False,
435
+ lines=2,
436
+ )
437
+ gauge_img = gr.Image(
438
+ label="AI Olasılığı",
439
+ type="filepath",
440
+ height=200,
441
+ )
442
+ bars_img = gr.Image(
443
+ label="Özellik Skorları",
444
+ type="filepath",
445
+ height=180,
446
+ )
447
+
448
+ details_output = gr.Markdown(label="Detaylar")
449
+
450
+ analyze_btn.click(
451
+ fn=predict,
452
+ inputs=[audio_input],
453
+ outputs=[verdict_text, gauge_img, bars_img, details_output],
454
+ )
455
+
456
+ # ── Tab 2: Model Comparison ──
457
+ with gr.Tab("Model Karşılaştırması"):
458
+ gr.Markdown(ALL_MODELS_MD)
459
+
460
+ # ── Tab 3: Academic Figures ──
461
+ with gr.Tab("Akademik Görseller"):
462
+ gr.Markdown("## Eğitim ve Değerlendirme Görselleri")
463
+ figure_paths = get_figure_paths()
464
+ if figure_paths:
465
+ gr.Gallery(
466
+ value=figure_paths,
467
+ label="Figures",
468
+ columns=3,
469
+ height="auto",
470
+ object_fit="contain",
471
+ )
472
+ else:
473
+ gr.Markdown("*Görseller bulunamadı.*")
474
+
475
+ gr.Markdown(
476
+ "---\n"
477
+ "*AURIS v1 — Düzce Üniversitesi Bilgisayar Mühendisliği Bitirme Projesi*\n\n"
478
+ "*Hasan Arthur Altuntaş — 2026*"
479
+ )
480
+
481
+
482
+ if __name__ == "__main__":
483
+ demo.launch(
484
+ server_name="0.0.0.0",
485
+ server_port=7861,
486
+ share=False,
487
+ inbrowser=True,
488
+ )