163 lines
5.7 KiB
Python
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),
|
|
}
|