Spaces:
Sleeping
Sleeping
Add Gradio dependency to requirements.txt with version constraints
Browse files- app/training/evaluate.py +2 -2
- app/training/train_classifier.py +421 -154
- local_demo.py +901 -503
- requirements.txt +2 -1
app/training/evaluate.py
CHANGED
|
@@ -111,7 +111,7 @@ def evaluate_predictions(
|
|
| 111 |
|
| 112 |
# Print report
|
| 113 |
print(f"\n{'=' * 50}")
|
| 114 |
-
print(f" {title}
|
| 115 |
print(f"{'=' * 50}")
|
| 116 |
print(f" Accuracy: {acc:.4f} ({acc:.1%})")
|
| 117 |
print(f" Precision: {prec:.4f}")
|
|
@@ -176,7 +176,7 @@ def evaluate_heuristic_baseline(features_csv: str | Path) -> dict:
|
|
| 176 |
y_pred_combined = (combined_scores > 0.5).astype(int)
|
| 177 |
|
| 178 |
print("\n" + "=" * 60)
|
| 179 |
-
print(" BASELINE EVALUATION
|
| 180 |
print("=" * 60)
|
| 181 |
|
| 182 |
print("\n--- Heuristic Only (spectral + temporal + harmonic) ---")
|
|
|
|
| 111 |
|
| 112 |
# Print report
|
| 113 |
print(f"\n{'=' * 50}")
|
| 114 |
+
print(f" {title} - Evaluation Report")
|
| 115 |
print(f"{'=' * 50}")
|
| 116 |
print(f" Accuracy: {acc:.4f} ({acc:.1%})")
|
| 117 |
print(f" Precision: {prec:.4f}")
|
|
|
|
| 176 |
y_pred_combined = (combined_scores > 0.5).astype(int)
|
| 177 |
|
| 178 |
print("\n" + "=" * 60)
|
| 179 |
+
print(" BASELINE EVALUATION - Current Heuristic System")
|
| 180 |
print("=" * 60)
|
| 181 |
|
| 182 |
print("\n--- Heuristic Only (spectral + temporal + harmonic) ---")
|
app/training/train_classifier.py
CHANGED
|
@@ -2,25 +2,26 @@
|
|
| 2 |
Comprehensive multi-model training pipeline for AURIS.
|
| 3 |
|
| 4 |
Trains and evaluates multiple classifier families on extracted
|
| 5 |
-
audio features using stratified
|
| 6 |
-
|
| 7 |
|
| 8 |
Models compared:
|
|
|
|
| 9 |
- Random Forest
|
| 10 |
- Gradient Boosting
|
| 11 |
-
- XGBoost
|
| 12 |
-
- LightGBM
|
| 13 |
- Support Vector Machine (RBF)
|
| 14 |
-
- Multi-Layer Perceptron
|
|
|
|
|
|
|
| 15 |
|
| 16 |
Usage:
|
| 17 |
python -m app.training.train_classifier data/training/features.csv
|
| 18 |
|
| 19 |
Outputs:
|
| 20 |
-
models/auris_classifier_v1.pkl
|
| 21 |
-
models/feature_scaler_v1.pkl
|
| 22 |
-
models/feature_columns_v1.json
|
| 23 |
-
models/training_results.json
|
| 24 |
"""
|
| 25 |
|
| 26 |
from __future__ import annotations
|
|
@@ -30,23 +31,15 @@ import json
|
|
| 30 |
import pickle
|
| 31 |
import sys
|
| 32 |
import time
|
|
|
|
| 33 |
from pathlib import Path
|
| 34 |
from typing import Any
|
| 35 |
|
| 36 |
import numpy as np
|
| 37 |
-
|
| 38 |
-
from sklearn.ensemble import
|
| 39 |
-
|
| 40 |
-
RandomForestClassifier,
|
| 41 |
-
)
|
| 42 |
from sklearn.linear_model import LogisticRegression
|
| 43 |
-
from sklearn.neural_network import MLPClassifier
|
| 44 |
-
from sklearn.svm import SVC
|
| 45 |
-
from sklearn.model_selection import (
|
| 46 |
-
StratifiedKFold,
|
| 47 |
-
cross_val_predict,
|
| 48 |
-
)
|
| 49 |
-
from sklearn.preprocessing import StandardScaler
|
| 50 |
from sklearn.metrics import (
|
| 51 |
accuracy_score,
|
| 52 |
f1_score,
|
|
@@ -54,10 +47,16 @@ from sklearn.metrics import (
|
|
| 54 |
recall_score,
|
| 55 |
roc_auc_score,
|
| 56 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
# Optional: XGBoost
|
| 59 |
try:
|
| 60 |
import xgboost as xgb
|
|
|
|
| 61 |
HAS_XGB = True
|
| 62 |
except ImportError:
|
| 63 |
HAS_XGB = False
|
|
@@ -65,15 +64,63 @@ except ImportError:
|
|
| 65 |
# Optional: LightGBM
|
| 66 |
try:
|
| 67 |
import lightgbm as lgb
|
|
|
|
| 68 |
HAS_LGBM = True
|
| 69 |
except ImportError:
|
| 70 |
HAS_LGBM = False
|
| 71 |
|
| 72 |
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
| 73 |
-
from app.training.evaluate import
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
def train(
|
|
@@ -87,53 +134,42 @@ def train(
|
|
| 87 |
Returns:
|
| 88 |
Dict with per-model metrics, best model info, and saved paths.
|
| 89 |
"""
|
|
|
|
| 90 |
models_dir = Path(models_dir)
|
| 91 |
models_dir.mkdir(parents=True, exist_ok=True)
|
| 92 |
|
| 93 |
-
# ── Load data ──────────────────────────────────
|
| 94 |
X, y = load_features_csv(features_csv)
|
| 95 |
-
|
| 96 |
-
with open(features_csv, "r", encoding="utf-8") as f:
|
| 97 |
-
reader = csv.DictReader(f)
|
| 98 |
-
# duration_sec and sample_rate are metadata, not audio features —
|
| 99 |
-
# including them causes data leakage (duration correlates with source, not content)
|
| 100 |
-
_EXCLUDE = {"file_path", "label_int", "duration_sec", "sample_rate"}
|
| 101 |
-
feature_cols = [
|
| 102 |
-
c for c in reader.fieldnames
|
| 103 |
-
if c not in _EXCLUDE
|
| 104 |
-
]
|
| 105 |
-
|
| 106 |
-
# ── Handle NaN/Inf ─���───────────────────────────
|
| 107 |
X = np.nan_to_num(X, nan=0.0, posinf=1.0, neginf=-1.0)
|
| 108 |
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
scaler = StandardScaler()
|
| 111 |
X_scaled = scaler.fit_transform(X)
|
| 112 |
|
| 113 |
-
# ── Train multiple models ──────────────────────
|
| 114 |
-
candidates = _build_candidates()
|
| 115 |
-
cv = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=42)
|
| 116 |
-
|
| 117 |
-
best_model = None
|
| 118 |
best_name = ""
|
| 119 |
-
best_auc =
|
| 120 |
all_results: dict[str, dict[str, Any]] = {}
|
| 121 |
|
| 122 |
-
for name, model in
|
| 123 |
-
print(
|
| 124 |
-
print(f"
|
| 125 |
-
print(
|
| 126 |
|
| 127 |
t0 = time.time()
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
acc = accuracy_score(y, y_pred)
|
| 139 |
prec = precision_score(y, y_pred, zero_division=0)
|
|
@@ -141,12 +177,14 @@ def train(
|
|
| 141 |
f1 = f1_score(y, y_pred, zero_division=0)
|
| 142 |
auc = roc_auc_score(y, y_prob)
|
| 143 |
|
| 144 |
-
|
| 145 |
-
print(f"
|
| 146 |
-
print(f"
|
| 147 |
-
print(f"
|
| 148 |
-
print(f"
|
| 149 |
-
print(f"
|
|
|
|
|
|
|
| 150 |
|
| 151 |
all_results[name] = {
|
| 152 |
"accuracy": round(acc, 4),
|
|
@@ -154,7 +192,10 @@ def train(
|
|
| 154 |
"recall": round(rec, 4),
|
| 155 |
"f1": round(f1, 4),
|
| 156 |
"roc_auc": round(auc, 4),
|
| 157 |
-
"
|
|
|
|
|
|
|
|
|
|
| 158 |
"y_true": y.tolist(),
|
| 159 |
"y_pred": y_pred.tolist(),
|
| 160 |
"y_prob": y_prob.tolist(),
|
|
@@ -163,48 +204,38 @@ def train(
|
|
| 163 |
if auc > best_auc:
|
| 164 |
best_auc = auc
|
| 165 |
best_name = name
|
| 166 |
-
best_model = model
|
| 167 |
|
| 168 |
-
|
| 169 |
-
print(f"
|
| 170 |
-
print(
|
| 171 |
-
print(f"{'═' * 60}")
|
| 172 |
|
| 173 |
y_prob_best = np.array(all_results[best_name]["y_prob"])
|
| 174 |
y_pred_best = np.array(all_results[best_name]["y_pred"])
|
|
|
|
| 175 |
|
| 176 |
-
|
| 177 |
-
y, y_pred_best, y_prob_best,
|
| 178 |
-
title=f"Best: {best_name}",
|
| 179 |
-
)
|
| 180 |
-
|
| 181 |
-
# ── Train ALL models on full data ────────────────
|
| 182 |
all_model_paths: dict[str, str] = {}
|
| 183 |
-
for name, model in
|
| 184 |
-
print(f"\
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
with open(model_pkl, "wb") as f:
|
| 189 |
-
pickle.dump(
|
| 190 |
all_model_paths[name] = str(model_pkl)
|
| 191 |
-
print(f"
|
| 192 |
|
| 193 |
-
best_model =
|
| 194 |
-
for name, model in candidates:
|
| 195 |
-
if name == best_name:
|
| 196 |
-
best_model = model
|
| 197 |
-
break
|
| 198 |
-
|
| 199 |
-
# ── Feature importance ─────────────────────────
|
| 200 |
importance_data = _extract_importance(best_model, feature_cols)
|
| 201 |
if importance_data:
|
| 202 |
print("\nTop 15 features:")
|
| 203 |
for fname, imp in importance_data[:15]:
|
| 204 |
-
|
| 205 |
-
print(f" {fname:<35} {imp:.4f} {bar}")
|
| 206 |
|
| 207 |
-
# ── Save artifacts ─────────────────────────────
|
| 208 |
model_path = models_dir / "auris_classifier_v1.pkl"
|
| 209 |
scaler_path = models_dir / "feature_scaler_v1.pkl"
|
| 210 |
columns_path = models_dir / "feature_columns_v1.json"
|
|
@@ -214,31 +245,38 @@ def train(
|
|
| 214 |
pickle.dump(best_model, f)
|
| 215 |
with open(scaler_path, "wb") as f:
|
| 216 |
pickle.dump(scaler, f)
|
| 217 |
-
with open(columns_path, "w") as f:
|
| 218 |
json.dump(feature_cols, f, indent=2)
|
| 219 |
|
| 220 |
-
|
| 221 |
-
json_results = {}
|
| 222 |
for name, data in all_results.items():
|
| 223 |
json_results[name] = {
|
| 224 |
-
|
| 225 |
-
|
|
|
|
| 226 |
}
|
| 227 |
json_results["_best_model"] = best_name
|
| 228 |
json_results["_n_samples"] = len(y)
|
| 229 |
json_results["_n_features"] = X.shape[1]
|
| 230 |
json_results["_n_folds"] = n_folds
|
| 231 |
-
json_results["
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
json_results["_model_paths"] = all_model_paths
|
| 233 |
if importance_data:
|
| 234 |
json_results["_feature_importance"] = {
|
| 235 |
-
|
| 236 |
}
|
| 237 |
|
| 238 |
-
with open(results_path, "w") as f:
|
| 239 |
json.dump(json_results, f, indent=2)
|
| 240 |
|
| 241 |
-
print(
|
| 242 |
print(f" Model: {model_path}")
|
| 243 |
print(f" Scaler: {scaler_path}")
|
| 244 |
print(f" Columns: {columns_path}")
|
|
@@ -253,116 +291,346 @@ def train(
|
|
| 253 |
}
|
| 254 |
|
| 255 |
|
| 256 |
-
def
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
LogisticRegression(
|
| 262 |
-
C=
|
| 263 |
-
max_iter=
|
| 264 |
class_weight="balanced",
|
| 265 |
random_state=42,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
),
|
| 267 |
-
),
|
| 268 |
-
(
|
| 269 |
-
"Random Forest",
|
| 270 |
RandomForestClassifier(
|
| 271 |
-
n_estimators=
|
| 272 |
-
max_depth=
|
| 273 |
-
min_samples_leaf=
|
| 274 |
-
min_samples_split=
|
| 275 |
-
|
|
|
|
| 276 |
random_state=42,
|
| 277 |
n_jobs=-1,
|
| 278 |
),
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
"Gradient Boosting",
|
| 282 |
GradientBoostingClassifier(
|
| 283 |
n_estimators=200,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
max_depth=4,
|
| 285 |
-
learning_rate=0.
|
| 286 |
subsample=0.75,
|
| 287 |
-
min_samples_leaf=
|
| 288 |
-
min_samples_split=
|
| 289 |
random_state=42,
|
| 290 |
),
|
| 291 |
-
|
| 292 |
-
(
|
| 293 |
-
"SVM (RBF)",
|
| 294 |
SVC(
|
| 295 |
kernel="rbf",
|
| 296 |
-
C=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
gamma="scale",
|
| 298 |
class_weight="balanced",
|
| 299 |
probability=True,
|
| 300 |
random_state=42,
|
| 301 |
),
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
MLPClassifier(
|
| 306 |
-
hidden_layer_sizes=(128, 64
|
| 307 |
activation="relu",
|
| 308 |
solver="adam",
|
| 309 |
-
alpha=0.
|
| 310 |
learning_rate="adaptive",
|
| 311 |
max_iter=500,
|
| 312 |
early_stopping=True,
|
| 313 |
validation_fraction=0.15,
|
| 314 |
random_state=42,
|
| 315 |
),
|
| 316 |
-
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
if HAS_XGB:
|
| 320 |
-
|
| 321 |
-
"XGBoost",
|
| 322 |
xgb.XGBClassifier(
|
| 323 |
-
n_estimators=
|
| 324 |
max_depth=4,
|
| 325 |
-
learning_rate=0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
subsample=0.75,
|
| 327 |
colsample_bytree=0.75,
|
| 328 |
-
min_child_weight=
|
| 329 |
-
reg_alpha=0.
|
| 330 |
reg_lambda=1.5,
|
| 331 |
gamma=0.2,
|
| 332 |
-
scale_pos_weight=1.0,
|
| 333 |
eval_metric="logloss",
|
|
|
|
| 334 |
random_state=42,
|
|
|
|
| 335 |
verbosity=0,
|
| 336 |
),
|
| 337 |
-
|
| 338 |
|
| 339 |
if HAS_LGBM:
|
| 340 |
-
|
| 341 |
-
"LightGBM",
|
| 342 |
lgb.LGBMClassifier(
|
| 343 |
-
n_estimators=
|
| 344 |
-
max_depth=
|
| 345 |
-
learning_rate=0.
|
| 346 |
-
num_leaves=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
subsample=0.75,
|
| 348 |
colsample_bytree=0.75,
|
| 349 |
-
|
| 350 |
reg_alpha=0.3,
|
| 351 |
reg_lambda=1.5,
|
| 352 |
class_weight="balanced",
|
| 353 |
random_state=42,
|
| 354 |
verbose=-1,
|
| 355 |
),
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
| 359 |
|
| 360 |
|
| 361 |
def _extract_importance(
|
| 362 |
model: Any,
|
| 363 |
feature_cols: list[str],
|
| 364 |
) -> list[tuple[str, float]]:
|
| 365 |
-
"""Extract feature importance from the trained model."""
|
| 366 |
importances = None
|
| 367 |
|
| 368 |
if hasattr(model, "feature_importances_"):
|
|
@@ -373,14 +641,13 @@ def _extract_importance(
|
|
| 373 |
if importances is None:
|
| 374 |
return []
|
| 375 |
|
| 376 |
-
# Normalize to sum to 1
|
| 377 |
total = np.sum(importances)
|
| 378 |
if total > 0:
|
| 379 |
importances = importances / total
|
| 380 |
|
| 381 |
return sorted(
|
| 382 |
zip(feature_cols, importances.tolist()),
|
| 383 |
-
key=lambda
|
| 384 |
reverse=True,
|
| 385 |
)
|
| 386 |
|
|
|
|
| 2 |
Comprehensive multi-model training pipeline for AURIS.
|
| 3 |
|
| 4 |
Trains and evaluates multiple classifier families on extracted
|
| 5 |
+
audio features using stratified cross-validation, then selects
|
| 6 |
+
the best model and exports it for production use.
|
| 7 |
|
| 8 |
Models compared:
|
| 9 |
+
- Logistic Regression
|
| 10 |
- Random Forest
|
| 11 |
- Gradient Boosting
|
|
|
|
|
|
|
| 12 |
- Support Vector Machine (RBF)
|
| 13 |
+
- Multi-Layer Perceptron
|
| 14 |
+
- XGBoost (optional)
|
| 15 |
+
- LightGBM (optional)
|
| 16 |
|
| 17 |
Usage:
|
| 18 |
python -m app.training.train_classifier data/training/features.csv
|
| 19 |
|
| 20 |
Outputs:
|
| 21 |
+
models/auris_classifier_v1.pkl - best trained model
|
| 22 |
+
models/feature_scaler_v1.pkl - fitted StandardScaler
|
| 23 |
+
models/feature_columns_v1.json - ordered feature column names
|
| 24 |
+
models/training_results.json - model metrics and metadata
|
| 25 |
"""
|
| 26 |
|
| 27 |
from __future__ import annotations
|
|
|
|
| 31 |
import pickle
|
| 32 |
import sys
|
| 33 |
import time
|
| 34 |
+
import warnings
|
| 35 |
from pathlib import Path
|
| 36 |
from typing import Any
|
| 37 |
|
| 38 |
import numpy as np
|
| 39 |
+
from sklearn.base import clone
|
| 40 |
+
from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier
|
| 41 |
+
from sklearn.exceptions import ConvergenceWarning
|
|
|
|
|
|
|
| 42 |
from sklearn.linear_model import LogisticRegression
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
from sklearn.metrics import (
|
| 44 |
accuracy_score,
|
| 45 |
f1_score,
|
|
|
|
| 47 |
recall_score,
|
| 48 |
roc_auc_score,
|
| 49 |
)
|
| 50 |
+
from sklearn.model_selection import StratifiedKFold, cross_val_predict, train_test_split
|
| 51 |
+
from sklearn.neural_network import MLPClassifier
|
| 52 |
+
from sklearn.pipeline import Pipeline
|
| 53 |
+
from sklearn.preprocessing import StandardScaler
|
| 54 |
+
from sklearn.svm import SVC
|
| 55 |
|
| 56 |
# Optional: XGBoost
|
| 57 |
try:
|
| 58 |
import xgboost as xgb
|
| 59 |
+
|
| 60 |
HAS_XGB = True
|
| 61 |
except ImportError:
|
| 62 |
HAS_XGB = False
|
|
|
|
| 64 |
# Optional: LightGBM
|
| 65 |
try:
|
| 66 |
import lightgbm as lgb
|
| 67 |
+
|
| 68 |
HAS_LGBM = True
|
| 69 |
except ImportError:
|
| 70 |
HAS_LGBM = False
|
| 71 |
|
| 72 |
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
| 73 |
+
from app.training.evaluate import evaluate_predictions, load_features_csv
|
| 74 |
+
|
| 75 |
+
_EXCLUDED_COLUMNS = {"file_path", "label_int", "duration_sec", "sample_rate"}
|
| 76 |
+
_TUNED_PARAM_KEYS: dict[str, tuple[str, ...]] = {
|
| 77 |
+
"Logistic Regression": ("C", "class_weight", "max_iter"),
|
| 78 |
+
"Random Forest": (
|
| 79 |
+
"n_estimators",
|
| 80 |
+
"max_depth",
|
| 81 |
+
"min_samples_leaf",
|
| 82 |
+
"min_samples_split",
|
| 83 |
+
"class_weight",
|
| 84 |
+
"max_features",
|
| 85 |
+
),
|
| 86 |
+
"Gradient Boosting": (
|
| 87 |
+
"n_estimators",
|
| 88 |
+
"max_depth",
|
| 89 |
+
"learning_rate",
|
| 90 |
+
"subsample",
|
| 91 |
+
"min_samples_leaf",
|
| 92 |
+
"min_samples_split",
|
| 93 |
+
),
|
| 94 |
+
"SVM (RBF)": ("C", "gamma", "class_weight"),
|
| 95 |
+
"MLP Neural Network": (
|
| 96 |
+
"hidden_layer_sizes",
|
| 97 |
+
"alpha",
|
| 98 |
+
"max_iter",
|
| 99 |
+
"validation_fraction",
|
| 100 |
+
),
|
| 101 |
+
"XGBoost": (
|
| 102 |
+
"n_estimators",
|
| 103 |
+
"max_depth",
|
| 104 |
+
"learning_rate",
|
| 105 |
+
"subsample",
|
| 106 |
+
"colsample_bytree",
|
| 107 |
+
"min_child_weight",
|
| 108 |
+
"reg_alpha",
|
| 109 |
+
"reg_lambda",
|
| 110 |
+
"gamma",
|
| 111 |
+
),
|
| 112 |
+
"LightGBM": (
|
| 113 |
+
"n_estimators",
|
| 114 |
+
"max_depth",
|
| 115 |
+
"learning_rate",
|
| 116 |
+
"num_leaves",
|
| 117 |
+
"subsample",
|
| 118 |
+
"colsample_bytree",
|
| 119 |
+
"min_child_samples",
|
| 120 |
+
"reg_alpha",
|
| 121 |
+
"reg_lambda",
|
| 122 |
+
),
|
| 123 |
+
}
|
| 124 |
|
| 125 |
|
| 126 |
def train(
|
|
|
|
| 134 |
Returns:
|
| 135 |
Dict with per-model metrics, best model info, and saved paths.
|
| 136 |
"""
|
| 137 |
+
features_csv = Path(features_csv)
|
| 138 |
models_dir = Path(models_dir)
|
| 139 |
models_dir.mkdir(parents=True, exist_ok=True)
|
| 140 |
|
|
|
|
| 141 |
X, y = load_features_csv(features_csv)
|
| 142 |
+
feature_cols = _load_feature_columns(features_csv)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
X = np.nan_to_num(X, nan=0.0, posinf=1.0, neginf=-1.0)
|
| 144 |
|
| 145 |
+
selected_candidates, tuning_results = _select_best_candidates(X, y)
|
| 146 |
+
cv = StratifiedKFold(n_splits=n_folds, shuffle=True, random_state=42)
|
| 147 |
+
|
| 148 |
scaler = StandardScaler()
|
| 149 |
X_scaled = scaler.fit_transform(X)
|
| 150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
best_name = ""
|
| 152 |
+
best_auc = -1.0
|
| 153 |
all_results: dict[str, dict[str, Any]] = {}
|
| 154 |
|
| 155 |
+
for name, model in selected_candidates:
|
| 156 |
+
print("\n" + "-" * 56)
|
| 157 |
+
print(f"Training: {name}")
|
| 158 |
+
print("-" * 56)
|
| 159 |
|
| 160 |
t0 = time.time()
|
| 161 |
+
pipeline = _build_eval_pipeline(model)
|
| 162 |
+
with warnings.catch_warnings():
|
| 163 |
+
warnings.simplefilter("ignore", category=ConvergenceWarning)
|
| 164 |
+
y_prob = cross_val_predict(
|
| 165 |
+
pipeline,
|
| 166 |
+
X,
|
| 167 |
+
y,
|
| 168 |
+
cv=cv,
|
| 169 |
+
method="predict_proba",
|
| 170 |
+
)[:, 1]
|
| 171 |
+
y_pred = (y_prob >= 0.5).astype(int)
|
| 172 |
+
cv_time = time.time() - t0
|
| 173 |
|
| 174 |
acc = accuracy_score(y, y_pred)
|
| 175 |
prec = precision_score(y, y_pred, zero_division=0)
|
|
|
|
| 177 |
f1 = f1_score(y, y_pred, zero_division=0)
|
| 178 |
auc = roc_auc_score(y, y_prob)
|
| 179 |
|
| 180 |
+
tuning_meta = tuning_results.get(name, {})
|
| 181 |
+
print(f" Validation AUC: {tuning_meta.get('validation_auc', 0.0):.4f}")
|
| 182 |
+
print(f" CV Accuracy: {acc:.4f}")
|
| 183 |
+
print(f" CV Precision: {prec:.4f}")
|
| 184 |
+
print(f" CV Recall: {rec:.4f}")
|
| 185 |
+
print(f" CV F1 Score: {f1:.4f}")
|
| 186 |
+
print(f" CV ROC-AUC: {auc:.4f}")
|
| 187 |
+
print(f" CV Time: {cv_time:.1f}s")
|
| 188 |
|
| 189 |
all_results[name] = {
|
| 190 |
"accuracy": round(acc, 4),
|
|
|
|
| 192 |
"recall": round(rec, 4),
|
| 193 |
"f1": round(f1, 4),
|
| 194 |
"roc_auc": round(auc, 4),
|
| 195 |
+
"validation_auc": round(tuning_meta.get("validation_auc", 0.0), 4),
|
| 196 |
+
"selection_time_sec": round(tuning_meta.get("selection_time_sec", 0.0), 2),
|
| 197 |
+
"train_time_sec": round(cv_time, 2),
|
| 198 |
+
"selected_params": tuning_meta.get("selected_params", {}),
|
| 199 |
"y_true": y.tolist(),
|
| 200 |
"y_pred": y_pred.tolist(),
|
| 201 |
"y_prob": y_prob.tolist(),
|
|
|
|
| 204 |
if auc > best_auc:
|
| 205 |
best_auc = auc
|
| 206 |
best_name = name
|
|
|
|
| 207 |
|
| 208 |
+
print("\n" + "=" * 64)
|
| 209 |
+
print(f"BEST MODEL: {best_name} (ROC-AUC = {best_auc:.4f})")
|
| 210 |
+
print("=" * 64)
|
|
|
|
| 211 |
|
| 212 |
y_prob_best = np.array(all_results[best_name]["y_prob"])
|
| 213 |
y_pred_best = np.array(all_results[best_name]["y_pred"])
|
| 214 |
+
evaluate_predictions(y, y_pred_best, y_prob_best, title=f"Best: {best_name}")
|
| 215 |
|
| 216 |
+
fitted_models: dict[str, Any] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
all_model_paths: dict[str, str] = {}
|
| 218 |
+
for name, model in selected_candidates:
|
| 219 |
+
print(f"\nFitting final {name} on all {len(y)} samples...")
|
| 220 |
+
final_model = clone(model)
|
| 221 |
+
with warnings.catch_warnings():
|
| 222 |
+
warnings.simplefilter("ignore", category=ConvergenceWarning)
|
| 223 |
+
final_model.fit(X_scaled, y)
|
| 224 |
+
fitted_models[name] = final_model
|
| 225 |
+
|
| 226 |
+
model_pkl = models_dir / f"model_{_safe_model_name(name)}.pkl"
|
| 227 |
with open(model_pkl, "wb") as f:
|
| 228 |
+
pickle.dump(final_model, f)
|
| 229 |
all_model_paths[name] = str(model_pkl)
|
| 230 |
+
print(f" Saved: {model_pkl}")
|
| 231 |
|
| 232 |
+
best_model = fitted_models[best_name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
importance_data = _extract_importance(best_model, feature_cols)
|
| 234 |
if importance_data:
|
| 235 |
print("\nTop 15 features:")
|
| 236 |
for fname, imp in importance_data[:15]:
|
| 237 |
+
print(f" {fname:<35} {imp:.4f}")
|
|
|
|
| 238 |
|
|
|
|
| 239 |
model_path = models_dir / "auris_classifier_v1.pkl"
|
| 240 |
scaler_path = models_dir / "feature_scaler_v1.pkl"
|
| 241 |
columns_path = models_dir / "feature_columns_v1.json"
|
|
|
|
| 245 |
pickle.dump(best_model, f)
|
| 246 |
with open(scaler_path, "wb") as f:
|
| 247 |
pickle.dump(scaler, f)
|
| 248 |
+
with open(columns_path, "w", encoding="utf-8") as f:
|
| 249 |
json.dump(feature_cols, f, indent=2)
|
| 250 |
|
| 251 |
+
json_results: dict[str, Any] = {}
|
|
|
|
| 252 |
for name, data in all_results.items():
|
| 253 |
json_results[name] = {
|
| 254 |
+
key: value
|
| 255 |
+
for key, value in data.items()
|
| 256 |
+
if key not in ("y_true", "y_pred", "y_prob")
|
| 257 |
}
|
| 258 |
json_results["_best_model"] = best_name
|
| 259 |
json_results["_n_samples"] = len(y)
|
| 260 |
json_results["_n_features"] = X.shape[1]
|
| 261 |
json_results["_n_folds"] = n_folds
|
| 262 |
+
json_results["_dataset_path"] = str(features_csv)
|
| 263 |
+
json_results["_class_balance"] = {
|
| 264 |
+
"ai": int(np.sum(y == 1)),
|
| 265 |
+
"human": int(np.sum(y == 0)),
|
| 266 |
+
}
|
| 267 |
+
json_results["_data_leakage_fix"] = (
|
| 268 |
+
"duration_sec and sample_rate removed from features; scaler fitted per fold during CV"
|
| 269 |
+
)
|
| 270 |
json_results["_model_paths"] = all_model_paths
|
| 271 |
if importance_data:
|
| 272 |
json_results["_feature_importance"] = {
|
| 273 |
+
feature_name: round(imp, 6) for feature_name, imp in importance_data
|
| 274 |
}
|
| 275 |
|
| 276 |
+
with open(results_path, "w", encoding="utf-8") as f:
|
| 277 |
json.dump(json_results, f, indent=2)
|
| 278 |
|
| 279 |
+
print("\nSaved artifacts:")
|
| 280 |
print(f" Model: {model_path}")
|
| 281 |
print(f" Scaler: {scaler_path}")
|
| 282 |
print(f" Columns: {columns_path}")
|
|
|
|
| 291 |
}
|
| 292 |
|
| 293 |
|
| 294 |
+
def _load_feature_columns(features_csv: Path) -> list[str]:
|
| 295 |
+
with open(features_csv, "r", encoding="utf-8") as f:
|
| 296 |
+
reader = csv.DictReader(f)
|
| 297 |
+
return [
|
| 298 |
+
column
|
| 299 |
+
for column in (reader.fieldnames or [])
|
| 300 |
+
if column not in _EXCLUDED_COLUMNS
|
| 301 |
+
]
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
def _select_best_candidates(
|
| 305 |
+
X: np.ndarray,
|
| 306 |
+
y: np.ndarray,
|
| 307 |
+
) -> tuple[list[tuple[str, Any]], dict[str, dict[str, Any]]]:
|
| 308 |
+
"""
|
| 309 |
+
Pick one tuned configuration per model family using a stratified holdout.
|
| 310 |
+
"""
|
| 311 |
+
X_train, X_val, y_train, y_val = train_test_split(
|
| 312 |
+
X,
|
| 313 |
+
y,
|
| 314 |
+
test_size=0.2,
|
| 315 |
+
stratify=y,
|
| 316 |
+
random_state=42,
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
selected: list[tuple[str, Any]] = []
|
| 320 |
+
tuning_results: dict[str, dict[str, Any]] = {}
|
| 321 |
+
|
| 322 |
+
for name, variants in _build_candidate_families().items():
|
| 323 |
+
print("\n" + "." * 56)
|
| 324 |
+
print(f"Selecting hyperparameters for: {name}")
|
| 325 |
+
print("." * 56)
|
| 326 |
+
|
| 327 |
+
best_model = None
|
| 328 |
+
best_auc = -1.0
|
| 329 |
+
best_params: dict[str, Any] = {}
|
| 330 |
+
selection_start = time.time()
|
| 331 |
+
|
| 332 |
+
for idx, model in enumerate(variants, start=1):
|
| 333 |
+
pipeline = _build_eval_pipeline(model)
|
| 334 |
+
with warnings.catch_warnings():
|
| 335 |
+
warnings.simplefilter("ignore", category=ConvergenceWarning)
|
| 336 |
+
pipeline.fit(X_train, y_train)
|
| 337 |
+
y_prob = pipeline.predict_proba(X_val)[:, 1]
|
| 338 |
+
auc = roc_auc_score(y_val, y_prob)
|
| 339 |
+
params = _summarize_selected_params(name, model)
|
| 340 |
+
|
| 341 |
+
print(f" Candidate {idx}: holdout AUC={auc:.4f} | params={params}")
|
| 342 |
+
if auc > best_auc:
|
| 343 |
+
best_auc = auc
|
| 344 |
+
best_model = model
|
| 345 |
+
best_params = params
|
| 346 |
+
|
| 347 |
+
if best_model is None:
|
| 348 |
+
raise RuntimeError(f"No valid candidate selected for {name}")
|
| 349 |
+
|
| 350 |
+
tuning_results[name] = {
|
| 351 |
+
"validation_auc": float(best_auc),
|
| 352 |
+
"selected_params": best_params,
|
| 353 |
+
"selection_time_sec": time.time() - selection_start,
|
| 354 |
+
}
|
| 355 |
+
selected.append((name, best_model))
|
| 356 |
+
print(f" Selected {name}: AUC={best_auc:.4f}")
|
| 357 |
+
|
| 358 |
+
return selected, tuning_results
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def _build_candidate_families() -> dict[str, list[Any]]:
|
| 362 |
+
families: dict[str, list[Any]] = {
|
| 363 |
+
"Logistic Regression": [
|
| 364 |
LogisticRegression(
|
| 365 |
+
C=value,
|
| 366 |
+
max_iter=2500,
|
| 367 |
class_weight="balanced",
|
| 368 |
random_state=42,
|
| 369 |
+
)
|
| 370 |
+
for value in (0.25, 0.5, 1.0, 2.0)
|
| 371 |
+
],
|
| 372 |
+
"Random Forest": [
|
| 373 |
+
RandomForestClassifier(
|
| 374 |
+
n_estimators=300,
|
| 375 |
+
max_depth=12,
|
| 376 |
+
min_samples_leaf=4,
|
| 377 |
+
min_samples_split=8,
|
| 378 |
+
max_features="sqrt",
|
| 379 |
+
class_weight="balanced_subsample",
|
| 380 |
+
random_state=42,
|
| 381 |
+
n_jobs=-1,
|
| 382 |
+
),
|
| 383 |
+
RandomForestClassifier(
|
| 384 |
+
n_estimators=450,
|
| 385 |
+
max_depth=18,
|
| 386 |
+
min_samples_leaf=2,
|
| 387 |
+
min_samples_split=4,
|
| 388 |
+
max_features="sqrt",
|
| 389 |
+
class_weight="balanced_subsample",
|
| 390 |
+
random_state=42,
|
| 391 |
+
n_jobs=-1,
|
| 392 |
),
|
|
|
|
|
|
|
|
|
|
| 393 |
RandomForestClassifier(
|
| 394 |
+
n_estimators=500,
|
| 395 |
+
max_depth=None,
|
| 396 |
+
min_samples_leaf=1,
|
| 397 |
+
min_samples_split=2,
|
| 398 |
+
max_features="log2",
|
| 399 |
+
class_weight="balanced_subsample",
|
| 400 |
random_state=42,
|
| 401 |
n_jobs=-1,
|
| 402 |
),
|
| 403 |
+
],
|
| 404 |
+
"Gradient Boosting": [
|
|
|
|
| 405 |
GradientBoostingClassifier(
|
| 406 |
n_estimators=200,
|
| 407 |
+
max_depth=3,
|
| 408 |
+
learning_rate=0.05,
|
| 409 |
+
subsample=0.8,
|
| 410 |
+
min_samples_leaf=10,
|
| 411 |
+
min_samples_split=20,
|
| 412 |
+
random_state=42,
|
| 413 |
+
),
|
| 414 |
+
GradientBoostingClassifier(
|
| 415 |
+
n_estimators=260,
|
| 416 |
+
max_depth=2,
|
| 417 |
+
learning_rate=0.04,
|
| 418 |
+
subsample=0.85,
|
| 419 |
+
min_samples_leaf=12,
|
| 420 |
+
min_samples_split=24,
|
| 421 |
+
random_state=42,
|
| 422 |
+
),
|
| 423 |
+
GradientBoostingClassifier(
|
| 424 |
+
n_estimators=180,
|
| 425 |
max_depth=4,
|
| 426 |
+
learning_rate=0.07,
|
| 427 |
subsample=0.75,
|
| 428 |
+
min_samples_leaf=8,
|
| 429 |
+
min_samples_split=16,
|
| 430 |
random_state=42,
|
| 431 |
),
|
| 432 |
+
],
|
| 433 |
+
"SVM (RBF)": [
|
|
|
|
| 434 |
SVC(
|
| 435 |
kernel="rbf",
|
| 436 |
+
C=1.0,
|
| 437 |
+
gamma="scale",
|
| 438 |
+
class_weight="balanced",
|
| 439 |
+
probability=True,
|
| 440 |
+
random_state=42,
|
| 441 |
+
),
|
| 442 |
+
SVC(
|
| 443 |
+
kernel="rbf",
|
| 444 |
+
C=3.0,
|
| 445 |
gamma="scale",
|
| 446 |
class_weight="balanced",
|
| 447 |
probability=True,
|
| 448 |
random_state=42,
|
| 449 |
),
|
| 450 |
+
SVC(
|
| 451 |
+
kernel="rbf",
|
| 452 |
+
C=6.0,
|
| 453 |
+
gamma=0.02,
|
| 454 |
+
class_weight="balanced",
|
| 455 |
+
probability=True,
|
| 456 |
+
random_state=42,
|
| 457 |
+
),
|
| 458 |
+
SVC(
|
| 459 |
+
kernel="rbf",
|
| 460 |
+
C=10.0,
|
| 461 |
+
gamma=0.05,
|
| 462 |
+
class_weight="balanced",
|
| 463 |
+
probability=True,
|
| 464 |
+
random_state=42,
|
| 465 |
+
),
|
| 466 |
+
],
|
| 467 |
+
"MLP Neural Network": [
|
| 468 |
MLPClassifier(
|
| 469 |
+
hidden_layer_sizes=(128, 64),
|
| 470 |
activation="relu",
|
| 471 |
solver="adam",
|
| 472 |
+
alpha=0.0005,
|
| 473 |
learning_rate="adaptive",
|
| 474 |
max_iter=500,
|
| 475 |
early_stopping=True,
|
| 476 |
validation_fraction=0.15,
|
| 477 |
random_state=42,
|
| 478 |
),
|
| 479 |
+
MLPClassifier(
|
| 480 |
+
hidden_layer_sizes=(192, 96, 32),
|
| 481 |
+
activation="relu",
|
| 482 |
+
solver="adam",
|
| 483 |
+
alpha=0.001,
|
| 484 |
+
learning_rate="adaptive",
|
| 485 |
+
max_iter=600,
|
| 486 |
+
early_stopping=True,
|
| 487 |
+
validation_fraction=0.15,
|
| 488 |
+
random_state=42,
|
| 489 |
+
),
|
| 490 |
+
MLPClassifier(
|
| 491 |
+
hidden_layer_sizes=(256, 128),
|
| 492 |
+
activation="relu",
|
| 493 |
+
solver="adam",
|
| 494 |
+
alpha=0.002,
|
| 495 |
+
learning_rate="adaptive",
|
| 496 |
+
max_iter=700,
|
| 497 |
+
early_stopping=True,
|
| 498 |
+
validation_fraction=0.15,
|
| 499 |
+
random_state=42,
|
| 500 |
+
),
|
| 501 |
+
],
|
| 502 |
+
}
|
| 503 |
|
| 504 |
if HAS_XGB:
|
| 505 |
+
families["XGBoost"] = [
|
|
|
|
| 506 |
xgb.XGBClassifier(
|
| 507 |
+
n_estimators=300,
|
| 508 |
max_depth=4,
|
| 509 |
+
learning_rate=0.05,
|
| 510 |
+
subsample=0.8,
|
| 511 |
+
colsample_bytree=0.8,
|
| 512 |
+
min_child_weight=4,
|
| 513 |
+
reg_alpha=0.2,
|
| 514 |
+
reg_lambda=1.2,
|
| 515 |
+
gamma=0.1,
|
| 516 |
+
eval_metric="logloss",
|
| 517 |
+
tree_method="hist",
|
| 518 |
+
random_state=42,
|
| 519 |
+
n_jobs=-1,
|
| 520 |
+
verbosity=0,
|
| 521 |
+
),
|
| 522 |
+
xgb.XGBClassifier(
|
| 523 |
+
n_estimators=500,
|
| 524 |
+
max_depth=3,
|
| 525 |
+
learning_rate=0.03,
|
| 526 |
+
subsample=0.9,
|
| 527 |
+
colsample_bytree=0.8,
|
| 528 |
+
min_child_weight=2,
|
| 529 |
+
reg_alpha=0.1,
|
| 530 |
+
reg_lambda=1.0,
|
| 531 |
+
gamma=0.0,
|
| 532 |
+
eval_metric="logloss",
|
| 533 |
+
tree_method="hist",
|
| 534 |
+
random_state=42,
|
| 535 |
+
n_jobs=-1,
|
| 536 |
+
verbosity=0,
|
| 537 |
+
),
|
| 538 |
+
xgb.XGBClassifier(
|
| 539 |
+
n_estimators=240,
|
| 540 |
+
max_depth=5,
|
| 541 |
+
learning_rate=0.06,
|
| 542 |
subsample=0.75,
|
| 543 |
colsample_bytree=0.75,
|
| 544 |
+
min_child_weight=6,
|
| 545 |
+
reg_alpha=0.4,
|
| 546 |
reg_lambda=1.5,
|
| 547 |
gamma=0.2,
|
|
|
|
| 548 |
eval_metric="logloss",
|
| 549 |
+
tree_method="hist",
|
| 550 |
random_state=42,
|
| 551 |
+
n_jobs=-1,
|
| 552 |
verbosity=0,
|
| 553 |
),
|
| 554 |
+
]
|
| 555 |
|
| 556 |
if HAS_LGBM:
|
| 557 |
+
families["LightGBM"] = [
|
|
|
|
| 558 |
lgb.LGBMClassifier(
|
| 559 |
+
n_estimators=300,
|
| 560 |
+
max_depth=-1,
|
| 561 |
+
learning_rate=0.05,
|
| 562 |
+
num_leaves=31,
|
| 563 |
+
subsample=0.8,
|
| 564 |
+
colsample_bytree=0.8,
|
| 565 |
+
min_child_samples=20,
|
| 566 |
+
reg_alpha=0.1,
|
| 567 |
+
reg_lambda=1.0,
|
| 568 |
+
class_weight="balanced",
|
| 569 |
+
random_state=42,
|
| 570 |
+
verbose=-1,
|
| 571 |
+
),
|
| 572 |
+
lgb.LGBMClassifier(
|
| 573 |
+
n_estimators=500,
|
| 574 |
+
max_depth=8,
|
| 575 |
+
learning_rate=0.03,
|
| 576 |
+
num_leaves=24,
|
| 577 |
+
subsample=0.9,
|
| 578 |
+
colsample_bytree=0.8,
|
| 579 |
+
min_child_samples=30,
|
| 580 |
+
reg_alpha=0.2,
|
| 581 |
+
reg_lambda=1.2,
|
| 582 |
+
class_weight="balanced",
|
| 583 |
+
random_state=42,
|
| 584 |
+
verbose=-1,
|
| 585 |
+
),
|
| 586 |
+
lgb.LGBMClassifier(
|
| 587 |
+
n_estimators=220,
|
| 588 |
+
max_depth=6,
|
| 589 |
+
learning_rate=0.07,
|
| 590 |
+
num_leaves=18,
|
| 591 |
subsample=0.75,
|
| 592 |
colsample_bytree=0.75,
|
| 593 |
+
min_child_samples=24,
|
| 594 |
reg_alpha=0.3,
|
| 595 |
reg_lambda=1.5,
|
| 596 |
class_weight="balanced",
|
| 597 |
random_state=42,
|
| 598 |
verbose=-1,
|
| 599 |
),
|
| 600 |
+
]
|
| 601 |
+
|
| 602 |
+
return families
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
def _build_eval_pipeline(model: Any) -> Pipeline:
|
| 606 |
+
return Pipeline(
|
| 607 |
+
[
|
| 608 |
+
("scaler", StandardScaler()),
|
| 609 |
+
("model", clone(model)),
|
| 610 |
+
]
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
def _safe_model_name(name: str) -> str:
|
| 615 |
+
return (
|
| 616 |
+
name.lower()
|
| 617 |
+
.replace(" ", "_")
|
| 618 |
+
.replace("(", "")
|
| 619 |
+
.replace(")", "")
|
| 620 |
+
.replace("/", "_")
|
| 621 |
+
)
|
| 622 |
+
|
| 623 |
|
| 624 |
+
def _summarize_selected_params(name: str, model: Any) -> dict[str, Any]:
|
| 625 |
+
tuned_keys = _TUNED_PARAM_KEYS.get(name, ())
|
| 626 |
+
params = model.get_params()
|
| 627 |
+
return {key: params[key] for key in tuned_keys if key in params}
|
| 628 |
|
| 629 |
|
| 630 |
def _extract_importance(
|
| 631 |
model: Any,
|
| 632 |
feature_cols: list[str],
|
| 633 |
) -> list[tuple[str, float]]:
|
|
|
|
| 634 |
importances = None
|
| 635 |
|
| 636 |
if hasattr(model, "feature_importances_"):
|
|
|
|
| 641 |
if importances is None:
|
| 642 |
return []
|
| 643 |
|
|
|
|
| 644 |
total = np.sum(importances)
|
| 645 |
if total > 0:
|
| 646 |
importances = importances / total
|
| 647 |
|
| 648 |
return sorted(
|
| 649 |
zip(feature_cols, importances.tolist()),
|
| 650 |
+
key=lambda item: item[1],
|
| 651 |
reverse=True,
|
| 652 |
)
|
| 653 |
|
local_demo.py
CHANGED
|
@@ -1,574 +1,972 @@
|
|
| 1 |
"""
|
| 2 |
-
AURIS Local Demo
|
| 3 |
-
Tüm eğitilmiş modelleri test edebileceğin local Gradio arayüzü.
|
| 4 |
|
| 5 |
-
|
| 6 |
python local_demo.py
|
| 7 |
"""
|
| 8 |
|
| 9 |
-
import
|
|
|
|
|
|
|
|
|
|
| 10 |
import json
|
| 11 |
import pickle
|
|
|
|
| 12 |
import time
|
|
|
|
|
|
|
|
|
|
| 13 |
from pathlib import Path
|
|
|
|
| 14 |
|
| 15 |
import gradio as gr
|
| 16 |
import numpy as np
|
| 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 |
-
chroma_std = float(np.mean(np.std(chroma, axis=1)))
|
| 100 |
-
chroma_entropy = float(-np.sum(
|
| 101 |
-
np.mean(chroma, axis=1) * np.log2(np.mean(chroma, axis=1) + 1e-10)
|
| 102 |
-
))
|
| 103 |
-
chroma_diff = np.diff(chroma, axis=1)
|
| 104 |
-
chroma_transition_rate = float(np.mean(np.abs(chroma_diff)))
|
| 105 |
-
|
| 106 |
-
# Tonnetz
|
| 107 |
-
tonnetz = librosa.feature.tonnetz(y=y, sr=sr)
|
| 108 |
-
tonnetz_std = float(np.mean(np.std(tonnetz, axis=1)))
|
| 109 |
-
|
| 110 |
-
# Harmonic ratio
|
| 111 |
-
y_harm, y_perc = librosa.effects.hpss(y)
|
| 112 |
-
harm_energy = float(np.sum(y_harm ** 2))
|
| 113 |
-
perc_energy = float(np.sum(y_perc ** 2))
|
| 114 |
-
total_energy = harm_energy + perc_energy + 1e-10
|
| 115 |
-
harmonic_ratio = harm_energy / total_energy
|
| 116 |
-
|
| 117 |
-
# Mel
|
| 118 |
-
mel = librosa.feature.melspectrogram(y=y, sr=sr, hop_length=512)
|
| 119 |
-
mel_flatness = float(np.mean(librosa.feature.spectral_flatness(S=mel)))
|
| 120 |
-
|
| 121 |
-
# Onset
|
| 122 |
-
onset_env = librosa.onset.onset_strength(y=y, sr=sr, hop_length=512)
|
| 123 |
-
|
| 124 |
-
# Pitch
|
| 125 |
-
pitches, magnitudes = librosa.piptrack(y=y, sr=sr, hop_length=512)
|
| 126 |
-
pitch_vals = []
|
| 127 |
-
for t in range(pitches.shape[1]):
|
| 128 |
-
idx = magnitudes[:, t].argmax()
|
| 129 |
-
p = pitches[idx, t]
|
| 130 |
-
if p > 50:
|
| 131 |
-
pitch_vals.append(p)
|
| 132 |
-
pitch_mean_hz = float(np.mean(pitch_vals)) if pitch_vals else 0.0
|
| 133 |
-
if len(pitch_vals) > 1 and pitch_mean_hz > 0:
|
| 134 |
-
cents = 1200 * np.log2(np.array(pitch_vals) / pitch_mean_hz + 1e-10)
|
| 135 |
-
pitch_std_cents = float(np.std(cents))
|
| 136 |
-
else:
|
| 137 |
-
pitch_std_cents = 0.0
|
| 138 |
-
|
| 139 |
-
def _sigmoid(x, center=0.5, steepness=6.0):
|
| 140 |
-
return 1.0 / (1.0 + np.exp(-steepness * (x - center)))
|
| 141 |
-
|
| 142 |
-
spectral_regularity = float(_sigmoid(1.0 - float(np.std(flat)), 0.5, 4))
|
| 143 |
-
temporal_patterns = float(_sigmoid(1.0 - tempo_cv, 0.6, 5) if tempo_cv > 0 else 0.5)
|
| 144 |
-
harmonic_structure = float(_sigmoid(harmonic_ratio, 0.5, 4))
|
| 145 |
-
|
| 146 |
-
feats = {
|
| 147 |
-
"rms_energy": rms_mean,
|
| 148 |
-
"rms_std": rms_std,
|
| 149 |
-
"spectral_centroid_mean": float(np.mean(cent)),
|
| 150 |
-
"spectral_centroid_std": float(np.std(cent)),
|
| 151 |
-
"spectral_flatness_mean": float(np.mean(flat)),
|
| 152 |
-
"spectral_flatness_std": float(np.std(flat)),
|
| 153 |
-
"spectral_bandwidth_mean": float(np.mean(bw)),
|
| 154 |
-
"spectral_bandwidth_std": float(np.std(bw)),
|
| 155 |
-
"spectral_rolloff_mean": float(np.mean(rolloff)),
|
| 156 |
-
"spectral_rolloff_std": float(np.std(rolloff)),
|
| 157 |
-
"spectral_contrast_mean": float(np.mean(contrast)),
|
| 158 |
-
"spectral_contrast_std": float(np.std(contrast)),
|
| 159 |
-
"mfcc_variance": float(np.mean(np.var(mfcc, axis=1))),
|
| 160 |
-
"mfcc_delta_var": float(np.mean(np.var(mfcc_delta, axis=1))),
|
| 161 |
-
"mfcc_delta2_var": float(np.mean(np.var(mfcc_delta2, axis=1))),
|
| 162 |
-
"zero_crossing_rate": float(np.mean(zcr)),
|
| 163 |
-
"zero_crossing_std": float(np.std(zcr)),
|
| 164 |
-
"tempo_bpm": tempo_val,
|
| 165 |
-
"tempo_stability": tempo_stability,
|
| 166 |
-
"tempo_cv": tempo_cv,
|
| 167 |
-
"beat_count": float(len(beats)),
|
| 168 |
-
"rms_dynamic_range": rms_dynamic_range,
|
| 169 |
-
"chroma_std": chroma_std,
|
| 170 |
-
"chroma_entropy": chroma_entropy,
|
| 171 |
-
"chroma_transition_rate": chroma_transition_rate,
|
| 172 |
-
"tonnetz_std": tonnetz_std,
|
| 173 |
-
"harmonic_ratio": harmonic_ratio,
|
| 174 |
-
"mel_flatness": mel_flatness,
|
| 175 |
-
"onset_strength_mean": float(np.mean(onset_env)),
|
| 176 |
-
"onset_strength_std": float(np.std(onset_env)),
|
| 177 |
-
"pitch_mean_hz": pitch_mean_hz,
|
| 178 |
-
"pitch_std_cents": pitch_std_cents,
|
| 179 |
-
"spectral_regularity": spectral_regularity,
|
| 180 |
-
"temporal_patterns": temporal_patterns,
|
| 181 |
-
"harmonic_structure": harmonic_structure,
|
| 182 |
-
"vocal_confidence": 0.0,
|
| 183 |
-
"vocal_ai_score": 0.0,
|
| 184 |
-
"vocal_energy_ratio": 0.0,
|
| 185 |
-
"vocal_harmonic_ratio": 0.0,
|
| 186 |
-
"vocal_texture_score": 0.0,
|
| 187 |
-
"has_vocals": 0.0,
|
| 188 |
-
"pitch_stability_score": float(_sigmoid(1.0 - min(pitch_std_cents / 200, 1.0), 0.5, 4)),
|
| 189 |
-
"vibrato_rate_hz": 0.0,
|
| 190 |
-
"vibrato_extent_cents": 0.0,
|
| 191 |
-
"vibrato_regularity_score": 0.0,
|
| 192 |
-
"formant_consistency_score": 0.0,
|
| 193 |
-
"breath_pattern_score": float(_sigmoid(rms_dynamic_range, 0.3, 5)),
|
| 194 |
}
|
| 195 |
-
return feats, duration_sec
|
| 196 |
|
| 197 |
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
import matplotlib.pyplot as plt
|
| 208 |
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
human_prob = float(prob[0])
|
| 231 |
-
|
| 232 |
-
if ai_prob > 0.8:
|
| 233 |
-
verdict = f"AI Uretimi Muzik Tespit Edildi | %{ai_prob*100:.1f} guven"
|
| 234 |
-
elif ai_prob > 0.5:
|
| 235 |
-
verdict = f"Muhtemelen AI Uretimi | %{ai_prob*100:.1f} guven"
|
| 236 |
-
elif ai_prob > 0.3:
|
| 237 |
-
verdict = f"Muhtemelen Insan Yapimi | %{human_prob*100:.1f} guven"
|
| 238 |
-
else:
|
| 239 |
-
verdict = f"Insan Yapimi Muzik | %{human_prob*100:.1f} guven"
|
| 240 |
-
|
| 241 |
-
sr_pct = feats["spectral_regularity"] * 100
|
| 242 |
-
tp_pct = feats["temporal_patterns"] * 100
|
| 243 |
-
hs_pct = feats["harmonic_structure"] * 100
|
| 244 |
-
|
| 245 |
-
# ── Main dashboard plot (16:9) ──
|
| 246 |
-
fig = plt.figure(figsize=(16, 9), facecolor="#1a1207")
|
| 247 |
-
|
| 248 |
-
# ── Left: Gauge + verdict ──
|
| 249 |
-
ax_gauge = fig.add_axes([0.02, 0.45, 0.28, 0.50], projection="polar")
|
| 250 |
-
ax_gauge.set_facecolor("#1a1207")
|
| 251 |
-
theta = np.linspace(np.pi, 0, 100)
|
| 252 |
-
r = np.ones(100)
|
| 253 |
-
ax_gauge.plot(theta, r, color="#3d2817", linewidth=24, alpha=0.4)
|
| 254 |
-
score_end = max(1, int(ai_prob * 100))
|
| 255 |
-
c = "#7fb069" if ai_prob < 0.4 else "#c99347" if ai_prob < 0.7 else "#a64b3c"
|
| 256 |
-
ax_gauge.plot(theta[:score_end], r[:score_end], color=c, linewidth=24)
|
| 257 |
-
needle = np.pi - ai_prob * np.pi
|
| 258 |
-
ax_gauge.plot([needle, needle], [0, 0.82], color="#faf6ed", linewidth=2.5)
|
| 259 |
-
ax_gauge.scatter([needle], [0.82], color="#faf6ed", s=40, zorder=5)
|
| 260 |
-
ax_gauge.set_ylim(0, 1.2)
|
| 261 |
-
ax_gauge.set_yticklabels([])
|
| 262 |
-
ax_gauge.set_xticklabels([])
|
| 263 |
-
ax_gauge.spines["polar"].set_visible(False)
|
| 264 |
-
ax_gauge.grid(False)
|
| 265 |
-
|
| 266 |
-
fig.text(0.16, 0.42, f"%{ai_prob*100:.0f}", ha="center", va="center",
|
| 267 |
-
fontsize=42, fontweight="bold", color="#faf6ed")
|
| 268 |
-
fig.text(0.16, 0.36, "AI Olasiligi", ha="center", va="center",
|
| 269 |
-
fontsize=12, color="#c99347")
|
| 270 |
-
|
| 271 |
-
label = "AI URETIMI" if ai_prob > 0.5 else "INSAN YAPIMI"
|
| 272 |
-
label_color = "#a64b3c" if ai_prob > 0.5 else "#7fb069"
|
| 273 |
-
fig.text(0.16, 0.30, label, ha="center", va="center",
|
| 274 |
-
fontsize=16, fontweight="bold", color=label_color,
|
| 275 |
-
bbox=dict(boxstyle="round,pad=0.4", facecolor="#2a1f10",
|
| 276 |
-
edgecolor=label_color, linewidth=2))
|
| 277 |
-
|
| 278 |
-
# ── Middle: Feature bars ──
|
| 279 |
-
ax_bars = fig.add_axes([0.35, 0.55, 0.28, 0.35])
|
| 280 |
-
ax_bars.set_facecolor("#2a1f10")
|
| 281 |
-
bars = [
|
| 282 |
-
("Spektral Duzenlilik", sr_pct),
|
| 283 |
-
("Zamansal Oruntuler", tp_pct),
|
| 284 |
-
("Harmonik Yapi", hs_pct),
|
| 285 |
-
]
|
| 286 |
-
y_pos = np.arange(len(bars))
|
| 287 |
-
vals = [v for _, v in bars]
|
| 288 |
-
colors_b = ["#c99347" if v > 60 else "#7fb069" if v < 40 else "#e7c77a" for v in vals]
|
| 289 |
-
|
| 290 |
-
ax_bars.barh(y_pos, vals, color=colors_b, edgecolor="#3d2817", height=0.55)
|
| 291 |
-
ax_bars.set_yticks(y_pos)
|
| 292 |
-
ax_bars.set_yticklabels([n for n, _ in bars], color="#faf6ed", fontsize=11)
|
| 293 |
-
ax_bars.set_xlim(0, 100)
|
| 294 |
-
ax_bars.set_xlabel("Skor (%)", color="#c99347", fontsize=10)
|
| 295 |
-
ax_bars.tick_params(colors="#c99347", labelsize=9)
|
| 296 |
-
for spine in ax_bars.spines.values():
|
| 297 |
-
spine.set_color("#3d2817")
|
| 298 |
-
for i, v in enumerate(vals):
|
| 299 |
-
ax_bars.text(v + 1.5, i, f"%{v:.0f}", va="center", color="#faf6ed",
|
| 300 |
-
fontsize=11, fontweight="bold")
|
| 301 |
-
ax_bars.set_title("Ses Ozellik Analizi", color="#c99347", fontsize=13,
|
| 302 |
-
fontweight="bold", pad=8)
|
| 303 |
-
|
| 304 |
-
# ── Right: Top feature importance ──
|
| 305 |
-
ax_imp = fig.add_axes([0.70, 0.35, 0.27, 0.58])
|
| 306 |
-
ax_imp.set_facecolor("#2a1f10")
|
| 307 |
-
top10 = top_features[:10]
|
| 308 |
-
imp_names = [n for n, _ in top10]
|
| 309 |
-
imp_vals = [v * 100 for _, v in top10]
|
| 310 |
-
imp_colors = plt.cm.copper(np.linspace(0.3, 0.85, len(imp_names)))
|
| 311 |
-
|
| 312 |
-
ax_imp.barh(np.arange(len(imp_names)), imp_vals, color=imp_colors,
|
| 313 |
-
edgecolor="#3d2817", height=0.6)
|
| 314 |
-
ax_imp.set_yticks(np.arange(len(imp_names)))
|
| 315 |
-
ax_imp.set_yticklabels(imp_names, color="#faf6ed", fontsize=8)
|
| 316 |
-
ax_imp.invert_yaxis()
|
| 317 |
-
ax_imp.set_xlabel("Onem (%)", color="#c99347", fontsize=9)
|
| 318 |
-
ax_imp.tick_params(colors="#c99347", labelsize=8)
|
| 319 |
-
for spine in ax_imp.spines.values():
|
| 320 |
-
spine.set_color("#3d2817")
|
| 321 |
-
ax_imp.set_title("En Onemli Ozellikler", color="#c99347", fontsize=12,
|
| 322 |
-
fontweight="bold", pad=8)
|
| 323 |
-
|
| 324 |
-
# ── Bottom: Info strip ──
|
| 325 |
-
info_text = (
|
| 326 |
-
f"Model: {best_model_name} | Sure: {duration:.1f}s | "
|
| 327 |
-
f"Islem: {elapsed:.2f}s | Ozellik: {n_features} | "
|
| 328 |
-
f"Tempo: {feats['tempo_bpm']:.0f} BPM | "
|
| 329 |
-
f"RMS: {feats['rms_energy']:.4f}"
|
| 330 |
)
|
| 331 |
-
fig.text(0.50, 0.06, info_text, ha="center", va="center",
|
| 332 |
-
fontsize=10, color="#c99347",
|
| 333 |
-
bbox=dict(boxstyle="round,pad=0.6", facecolor="#2a1f10",
|
| 334 |
-
edgecolor="#3d2817", linewidth=1))
|
| 335 |
-
|
| 336 |
-
# ── Title ──
|
| 337 |
-
fig.text(0.50, 0.97, "AURIS — AI Music Detection System",
|
| 338 |
-
ha="center", va="top", fontsize=20, fontweight="bold",
|
| 339 |
-
color="#c99347")
|
| 340 |
-
fig.text(0.50, 0.93,
|
| 341 |
-
f"{best_model_name} | AUC={training_results.get(best_model_name, {}).get('roc_auc', 0):.4f}",
|
| 342 |
-
ha="center", va="top", fontsize=11, color="#faf6ed", alpha=0.7)
|
| 343 |
-
|
| 344 |
-
# ── Bottom left: Mini model comparison ──
|
| 345 |
-
ax_models = fig.add_axes([0.04, 0.12, 0.58, 0.22])
|
| 346 |
-
ax_models.set_facecolor("#2a1f10")
|
| 347 |
-
model_names = [n for n, _ in all_models]
|
| 348 |
-
model_aucs = [d.get("roc_auc", 0) for _, d in all_models]
|
| 349 |
-
model_types = []
|
| 350 |
-
for _, d in all_models:
|
| 351 |
-
if d.get("type") == "deep_learning":
|
| 352 |
-
model_types.append("#a64b3c")
|
| 353 |
-
else:
|
| 354 |
-
model_types.append("#c99347")
|
| 355 |
-
|
| 356 |
-
x_pos = np.arange(len(model_names))
|
| 357 |
-
ax_models.bar(x_pos, model_aucs, color=model_types, edgecolor="#3d2817",
|
| 358 |
-
width=0.7)
|
| 359 |
-
ax_models.set_xticks(x_pos)
|
| 360 |
-
ax_models.set_xticklabels(model_names, rotation=30, ha="right",
|
| 361 |
-
color="#faf6ed", fontsize=7)
|
| 362 |
-
ax_models.set_ylabel("ROC-AUC", color="#c99347", fontsize=9)
|
| 363 |
-
ax_models.set_ylim(0.80, 0.97)
|
| 364 |
-
ax_models.tick_params(colors="#c99347", labelsize=8)
|
| 365 |
-
for spine in ax_models.spines.values():
|
| 366 |
-
spine.set_color("#3d2817")
|
| 367 |
-
ax_models.set_title("Tum Modeller (sari=ML, kirmizi=DL)", color="#c99347",
|
| 368 |
-
fontsize=10, fontweight="bold", pad=6)
|
| 369 |
-
for i, v in enumerate(model_aucs):
|
| 370 |
-
ax_models.text(i, v + 0.002, f"{v:.3f}", ha="center", va="bottom",
|
| 371 |
-
color="#faf6ed", fontsize=6)
|
| 372 |
-
|
| 373 |
-
dashboard_path = str(Path(__file__).parent / "_dashboard_temp.png")
|
| 374 |
-
plt.savefig(dashboard_path, dpi=120, bbox_inches="tight",
|
| 375 |
-
facecolor="#1a1207", edgecolor="none")
|
| 376 |
-
plt.close()
|
| 377 |
-
|
| 378 |
-
# ── Details markdown ──
|
| 379 |
-
details_md = f"""
|
| 380 |
-
## Detayli Sonuclar
|
| 381 |
-
|
| 382 |
-
| Metrik | Deger |
|
| 383 |
-
|--------|-------|
|
| 384 |
-
| AI Olasiligi | %{ai_prob*100:.2f} |
|
| 385 |
-
| Insan Olasiligi | %{human_prob*100:.2f} |
|
| 386 |
-
| Model | {best_model_name} |
|
| 387 |
-
| Audio Suresi | {duration:.1f}s |
|
| 388 |
-
| Islem Suresi | {elapsed:.2f}s |
|
| 389 |
-
| Tempo | {feats['tempo_bpm']:.1f} BPM |
|
| 390 |
-
| RMS Energy | {feats['rms_energy']:.6f} |
|
| 391 |
-
| Spectral Centroid | {feats['spectral_centroid_mean']:.1f} Hz |
|
| 392 |
-
| Spectral Flatness | {feats['spectral_flatness_mean']:.6f} |
|
| 393 |
-
| Harmonic Ratio | {feats['harmonic_ratio']:.4f} |
|
| 394 |
-
| Zero Crossing Rate | {feats['zero_crossing_rate']:.6f} |
|
| 395 |
-
| Beat Count | {feats['beat_count']:.0f} |
|
| 396 |
-
|
| 397 |
-
## Tum {n_features} Ozellik Degerleri
|
| 398 |
-
|
| 399 |
-
| Ozellik | Deger | Global Onem |
|
| 400 |
-
|---------|-------|-------------|
|
| 401 |
-
"""
|
| 402 |
-
for col in feature_cols:
|
| 403 |
-
val = feats.get(col, 0.0)
|
| 404 |
-
imp_val = importance.get(col, 0.0)
|
| 405 |
-
bar = "█" * int(imp_val * 200)
|
| 406 |
-
details_md += f"| {col} | {val:.6f} | {imp_val:.4f} {bar} |\n"
|
| 407 |
|
| 408 |
-
return verdict, dashboard_path, None, details_md
|
| 409 |
|
|
|
|
| 410 |
|
| 411 |
-
# ── Figures gallery ─────────────────────────────────────────────
|
| 412 |
|
| 413 |
-
def
|
| 414 |
-
if
|
| 415 |
-
return
|
| 416 |
-
return []
|
| 417 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
|
| 419 |
-
# ── Model comparison table ──────────────────────────────────────
|
| 420 |
|
| 421 |
-
|
|
|
|
| 422 |
|
| 423 |
-
> **En iyi model**: {best} | **Veri**: {n_samples} ornek | **Ozellik**: {n_feat} | **CV**: 5-fold stratified
|
| 424 |
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
| 426 |
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
)
|
| 434 |
|
| 435 |
-
DL_MODELS_MD = """
|
| 436 |
-
### Derin Ogrenme (DL) Modelleri
|
| 437 |
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
|
|
|
|
|
|
| 451 |
)
|
| 452 |
-
if data.get("type") == "deep_learning":
|
| 453 |
-
DL_MODELS_MD += row
|
| 454 |
-
else:
|
| 455 |
-
ALL_MODELS_MD += row
|
| 456 |
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
-
|
| 460 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
|
| 477 |
-
# ── Gradio UI ───────────────────────────────────────────────────
|
| 478 |
|
| 479 |
AURIS_CSS = """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
.gradio-container {
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
"""
|
| 504 |
|
| 505 |
-
with gr.Blocks(title="AURIS — AI Music Detection") as demo:
|
| 506 |
|
| 507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
|
| 509 |
with gr.Tabs():
|
| 510 |
with gr.Tab("Analiz"):
|
| 511 |
-
with gr.Row():
|
| 512 |
-
with gr.Column(scale=1):
|
| 513 |
audio_input = gr.Audio(
|
| 514 |
-
label="
|
| 515 |
type="filepath",
|
| 516 |
)
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
|
|
|
| 521 |
)
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
fn=
|
| 539 |
-
inputs=[audio_input],
|
| 540 |
-
outputs=[
|
| 541 |
)
|
| 542 |
|
| 543 |
-
with gr.Tab("
|
| 544 |
-
gr.Markdown(
|
| 545 |
|
| 546 |
-
with gr.Tab("
|
| 547 |
-
gr.Markdown(
|
| 548 |
-
|
|
|
|
|
|
|
| 549 |
if figure_paths:
|
| 550 |
gr.Gallery(
|
| 551 |
value=figure_paths,
|
| 552 |
-
label="
|
| 553 |
columns=3,
|
| 554 |
height="auto",
|
| 555 |
object_fit="contain",
|
| 556 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
|
| 558 |
-
gr.HTML("""
|
| 559 |
-
<div style="text-align:center; padding:12px; opacity:0.6;">
|
| 560 |
-
<p style="color:#c99347; font-size:0.85em;">
|
| 561 |
-
AURIS v1 — Duzce Universitesi Bilgisayar Muhendisligi Bitirme Projesi<br>
|
| 562 |
-
Hasan Arthur Altuntas — 2026
|
| 563 |
-
</p>
|
| 564 |
-
</div>
|
| 565 |
-
""")
|
| 566 |
|
| 567 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
demo.launch(
|
| 569 |
-
server_name=
|
| 570 |
-
server_port=
|
| 571 |
share=False,
|
| 572 |
-
inbrowser=
|
|
|
|
| 573 |
css=AURIS_CSS,
|
| 574 |
)
|
|
|
|
| 1 |
"""
|
| 2 |
+
AURIS Local Demo - AI Music Detection
|
|
|
|
| 3 |
|
| 4 |
+
Calistir:
|
| 5 |
python local_demo.py
|
| 6 |
"""
|
| 7 |
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import csv
|
| 12 |
import json
|
| 13 |
import pickle
|
| 14 |
+
import socket
|
| 15 |
import time
|
| 16 |
+
import warnings
|
| 17 |
+
from collections import Counter
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
from pathlib import Path
|
| 20 |
+
from typing import Any
|
| 21 |
|
| 22 |
import gradio as gr
|
| 23 |
import numpy as np
|
| 24 |
|
| 25 |
+
from app.training.extract_features_batch import extract_sample_features
|
| 26 |
+
|
| 27 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 28 |
+
PROJECT_ROOT = BASE_DIR.parent
|
| 29 |
+
MODELS_DIR = BASE_DIR / "models"
|
| 30 |
+
FIGURES_DIR = PROJECT_ROOT / "docs" / "academic" / "figures"
|
| 31 |
+
DATASET_DIR = PROJECT_ROOT / "DataSet"
|
| 32 |
+
TEST_AUDIO_DIR = BASE_DIR / "test_audio"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass(frozen=True)
|
| 36 |
+
class DemoArtifacts:
|
| 37 |
+
feature_cols: list[str]
|
| 38 |
+
training_results: dict[str, Any]
|
| 39 |
+
scaler: Any
|
| 40 |
+
loaded_models: dict[str, Any]
|
| 41 |
+
best_model_name: str
|
| 42 |
+
best_model_label: str
|
| 43 |
+
model_labels: list[str]
|
| 44 |
+
label_to_name: dict[str, str]
|
| 45 |
+
feature_importance: dict[str, float]
|
| 46 |
+
feature_stats: dict[str, Any]
|
| 47 |
+
dataset_summary: dict[str, Any]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _safe_model_name(name: str) -> str:
|
| 51 |
+
return (
|
| 52 |
+
name.lower()
|
| 53 |
+
.replace(" ", "_")
|
| 54 |
+
.replace("(", "")
|
| 55 |
+
.replace(")", "")
|
| 56 |
+
.replace("/", "_")
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _load_pickle(path: Path) -> Any:
|
| 61 |
+
with open(path, "rb") as f:
|
| 62 |
+
return pickle.load(f)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _load_json(path: Path) -> dict[str, Any]:
|
| 66 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 67 |
+
return json.load(f)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _require_file(path: Path) -> None:
|
| 71 |
+
if not path.exists():
|
| 72 |
+
raise FileNotFoundError(f"Missing required artifact: {path}")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _load_feature_stats() -> dict[str, Any]:
|
| 76 |
+
stats_path = MODELS_DIR / "feature_stats_v1.json"
|
| 77 |
+
if not stats_path.exists():
|
| 78 |
+
return {}
|
| 79 |
+
return _load_json(stats_path)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _load_dataset_summary() -> dict[str, Any]:
|
| 83 |
+
manifest_path = DATASET_DIR / "manifest.csv"
|
| 84 |
+
if not manifest_path.exists():
|
| 85 |
+
return {}
|
| 86 |
+
|
| 87 |
+
label_counts: Counter[str] = Counter()
|
| 88 |
+
generator_counts: Counter[str] = Counter()
|
| 89 |
+
total = 0
|
| 90 |
+
|
| 91 |
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
| 92 |
+
reader = csv.DictReader(f)
|
| 93 |
+
for row in reader:
|
| 94 |
+
total += 1
|
| 95 |
+
label = row.get("label", "").strip() or str(row.get("label_int", ""))
|
| 96 |
+
generator = row.get("generator", "").strip() or "unknown"
|
| 97 |
+
label_counts[label] += 1
|
| 98 |
+
generator_counts[generator] += 1
|
| 99 |
+
|
| 100 |
+
return {
|
| 101 |
+
"manifest_path": str(manifest_path),
|
| 102 |
+
"total": total,
|
| 103 |
+
"ai": label_counts.get("ai", 0) + label_counts.get("1", 0),
|
| 104 |
+
"human": label_counts.get("human", 0) + label_counts.get("0", 0),
|
| 105 |
+
"generators": generator_counts.most_common(8),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
|
|
|
| 107 |
|
| 108 |
|
| 109 |
+
def _find_matching_name(raw_name: str, training_results: dict[str, Any]) -> str:
|
| 110 |
+
for name in training_results:
|
| 111 |
+
if name.startswith("_"):
|
| 112 |
+
continue
|
| 113 |
+
if _safe_model_name(name) == raw_name:
|
| 114 |
+
return name
|
| 115 |
+
return raw_name.replace("_", " ").title()
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _is_model_compatible(model: Any, n_features: int) -> bool:
|
| 119 |
+
expected = getattr(model, "n_features_in_", None)
|
| 120 |
+
return expected in (None, n_features)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _load_artifacts() -> DemoArtifacts:
|
| 124 |
+
scaler_path = MODELS_DIR / "feature_scaler_v1.pkl"
|
| 125 |
+
columns_path = MODELS_DIR / "feature_columns_v1.json"
|
| 126 |
+
results_path = MODELS_DIR / "training_results.json"
|
| 127 |
+
best_model_path = MODELS_DIR / "auris_classifier_v1.pkl"
|
| 128 |
+
|
| 129 |
+
for required in (scaler_path, columns_path, results_path, best_model_path):
|
| 130 |
+
_require_file(required)
|
| 131 |
+
|
| 132 |
+
scaler = _load_pickle(scaler_path)
|
| 133 |
+
feature_cols = _load_json(columns_path)
|
| 134 |
+
training_results = _load_json(results_path)
|
| 135 |
+
feature_importance = training_results.get("_feature_importance", {})
|
| 136 |
+
best_model_name = training_results.get("_best_model", "Gradient Boosting")
|
| 137 |
+
|
| 138 |
+
loaded_models: dict[str, Any] = {}
|
| 139 |
+
for model_path in sorted(MODELS_DIR.glob("model_*.pkl")):
|
| 140 |
+
try:
|
| 141 |
+
model = _load_pickle(model_path)
|
| 142 |
+
except Exception as exc: # noqa: BLE001
|
| 143 |
+
print(f"Skipping model file {model_path.name}: {exc}")
|
| 144 |
+
continue
|
| 145 |
+
|
| 146 |
+
raw_name = model_path.stem.replace("model_", "")
|
| 147 |
+
model_name = _find_matching_name(raw_name, training_results)
|
| 148 |
+
if not _is_model_compatible(model, len(feature_cols)):
|
| 149 |
+
print(
|
| 150 |
+
f"Skipping incompatible model {model_path.name}: "
|
| 151 |
+
f"expected {len(feature_cols)} features"
|
| 152 |
+
)
|
| 153 |
+
continue
|
| 154 |
+
loaded_models[model_name] = model
|
| 155 |
|
| 156 |
+
if best_model_name not in loaded_models:
|
| 157 |
+
best_model = _load_pickle(best_model_path)
|
| 158 |
+
if _is_model_compatible(best_model, len(feature_cols)):
|
| 159 |
+
loaded_models[best_model_name] = best_model
|
| 160 |
|
| 161 |
+
if not loaded_models:
|
| 162 |
+
raise RuntimeError("No compatible models were found in the models directory.")
|
|
|
|
| 163 |
|
| 164 |
+
sorted_names = sorted(
|
| 165 |
+
loaded_models,
|
| 166 |
+
key=lambda name: training_results.get(name, {}).get("roc_auc", 0.0),
|
| 167 |
+
reverse=True,
|
| 168 |
+
)
|
| 169 |
|
| 170 |
+
label_to_name: dict[str, str] = {}
|
| 171 |
+
model_labels: list[str] = []
|
| 172 |
+
for name in sorted_names:
|
| 173 |
+
result = training_results.get(name, {})
|
| 174 |
+
auc = result.get("roc_auc", 0.0)
|
| 175 |
+
acc = result.get("accuracy", 0.0)
|
| 176 |
+
badge = " [EN IYI]" if name == best_model_name else ""
|
| 177 |
+
label = f"{name}{badge} | AUC {auc:.3f} | Acc {acc:.1%}"
|
| 178 |
+
label_to_name[label] = name
|
| 179 |
+
model_labels.append(label)
|
| 180 |
+
|
| 181 |
+
best_model_label = next(
|
| 182 |
+
label for label, name in label_to_name.items() if name == best_model_name
|
| 183 |
+
)
|
| 184 |
|
| 185 |
+
return DemoArtifacts(
|
| 186 |
+
feature_cols=feature_cols,
|
| 187 |
+
training_results=training_results,
|
| 188 |
+
scaler=scaler,
|
| 189 |
+
loaded_models=loaded_models,
|
| 190 |
+
best_model_name=best_model_name,
|
| 191 |
+
best_model_label=best_model_label,
|
| 192 |
+
model_labels=model_labels,
|
| 193 |
+
label_to_name=label_to_name,
|
| 194 |
+
feature_importance=feature_importance,
|
| 195 |
+
feature_stats=_load_feature_stats(),
|
| 196 |
+
dataset_summary=_load_dataset_summary(),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
|
|
|
| 199 |
|
| 200 |
+
ARTIFACTS = _load_artifacts()
|
| 201 |
|
|
|
|
| 202 |
|
| 203 |
+
def _example_audio_paths(limit: int = 6) -> list[list[str]]:
|
| 204 |
+
if not TEST_AUDIO_DIR.exists():
|
| 205 |
+
return []
|
|
|
|
| 206 |
|
| 207 |
+
candidates = sorted(
|
| 208 |
+
path
|
| 209 |
+
for path in TEST_AUDIO_DIR.iterdir()
|
| 210 |
+
if path.is_file() and path.suffix.lower() in {".wav", ".mp3", ".flac"}
|
| 211 |
+
)
|
| 212 |
+
return [[str(path)] for path in candidates[:limit]]
|
| 213 |
|
|
|
|
| 214 |
|
| 215 |
+
def _normalize_score(value: float, cap: float = 1.0) -> float:
|
| 216 |
+
return max(0.0, min(float(value), cap))
|
| 217 |
|
|
|
|
| 218 |
|
| 219 |
+
def _extract_demo_features(audio_path: str) -> tuple[dict[str, float], float]:
|
| 220 |
+
row = extract_sample_features(audio_path)
|
| 221 |
+
if row is None:
|
| 222 |
+
raise RuntimeError("Ozellik cikarimi basarisiz oldu.")
|
| 223 |
|
| 224 |
+
features = {
|
| 225 |
+
column: float(row.get(column, 0.0))
|
| 226 |
+
for column in ARTIFACTS.feature_cols
|
| 227 |
+
}
|
| 228 |
+
duration_sec = float(row.get("duration_sec", 0.0))
|
| 229 |
+
return features, duration_sec
|
|
|
|
| 230 |
|
|
|
|
|
|
|
| 231 |
|
| 232 |
+
def _build_feature_vector(features: dict[str, float]) -> np.ndarray:
|
| 233 |
+
vector = np.array(
|
| 234 |
+
[[features.get(column, 0.0) for column in ARTIFACTS.feature_cols]],
|
| 235 |
+
dtype=np.float32,
|
| 236 |
+
)
|
| 237 |
+
return np.nan_to_num(vector, nan=0.0, posinf=1.0, neginf=-1.0)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _format_verdict(ai_prob: float) -> tuple[str, str, str]:
|
| 241 |
+
if ai_prob >= 0.75:
|
| 242 |
+
return "ai-high", "Yuksek AI ihtimali", "AI kaynakli izler baskin"
|
| 243 |
+
if ai_prob >= 0.55:
|
| 244 |
+
return "ai-mid", "AI olasiligi yuksek", "Model sentetik duzene yakin buldu"
|
| 245 |
+
if ai_prob >= 0.40:
|
| 246 |
+
return "human-mid", "Sinirda sonuc", "Insan ve AI sinyalleri birbirine yakin"
|
| 247 |
+
return "human-high", "Insan yapimiya yakin", "Dogal varyasyon daha guclu"
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def _build_result_html(
|
| 251 |
+
ai_prob: float,
|
| 252 |
+
duration: float,
|
| 253 |
+
elapsed: float,
|
| 254 |
+
selected_model_name: str,
|
| 255 |
+
) -> str:
|
| 256 |
+
verdict_class, verdict_title, verdict_subtitle = _format_verdict(ai_prob)
|
| 257 |
+
confidence_pct = ai_prob * 100
|
| 258 |
+
human_pct = (1.0 - ai_prob) * 100
|
| 259 |
+
|
| 260 |
+
return f"""
|
| 261 |
+
<section class="hero-card {verdict_class}">
|
| 262 |
+
<div class="hero-card__eyebrow">Canli analiz sonucu</div>
|
| 263 |
+
<div class="hero-card__score">%{confidence_pct:.1f}</div>
|
| 264 |
+
<div class="hero-card__title">{verdict_title}</div>
|
| 265 |
+
<div class="hero-card__subtitle">{verdict_subtitle}</div>
|
| 266 |
+
<div class="hero-card__meta">
|
| 267 |
+
<span>Model: {selected_model_name}</span>
|
| 268 |
+
<span>Sure: {duration:.1f}s</span>
|
| 269 |
+
<span>Islem: {elapsed:.2f}s</span>
|
| 270 |
+
<span>Insan olasiligi: %{human_pct:.1f}</span>
|
| 271 |
+
</div>
|
| 272 |
+
</section>
|
| 273 |
+
"""
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def _build_signal_html(features: dict[str, float]) -> str:
|
| 277 |
+
rows = [
|
| 278 |
+
("Spektral duzen", _normalize_score(features.get("spectral_regularity", 0.0))),
|
| 279 |
+
("Zamansal kalip", _normalize_score(features.get("temporal_patterns", 0.0))),
|
| 280 |
+
("Harmonik yapi", _normalize_score(features.get("harmonic_structure", 0.0))),
|
| 281 |
+
("Vokal AI izi", _normalize_score(features.get("vocal_ai_score", 0.0))),
|
| 282 |
+
("Vokal guveni", _normalize_score(features.get("vocal_confidence", 0.0))),
|
| 283 |
+
("Pitch stabilitesi", _normalize_score(features.get("pitch_stability_score", 0.0))),
|
| 284 |
+
]
|
| 285 |
+
|
| 286 |
+
parts = ['<section class="panel-card"><div class="panel-card__title">Sinyal panosu</div>']
|
| 287 |
+
for label, raw_value in rows:
|
| 288 |
+
pct = raw_value * 100
|
| 289 |
+
bar_class = "bar-warm" if pct >= 60 else "bar-cool" if pct <= 35 else "bar-mid"
|
| 290 |
+
parts.append(
|
| 291 |
+
f"""
|
| 292 |
+
<div class="meter-row">
|
| 293 |
+
<div class="meter-row__label">{label}</div>
|
| 294 |
+
<div class="meter-row__track">
|
| 295 |
+
<div class="meter-row__fill {bar_class}" style="width:{pct:.0f}%"></div>
|
| 296 |
+
</div>
|
| 297 |
+
<div class="meter-row__value">%{pct:.0f}</div>
|
| 298 |
+
</div>
|
| 299 |
+
"""
|
| 300 |
+
)
|
| 301 |
+
parts.append("</section>")
|
| 302 |
+
return "".join(parts)
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def _build_model_table_html(
|
| 306 |
+
selected_model_name: str,
|
| 307 |
+
feature_vector: np.ndarray,
|
| 308 |
+
) -> str:
|
| 309 |
+
scaled = ARTIFACTS.scaler.transform(feature_vector)
|
| 310 |
+
scored_rows: list[tuple[str, float]] = []
|
| 311 |
+
|
| 312 |
+
for name, model in ARTIFACTS.loaded_models.items():
|
| 313 |
+
try:
|
| 314 |
+
with warnings.catch_warnings():
|
| 315 |
+
warnings.simplefilter("ignore", category=UserWarning)
|
| 316 |
+
probability = float(model.predict_proba(scaled)[0][1])
|
| 317 |
+
except Exception: # noqa: BLE001
|
| 318 |
+
continue
|
| 319 |
+
scored_rows.append((name, probability))
|
| 320 |
+
|
| 321 |
+
scored_rows.sort(key=lambda item: item[1], reverse=True)
|
| 322 |
+
parts = [
|
| 323 |
+
'<section class="panel-card"><div class="panel-card__title">Model karsilastirmasi</div>',
|
| 324 |
+
"<table class='model-table'><thead><tr><th>Model</th><th>Canli AI %</th><th>CV AUC</th><th>Acc</th></tr></thead><tbody>",
|
| 325 |
+
]
|
| 326 |
+
|
| 327 |
+
for name, probability in scored_rows:
|
| 328 |
+
metrics = ARTIFACTS.training_results.get(name, {})
|
| 329 |
+
row_class = "is-selected" if name == selected_model_name else ""
|
| 330 |
+
best_badge = " <span class='badge'>en iyi</span>" if name == ARTIFACTS.best_model_name else ""
|
| 331 |
+
parts.append(
|
| 332 |
+
f"""
|
| 333 |
+
<tr class="{row_class}">
|
| 334 |
+
<td>{name}{best_badge}</td>
|
| 335 |
+
<td>%{probability * 100:.1f}</td>
|
| 336 |
+
<td>{metrics.get("roc_auc", 0.0):.4f}</td>
|
| 337 |
+
<td>{metrics.get("accuracy", 0.0):.4f}</td>
|
| 338 |
+
</tr>
|
| 339 |
+
"""
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
parts.append("</tbody></table></section>")
|
| 343 |
+
return "".join(parts)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def _build_feature_details_md(features: dict[str, float], duration: float) -> str:
|
| 347 |
+
importance = ARTIFACTS.feature_importance
|
| 348 |
+
|
| 349 |
+
lines = [
|
| 350 |
+
"## Ses ozet",
|
| 351 |
+
"",
|
| 352 |
+
"| Metrik | Deger |",
|
| 353 |
+
"|--------|-------|",
|
| 354 |
+
f"| Sure | {duration:.1f}s |",
|
| 355 |
+
f"| Tempo | {features.get('tempo_bpm', 0.0):.1f} BPM |",
|
| 356 |
+
f"| RMS energy | {features.get('rms_energy', 0.0):.6f} |",
|
| 357 |
+
f"| Harmonic ratio | {features.get('harmonic_ratio', 0.0):.4f} |",
|
| 358 |
+
f"| Spectral centroid | {features.get('spectral_centroid_mean', 0.0):.1f} Hz |",
|
| 359 |
+
f"| Vocal confidence | {features.get('vocal_confidence', 0.0):.3f} |",
|
| 360 |
+
"",
|
| 361 |
+
]
|
| 362 |
|
| 363 |
+
insight_block = _build_feature_insights_md(features)
|
| 364 |
+
if insight_block:
|
| 365 |
+
lines.extend([insight_block, ""])
|
| 366 |
+
|
| 367 |
+
lines.extend(
|
| 368 |
+
[
|
| 369 |
+
"## Tum ozellikler",
|
| 370 |
+
"",
|
| 371 |
+
"| Ozellik | Deger | Global onem |",
|
| 372 |
+
"|---------|-------|-------------|",
|
| 373 |
+
]
|
| 374 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
|
| 376 |
+
for column in ARTIFACTS.feature_cols:
|
| 377 |
+
value = features.get(column, 0.0)
|
| 378 |
+
weight = importance.get(column, 0.0)
|
| 379 |
+
lines.append(f"| {column} | {value:.6f} | {weight:.4f} |")
|
| 380 |
+
|
| 381 |
+
return "\n".join(lines)
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def _build_feature_insights_md(features: dict[str, float]) -> str:
|
| 385 |
+
stats = ARTIFACTS.feature_stats
|
| 386 |
+
if not stats:
|
| 387 |
+
return ""
|
| 388 |
+
|
| 389 |
+
by_class = stats.get("_by_class", {})
|
| 390 |
+
rows: list[tuple[float, str, float, float, float, float]] = []
|
| 391 |
+
for column in ARTIFACTS.feature_cols:
|
| 392 |
+
feature_stats = stats.get(column)
|
| 393 |
+
if not feature_stats:
|
| 394 |
+
continue
|
| 395 |
+
|
| 396 |
+
std = float(feature_stats.get("std", 1.0)) or 1.0
|
| 397 |
+
value = float(features.get(column, 0.0))
|
| 398 |
+
z_score = (value - float(feature_stats.get("mean", 0.0))) / std
|
| 399 |
+
ai_mean = float(by_class.get("ai", {}).get(column, {}).get("mean", 0.0))
|
| 400 |
+
human_mean = float(by_class.get("human", {}).get(column, {}).get("mean", 0.0))
|
| 401 |
+
rows.append((abs(z_score), column, value, z_score, ai_mean, human_mean))
|
| 402 |
+
|
| 403 |
+
if not rows:
|
| 404 |
+
return ""
|
| 405 |
+
|
| 406 |
+
rows.sort(reverse=True)
|
| 407 |
+
lines = [
|
| 408 |
+
"## Dikkat ceken sapmalar",
|
| 409 |
+
"",
|
| 410 |
+
"| Ozellik | Deger | Z-score | AI ort. | Human ort. |",
|
| 411 |
+
"|---------|-------|---------|---------|------------|",
|
| 412 |
+
]
|
| 413 |
+
for _, column, value, z_score, ai_mean, human_mean in rows[:10]:
|
| 414 |
+
lines.append(
|
| 415 |
+
f"| {column} | {value:.6f} | {z_score:+.2f} | {ai_mean:.6f} | {human_mean:.6f} |"
|
| 416 |
+
)
|
| 417 |
+
return "\n".join(lines)
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def analyze_audio(audio_file: Any, selected_model_label: str) -> tuple[str, str, str, str]:
|
| 421 |
+
if not audio_file:
|
| 422 |
+
empty = '<section class="hero-card neutral"><div class="hero-card__title">Ses dosyasi bekleniyor</div><div class="hero-card__subtitle">Analiz icin bir .wav, .mp3 veya .flac yukleyin.</div></section>'
|
| 423 |
+
return empty, "", "", ""
|
| 424 |
+
|
| 425 |
+
audio_path = audio_file[0] if isinstance(audio_file, tuple) else audio_file
|
| 426 |
+
selected_model_name = ARTIFACTS.label_to_name.get(
|
| 427 |
+
selected_model_label,
|
| 428 |
+
ARTIFACTS.best_model_name,
|
| 429 |
+
)
|
| 430 |
|
| 431 |
+
start_time = time.time()
|
| 432 |
+
try:
|
| 433 |
+
features, duration = _extract_demo_features(str(audio_path))
|
| 434 |
+
except Exception as exc: # noqa: BLE001
|
| 435 |
+
error_html = f'<section class="hero-card neutral"><div class="hero-card__title">Analiz basarisiz</div><div class="hero-card__subtitle">{exc}</div></section>'
|
| 436 |
+
return error_html, "", "", ""
|
| 437 |
+
|
| 438 |
+
feature_vector = _build_feature_vector(features)
|
| 439 |
+
scaled = ARTIFACTS.scaler.transform(feature_vector)
|
| 440 |
+
model = ARTIFACTS.loaded_models[selected_model_name]
|
| 441 |
+
with warnings.catch_warnings():
|
| 442 |
+
warnings.simplefilter("ignore", category=UserWarning)
|
| 443 |
+
ai_prob = float(model.predict_proba(scaled)[0][1])
|
| 444 |
+
elapsed = time.time() - start_time
|
| 445 |
+
|
| 446 |
+
result_html = _build_result_html(ai_prob, duration, elapsed, selected_model_name)
|
| 447 |
+
signal_html = _build_signal_html(features)
|
| 448 |
+
model_table_html = _build_model_table_html(selected_model_name, feature_vector)
|
| 449 |
+
details_md = _build_feature_details_md(features, duration)
|
| 450 |
+
return result_html, signal_html, model_table_html, details_md
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def build_models_md() -> str:
|
| 454 |
+
training_results = ARTIFACTS.training_results
|
| 455 |
+
lines = [
|
| 456 |
+
"## Egitilmis modeller",
|
| 457 |
+
"",
|
| 458 |
+
f"- En iyi model: **{ARTIFACTS.best_model_name}**",
|
| 459 |
+
f"- Ornek sayisi: **{training_results.get('_n_samples', '?')}**",
|
| 460 |
+
f"- Ozellik sayisi: **{training_results.get('_n_features', len(ARTIFACTS.feature_cols))}**",
|
| 461 |
+
f"- CV kat sayisi: **{training_results.get('_n_folds', '?')}**",
|
| 462 |
+
"",
|
| 463 |
+
"| Model | CV AUC | Holdout AUC | Acc | F1 |",
|
| 464 |
+
"|------|--------|-------------|-----|----|",
|
| 465 |
+
]
|
| 466 |
|
| 467 |
+
model_names = [
|
| 468 |
+
name
|
| 469 |
+
for name in training_results
|
| 470 |
+
if not name.startswith("_") and isinstance(training_results[name], dict)
|
| 471 |
+
]
|
| 472 |
+
model_names.sort(key=lambda name: training_results[name].get("roc_auc", 0.0), reverse=True)
|
| 473 |
+
|
| 474 |
+
for name in model_names:
|
| 475 |
+
result = training_results[name]
|
| 476 |
+
display = f"**{name}**" if name == ARTIFACTS.best_model_name else name
|
| 477 |
+
lines.append(
|
| 478 |
+
f"| {display} | {result.get('roc_auc', 0.0):.4f} | "
|
| 479 |
+
f"{result.get('validation_auc', 0.0):.4f} | "
|
| 480 |
+
f"{result.get('accuracy', 0.0):.4f} | {result.get('f1', 0.0):.4f} |"
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
lines.extend(["", "## Secilen parametreler", ""])
|
| 484 |
+
for name in model_names:
|
| 485 |
+
params = training_results[name].get("selected_params", {})
|
| 486 |
+
lines.append(f"- **{name}**: `{json.dumps(params, ensure_ascii=True)}`")
|
| 487 |
+
|
| 488 |
+
importance_items = sorted(
|
| 489 |
+
ARTIFACTS.feature_importance.items(),
|
| 490 |
+
key=lambda item: item[1],
|
| 491 |
+
reverse=True,
|
| 492 |
+
)[:15]
|
| 493 |
+
if importance_items:
|
| 494 |
+
lines.extend(["", "## Ilk 15 ozellik onemi", "", "| Ozellik | Onem |", "|---------|------|"])
|
| 495 |
+
for feature_name, score in importance_items:
|
| 496 |
+
lines.append(f"| {feature_name} | {score:.4f} |")
|
| 497 |
+
|
| 498 |
+
return "\n".join(lines)
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
def build_dataset_md() -> str:
|
| 502 |
+
summary = ARTIFACTS.dataset_summary
|
| 503 |
+
if not summary:
|
| 504 |
+
return "Veri seti ozeti bulunamadi."
|
| 505 |
+
|
| 506 |
+
lines = [
|
| 507 |
+
"## Egitim veri seti",
|
| 508 |
+
"",
|
| 509 |
+
"| Metrik | Deger |",
|
| 510 |
+
"|--------|-------|",
|
| 511 |
+
f"| Manifest | `{summary.get('manifest_path', '-')}` |",
|
| 512 |
+
f"| Toplam ornek | {summary.get('total', 0)} |",
|
| 513 |
+
f"| AI | {summary.get('ai', 0)} |",
|
| 514 |
+
f"| Human | {summary.get('human', 0)} |",
|
| 515 |
+
f"| Ozellik | {len(ARTIFACTS.feature_cols)} |",
|
| 516 |
+
"",
|
| 517 |
+
"## Kaynak dagilimi",
|
| 518 |
+
"",
|
| 519 |
+
"| Kaynak | Adet |",
|
| 520 |
+
"|--------|------|",
|
| 521 |
+
]
|
| 522 |
+
for generator, count in summary.get("generators", []):
|
| 523 |
+
lines.append(f"| {generator} | {count} |")
|
| 524 |
+
return "\n".join(lines)
|
| 525 |
|
|
|
|
| 526 |
|
| 527 |
AURIS_CSS = """
|
| 528 |
+
:root {
|
| 529 |
+
--bg: #120f0b;
|
| 530 |
+
--panel: rgba(31, 24, 17, 0.92);
|
| 531 |
+
--panel-strong: rgba(42, 31, 22, 0.98);
|
| 532 |
+
--line: rgba(215, 182, 122, 0.18);
|
| 533 |
+
--text: #f5ead8;
|
| 534 |
+
--muted: #c8af8a;
|
| 535 |
+
--gold: #dfb56f;
|
| 536 |
+
--gold-soft: #f1d4a2;
|
| 537 |
+
--danger: #d66a55;
|
| 538 |
+
--danger-soft: #5d2218;
|
| 539 |
+
--safe: #7fbb7c;
|
| 540 |
+
--safe-soft: #1f3b2d;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
body {
|
| 544 |
+
background:
|
| 545 |
+
radial-gradient(circle at top left, rgba(223, 181, 111, 0.12), transparent 28%),
|
| 546 |
+
radial-gradient(circle at bottom right, rgba(88, 43, 23, 0.24), transparent 26%),
|
| 547 |
+
linear-gradient(135deg, #0d0a07 0%, #18120d 45%, #120f0b 100%);
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
.gradio-container {
|
| 551 |
+
max-width: 1360px !important;
|
| 552 |
+
margin: 0 auto !important;
|
| 553 |
+
background: transparent !important;
|
| 554 |
+
color: var(--text) !important;
|
| 555 |
+
font-family: "Segoe UI", sans-serif !important;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.app-shell {
|
| 559 |
+
padding: 24px 0 10px;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
.app-hero {
|
| 563 |
+
display: grid;
|
| 564 |
+
grid-template-columns: 1.4fr 1fr;
|
| 565 |
+
gap: 18px;
|
| 566 |
+
align-items: stretch;
|
| 567 |
+
margin-bottom: 18px;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.app-brand,
|
| 571 |
+
.app-meta {
|
| 572 |
+
background: linear-gradient(160deg, rgba(35, 26, 18, 0.95), rgba(19, 14, 10, 0.96));
|
| 573 |
+
border: 1px solid var(--line);
|
| 574 |
+
border-radius: 22px;
|
| 575 |
+
padding: 22px 24px;
|
| 576 |
+
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.28);
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
.app-brand__eyebrow {
|
| 580 |
+
text-transform: uppercase;
|
| 581 |
+
letter-spacing: 0.24em;
|
| 582 |
+
font-size: 0.78rem;
|
| 583 |
+
color: var(--gold);
|
| 584 |
+
margin-bottom: 12px;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.app-brand__title {
|
| 588 |
+
font-size: 3rem;
|
| 589 |
+
font-weight: 800;
|
| 590 |
+
line-height: 0.98;
|
| 591 |
+
margin: 0;
|
| 592 |
+
color: #fff6e6;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.app-brand__subtitle {
|
| 596 |
+
margin: 14px 0 0;
|
| 597 |
+
color: var(--muted);
|
| 598 |
+
line-height: 1.6;
|
| 599 |
+
max-width: 46rem;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.app-meta__grid {
|
| 603 |
+
display: grid;
|
| 604 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 605 |
+
gap: 12px;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.meta-chip {
|
| 609 |
+
background: rgba(255, 255, 255, 0.03);
|
| 610 |
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
| 611 |
+
border-radius: 16px;
|
| 612 |
+
padding: 14px 16px;
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
.meta-chip__label {
|
| 616 |
+
display: block;
|
| 617 |
+
color: var(--muted);
|
| 618 |
+
font-size: 0.78rem;
|
| 619 |
+
margin-bottom: 6px;
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
.meta-chip__value {
|
| 623 |
+
display: block;
|
| 624 |
+
color: #fff2db;
|
| 625 |
+
font-size: 1.1rem;
|
| 626 |
+
font-weight: 700;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.hero-card,
|
| 630 |
+
.panel-card {
|
| 631 |
+
background: linear-gradient(180deg, rgba(34, 26, 19, 0.96), rgba(19, 14, 10, 0.96));
|
| 632 |
+
border: 1px solid var(--line);
|
| 633 |
+
border-radius: 20px;
|
| 634 |
+
padding: 20px;
|
| 635 |
+
box-shadow: 0 22px 60px rgba(0, 0, 0, 0.24);
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
.hero-card__eyebrow,
|
| 639 |
+
.panel-card__title {
|
| 640 |
+
font-size: 0.82rem;
|
| 641 |
+
letter-spacing: 0.12em;
|
| 642 |
+
text-transform: uppercase;
|
| 643 |
+
color: var(--gold);
|
| 644 |
+
margin-bottom: 10px;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
.hero-card__score {
|
| 648 |
+
font-size: clamp(2.8rem, 7vw, 4.8rem);
|
| 649 |
+
line-height: 0.95;
|
| 650 |
+
font-weight: 900;
|
| 651 |
+
color: #fff6e7;
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
.hero-card__title {
|
| 655 |
+
margin-top: 8px;
|
| 656 |
+
font-size: 1.4rem;
|
| 657 |
+
font-weight: 800;
|
| 658 |
+
color: #fff6e7;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
.hero-card__subtitle {
|
| 662 |
+
margin-top: 8px;
|
| 663 |
+
color: var(--muted);
|
| 664 |
+
line-height: 1.6;
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
.hero-card__meta {
|
| 668 |
+
display: flex;
|
| 669 |
+
flex-wrap: wrap;
|
| 670 |
+
gap: 10px;
|
| 671 |
+
margin-top: 16px;
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
.hero-card__meta span {
|
| 675 |
+
padding: 7px 12px;
|
| 676 |
+
border-radius: 999px;
|
| 677 |
+
background: rgba(255, 255, 255, 0.04);
|
| 678 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 679 |
+
color: #f8ead4;
|
| 680 |
+
font-size: 0.88rem;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
.hero-card.ai-high,
|
| 684 |
+
.hero-card.ai-mid {
|
| 685 |
+
background: linear-gradient(150deg, rgba(84, 28, 20, 0.96), rgba(33, 15, 12, 0.97));
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.hero-card.human-high,
|
| 689 |
+
.hero-card.human-mid {
|
| 690 |
+
background: linear-gradient(150deg, rgba(23, 49, 34, 0.96), rgba(12, 23, 18, 0.97));
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
.hero-card.neutral {
|
| 694 |
+
background: linear-gradient(150deg, rgba(36, 29, 22, 0.96), rgba(17, 14, 11, 0.97));
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
.meter-row {
|
| 698 |
+
display: grid;
|
| 699 |
+
grid-template-columns: 170px minmax(0, 1fr) 54px;
|
| 700 |
+
gap: 12px;
|
| 701 |
+
align-items: center;
|
| 702 |
+
margin-top: 12px;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
.meter-row__label {
|
| 706 |
+
color: #f7ecd8;
|
| 707 |
+
font-size: 0.92rem;
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
.meter-row__track {
|
| 711 |
+
position: relative;
|
| 712 |
+
height: 14px;
|
| 713 |
+
background: rgba(255, 255, 255, 0.06);
|
| 714 |
+
border-radius: 999px;
|
| 715 |
+
overflow: hidden;
|
| 716 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.meter-row__fill {
|
| 720 |
+
height: 100%;
|
| 721 |
+
border-radius: 999px;
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
.bar-warm {
|
| 725 |
+
background: linear-gradient(90deg, var(--gold), var(--danger));
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
.bar-mid {
|
| 729 |
+
background: linear-gradient(90deg, var(--gold), var(--gold-soft));
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
.bar-cool {
|
| 733 |
+
background: linear-gradient(90deg, #76c490, #5ca39b);
|
| 734 |
+
}
|
| 735 |
|
| 736 |
+
.meter-row__value {
|
| 737 |
+
color: var(--gold-soft);
|
| 738 |
+
font-weight: 700;
|
| 739 |
+
text-align: right;
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
.model-table {
|
| 743 |
+
width: 100%;
|
| 744 |
+
border-collapse: collapse;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.model-table th,
|
| 748 |
+
.model-table td {
|
| 749 |
+
padding: 10px 12px;
|
| 750 |
+
text-align: left;
|
| 751 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.model-table th {
|
| 755 |
+
color: var(--gold);
|
| 756 |
+
font-size: 0.82rem;
|
| 757 |
+
text-transform: uppercase;
|
| 758 |
+
letter-spacing: 0.06em;
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
.model-table td {
|
| 762 |
+
color: #f6ead6;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
.model-table tr.is-selected td {
|
| 766 |
+
background: rgba(223, 181, 111, 0.08);
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
.badge {
|
| 770 |
+
display: inline-block;
|
| 771 |
+
margin-left: 8px;
|
| 772 |
+
padding: 3px 8px;
|
| 773 |
+
border-radius: 999px;
|
| 774 |
+
font-size: 0.72rem;
|
| 775 |
+
color: #fff2db;
|
| 776 |
+
background: rgba(223, 181, 111, 0.18);
|
| 777 |
+
border: 1px solid rgba(223, 181, 111, 0.25);
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
.block {
|
| 781 |
+
border: 1px solid var(--line) !important;
|
| 782 |
+
border-radius: 18px !important;
|
| 783 |
+
background: var(--panel) !important;
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
.gr-button-primary {
|
| 787 |
+
background: linear-gradient(135deg, #d4a85c, #b46d3f) !important;
|
| 788 |
+
border: 0 !important;
|
| 789 |
+
color: #20150d !important;
|
| 790 |
+
font-weight: 800 !important;
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
.prose,
|
| 794 |
+
.prose * {
|
| 795 |
+
color: var(--text) !important;
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
.prose table {
|
| 799 |
+
border-collapse: collapse;
|
| 800 |
+
width: 100%;
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
.prose th,
|
| 804 |
+
.prose td {
|
| 805 |
+
padding: 8px 10px;
|
| 806 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
.prose th {
|
| 810 |
+
background: rgba(255, 255, 255, 0.04);
|
| 811 |
+
color: var(--gold) !important;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
footer {
|
| 815 |
+
display: none !important;
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
@media (max-width: 920px) {
|
| 819 |
+
.app-hero {
|
| 820 |
+
grid-template-columns: 1fr;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
.app-meta__grid {
|
| 824 |
+
grid-template-columns: 1fr;
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
.meter-row {
|
| 828 |
+
grid-template-columns: 1fr;
|
| 829 |
+
}
|
| 830 |
+
}
|
| 831 |
"""
|
| 832 |
|
|
|
|
| 833 |
|
| 834 |
+
def _build_header_html() -> str:
|
| 835 |
+
dataset_summary = ARTIFACTS.dataset_summary
|
| 836 |
+
training_results = ARTIFACTS.training_results
|
| 837 |
+
top_generator = dataset_summary.get("generators", [["-", 0]])[0][0] if dataset_summary else "-"
|
| 838 |
+
|
| 839 |
+
return f"""
|
| 840 |
+
<section class="app-shell">
|
| 841 |
+
<div class="app-hero">
|
| 842 |
+
<div class="app-brand">
|
| 843 |
+
<div class="app-brand__eyebrow">AURIS local demo</div>
|
| 844 |
+
<h1 class="app-brand__title">AI Muzik Tespiti<br />Canli Analiz</h1>
|
| 845 |
+
<p class="app-brand__subtitle">
|
| 846 |
+
Demo arayuzu artik egitim artefact'lari ile ayni ozellik semasini kullaniyor.
|
| 847 |
+
Yani yukledigin ses, `DataSet/features.csv` ile egitilen modellerle birebir uyumlu
|
| 848 |
+
sekilde analiz ediliyor.
|
| 849 |
+
</p>
|
| 850 |
+
</div>
|
| 851 |
+
<div class="app-meta">
|
| 852 |
+
<div class="app-meta__grid">
|
| 853 |
+
<div class="meta-chip">
|
| 854 |
+
<span class="meta-chip__label">En iyi model</span>
|
| 855 |
+
<span class="meta-chip__value">{ARTIFACTS.best_model_name}</span>
|
| 856 |
+
</div>
|
| 857 |
+
<div class="meta-chip">
|
| 858 |
+
<span class="meta-chip__label">Model sayisi</span>
|
| 859 |
+
<span class="meta-chip__value">{len(ARTIFACTS.loaded_models)}</span>
|
| 860 |
+
</div>
|
| 861 |
+
<div class="meta-chip">
|
| 862 |
+
<span class="meta-chip__label">Veri seti</span>
|
| 863 |
+
<span class="meta-chip__value">{training_results.get('_n_samples', '?')} ornek</span>
|
| 864 |
+
</div>
|
| 865 |
+
<div class="meta-chip">
|
| 866 |
+
<span class="meta-chip__label">Baskin kaynak</span>
|
| 867 |
+
<span class="meta-chip__value">{top_generator}</span>
|
| 868 |
+
</div>
|
| 869 |
+
</div>
|
| 870 |
+
</div>
|
| 871 |
+
</div>
|
| 872 |
+
</section>
|
| 873 |
+
"""
|
| 874 |
+
|
| 875 |
+
|
| 876 |
+
with gr.Blocks(title="AURIS Local Demo") as demo:
|
| 877 |
+
gr.HTML(_build_header_html())
|
| 878 |
|
| 879 |
with gr.Tabs():
|
| 880 |
with gr.Tab("Analiz"):
|
| 881 |
+
with gr.Row(equal_height=False):
|
| 882 |
+
with gr.Column(scale=1, min_width=320):
|
| 883 |
audio_input = gr.Audio(
|
| 884 |
+
label="Ses dosyasi yukle",
|
| 885 |
type="filepath",
|
| 886 |
)
|
| 887 |
+
model_dropdown = gr.Dropdown(
|
| 888 |
+
choices=ARTIFACTS.model_labels,
|
| 889 |
+
value=ARTIFACTS.best_model_label,
|
| 890 |
+
label="Calistirilacak model",
|
| 891 |
+
interactive=True,
|
| 892 |
)
|
| 893 |
+
analyze_button = gr.Button("Analizi baslat", variant="primary", size="lg")
|
| 894 |
+
if _example_audio_paths():
|
| 895 |
+
gr.Examples(
|
| 896 |
+
examples=_example_audio_paths(),
|
| 897 |
+
inputs=[audio_input],
|
| 898 |
+
label="Hazir ornekler",
|
| 899 |
+
)
|
| 900 |
+
|
| 901 |
+
with gr.Column(scale=2, min_width=520):
|
| 902 |
+
result_html = gr.HTML()
|
| 903 |
+
with gr.Row(equal_height=False):
|
| 904 |
+
signal_html = gr.HTML()
|
| 905 |
+
model_table_html = gr.HTML()
|
| 906 |
+
details_md = gr.Markdown()
|
| 907 |
+
|
| 908 |
+
analyze_button.click(
|
| 909 |
+
fn=analyze_audio,
|
| 910 |
+
inputs=[audio_input, model_dropdown],
|
| 911 |
+
outputs=[result_html, signal_html, model_table_html, details_md],
|
| 912 |
)
|
| 913 |
|
| 914 |
+
with gr.Tab("Modeller"):
|
| 915 |
+
gr.Markdown(build_models_md())
|
| 916 |
|
| 917 |
+
with gr.Tab("Veri Seti"):
|
| 918 |
+
gr.Markdown(build_dataset_md())
|
| 919 |
+
|
| 920 |
+
with gr.Tab("Gorseller"):
|
| 921 |
+
figure_paths = sorted(str(path) for path in FIGURES_DIR.glob("*.png")) if FIGURES_DIR.exists() else []
|
| 922 |
if figure_paths:
|
| 923 |
gr.Gallery(
|
| 924 |
value=figure_paths,
|
| 925 |
+
label="Akademik ciktılar",
|
| 926 |
columns=3,
|
| 927 |
height="auto",
|
| 928 |
object_fit="contain",
|
| 929 |
)
|
| 930 |
+
else:
|
| 931 |
+
gr.Markdown("Gorsel bulunamadi.")
|
| 932 |
+
|
| 933 |
+
|
| 934 |
+
def _pick_available_port(preferred_port: int) -> int:
|
| 935 |
+
for port in range(preferred_port, preferred_port + 25):
|
| 936 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
| 937 |
+
sock.settimeout(0.2)
|
| 938 |
+
if sock.connect_ex(("127.0.0.1", port)) != 0:
|
| 939 |
+
return port
|
| 940 |
+
raise RuntimeError("Bos bir port bulunamadi.")
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
def _parse_args() -> argparse.Namespace:
|
| 944 |
+
parser = argparse.ArgumentParser(description="Run the AURIS local Gradio demo")
|
| 945 |
+
parser.add_argument("--host", default="127.0.0.1", help="Bind address")
|
| 946 |
+
parser.add_argument("--port", type=int, default=7864, help="Preferred port")
|
| 947 |
+
parser.add_argument(
|
| 948 |
+
"--no-browser",
|
| 949 |
+
action="store_true",
|
| 950 |
+
help="Do not open the browser automatically",
|
| 951 |
+
)
|
| 952 |
+
return parser.parse_args()
|
| 953 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
|
| 955 |
if __name__ == "__main__":
|
| 956 |
+
args = _parse_args()
|
| 957 |
+
port = _pick_available_port(args.port)
|
| 958 |
+
local_host = "127.0.0.1" if args.host == "0.0.0.0" else args.host
|
| 959 |
+
|
| 960 |
+
print("AURIS local demo")
|
| 961 |
+
print(f"Host: {args.host}")
|
| 962 |
+
print(f"Port: {port}")
|
| 963 |
+
print(f"Open: http://{local_host}:{port}")
|
| 964 |
+
|
| 965 |
demo.launch(
|
| 966 |
+
server_name=args.host,
|
| 967 |
+
server_port=port,
|
| 968 |
share=False,
|
| 969 |
+
inbrowser=not args.no_browser,
|
| 970 |
+
show_error=True,
|
| 971 |
css=AURIS_CSS,
|
| 972 |
)
|
requirements.txt
CHANGED
|
@@ -38,6 +38,7 @@ scikit-learn>=1.3.2
|
|
| 38 |
aiohttp>=3.9.1
|
| 39 |
httpx>=0.26.0
|
| 40 |
requests>=2.31.0
|
|
|
|
| 41 |
|
| 42 |
# === Gradio Client (FST Layer 3 - external API) ===
|
| 43 |
gradio_client>=1.0.0
|
|
@@ -62,4 +63,4 @@ google-api-python-client>=2.134.0
|
|
| 62 |
google-auth-httplib2>=0.2.0
|
| 63 |
google-auth-oauthlib>=1.2.0
|
| 64 |
isodate>=0.6.1
|
| 65 |
-
youtube-transcript-api>=0.6.2
|
|
|
|
| 38 |
aiohttp>=3.9.1
|
| 39 |
httpx>=0.26.0
|
| 40 |
requests>=2.31.0
|
| 41 |
+
gradio>=6.0.0,<7.0.0
|
| 42 |
|
| 43 |
# === Gradio Client (FST Layer 3 - external API) ===
|
| 44 |
gradio_client>=1.0.0
|
|
|
|
| 63 |
google-auth-httplib2>=0.2.0
|
| 64 |
google-auth-oauthlib>=1.2.0
|
| 65 |
isodate>=0.6.1
|
| 66 |
+
youtube-transcript-api>=0.6.2
|