MIF_E31231708/ml/predict_api.py

163 lines
5.7 KiB
Python

from pathlib import Path
import joblib
import numpy as np
from audio_utils_api import convert_audio_to_wav
from features import (
LABEL_PD,
LABEL_TPD,
analyze_audio,
build_prediction_explanation,
)
BASE_DIR = Path(__file__).resolve().parent
PROJECT_DIR = BASE_DIR.parent
MODEL_DIR = BASE_DIR / "models"
MODEL_PATH = MODEL_DIR / "svm_voice_confidence_model.joblib"
CONFIDENCE_THRESHOLD = 0.60
MODEL_NOT_FOUND_MESSAGE = "Model tidak ditemukan. Pastikan file model berada di folder ml/models/."
LABEL_DESCRIPTION = {
LABEL_PD: "Percaya Diri",
LABEL_TPD: "Tidak Percaya Diri",
}
def find_model_path():
if MODEL_PATH.exists():
return MODEL_PATH
available_models = sorted(MODEL_DIR.glob("*.joblib"))
if available_models:
return available_models[0]
raise FileNotFoundError(MODEL_NOT_FOUND_MESSAGE)
def load_prediction_model(model_path=None):
model_path = Path(model_path) if model_path else find_model_path()
if not model_path.exists():
raise FileNotFoundError(MODEL_NOT_FOUND_MESSAGE)
return joblib.load(model_path)
def get_expected_feature_count(model):
if hasattr(model, "named_steps") and "scaler" in model.named_steps:
return getattr(model.named_steps["scaler"], "n_features_in_", None)
return getattr(model, "n_features_in_", None)
def calculate_indicator_probability(model_probability_pd, indicators):
indicator_pd = (
0.15 * indicators["volume_score"]
+ 0.35 * indicators["intonation_score"]
+ 0.50 * indicators["pause_score"]
)
adjusted_pd = 0.80 * model_probability_pd + 0.20 * indicator_pd
if indicators["pause_score"] < 0.35:
adjusted_pd -= (0.35 - indicators["pause_score"]) * 0.25
if indicators["volume_score"] < 0.18:
adjusted_pd -= (0.18 - indicators["volume_score"]) * 0.10
if indicators["intonation_score"] < 0.45:
adjusted_pd -= (0.45 - indicators["intonation_score"]) * 0.15
return float(np.clip(adjusted_pd, 0.01, 0.99)), float(np.clip(indicator_pd, 0.0, 1.0))
def predict_audio(audio_path, model_path=None):
model = load_prediction_model(model_path)
wav_path = convert_audio_to_wav(audio_path)
try:
try:
analysis = analyze_audio(wav_path, validate_quality=True)
except ValueError as error:
fallback_analysis = analyze_audio(wav_path, validate_quality=False)
return {
"is_valid_audio": False,
"error_message": str(error),
"predicted_label": None,
"description": "Audio tidak valid",
"confidence": 0.0,
"probability_pd": 0.0,
"probability_tpd": 0.0,
"margin": 0.0,
"audio_quality": fallback_analysis["audio_quality"],
"voice_indicators": fallback_analysis["voice_indicators"],
"explanation": str(error),
}
features = analysis["features"]
feature_matrix = np.asarray(features, dtype=np.float32).reshape(1, -1)
expected_feature_count = get_expected_feature_count(model)
if expected_feature_count and feature_matrix.shape[1] != expected_feature_count:
raise ValueError(
"Jumlah fitur audio tidak sesuai dengan model. "
f"Audio menghasilkan {feature_matrix.shape[1]} fitur, "
f"sedangkan model mengharapkan {expected_feature_count} fitur."
)
prediction = model.predict(feature_matrix)
probabilities = model.predict_proba(feature_matrix)
predicted_label = str(prediction[0])
class_probabilities = {
str(label): float(probability)
for label, probability in zip(model.classes_, probabilities[0])
}
model_probability_pd = float(class_probabilities.get(LABEL_PD, 0.0))
probability_pd, indicator_pd_score = calculate_indicator_probability(
model_probability_pd,
analysis["voice_indicators"],
)
probability_tpd = float(1.0 - probability_pd)
predicted_label = LABEL_PD if probability_pd >= probability_tpd else LABEL_TPD
confidence = float(max(probability_pd, probability_tpd))
margin = float(abs(probability_pd - probability_tpd))
explanation = build_prediction_explanation(
predicted_label,
confidence,
analysis["voice_indicators"],
)
return {
"is_valid_audio": True,
"predicted_label": predicted_label,
"label": predicted_label,
"description": LABEL_DESCRIPTION.get(predicted_label, predicted_label),
"confidence": confidence,
"probability_pd": probability_pd,
"probability_tpd": probability_tpd,
"margin": margin,
"probabilities": {
LABEL_PD: probability_pd,
LABEL_TPD: probability_tpd,
},
"audio_quality": analysis["audio_quality"],
"voice_indicators": analysis["voice_indicators"],
"indicator_pd_score": indicator_pd_score,
"model_probability_pd": model_probability_pd,
"model_probability_tpd": float(class_probabilities.get(LABEL_TPD, 0.0)),
"explanation": explanation,
}
finally:
Path(wav_path).unlink(missing_ok=True)
def get_model_info():
model_path = find_model_path()
model = load_prediction_model(model_path)
return {
"model_path": str(model_path.relative_to(PROJECT_DIR)),
"model_type": type(model).__name__,
"classes": [str(label) for label in getattr(model, "classes_", [])],
"expected_features": get_expected_feature_count(model),
}