Spaces:
Sleeping
Sleeping
Upload 15 files
Browse files- __pycache__/extract.cpython-312.pyc +0 -0
- app.py +99 -0
- extract.py +56 -0
- features.csv +0 -0
- models/feature_list.pkl +3 -0
- models/gender_model_lr.pkl +3 -0
- models/gender_model_rf.pkl +3 -0
- models/gender_model_svm.pkl +3 -0
- models/scaler_gender_model_lr.pkl +3 -0
- models/scaler_gender_model_rf.pkl +3 -0
- models/scaler_gender_model_svm.pkl +3 -0
- requirements.txt +9 -0
- static/script.js +134 -0
- static/styles.css +153 -0
- templates/index.html +81 -0
__pycache__/extract.cpython-312.pyc
ADDED
Binary file (4.1 kB). View file
|
|
app.py
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import pickle
|
3 |
+
import joblib
|
4 |
+
import numpy as np
|
5 |
+
from flask_cors import CORS
|
6 |
+
from flask import Flask, request, render_template, jsonify
|
7 |
+
from werkzeug.utils import secure_filename
|
8 |
+
from extract import extract_features # Import feature extractor
|
9 |
+
|
10 |
+
# Initialize Flask app
|
11 |
+
app = Flask(__name__)
|
12 |
+
CORS(app) # Allow all cross-origin requests
|
13 |
+
|
14 |
+
# Set upload folder and allowed file types
|
15 |
+
UPLOAD_FOLDER = "uploads"
|
16 |
+
ALLOWED_EXTENSIONS = {"wav", "mp3", "ogg", "m4a"}
|
17 |
+
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
|
18 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
19 |
+
|
20 |
+
# Load trained model, scaler, and feature list
|
21 |
+
model_path = "models/gender_model_lr.pkl"
|
22 |
+
scaler_path = "models/scaler_gender_model_lr.pkl"
|
23 |
+
feature_list_path = "models/feature_list.pkl"
|
24 |
+
|
25 |
+
model = joblib.load(model_path)
|
26 |
+
scaler = joblib.load(scaler_path)
|
27 |
+
with open(feature_list_path, "rb") as f:
|
28 |
+
feature_list = pickle.load(f)
|
29 |
+
|
30 |
+
print("β
Model, Scaler, and Feature List Loaded Successfully!")
|
31 |
+
|
32 |
+
# Function to check valid file extensions
|
33 |
+
def allowed_file(filename):
|
34 |
+
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
35 |
+
|
36 |
+
# Route to render the HTML interface
|
37 |
+
@app.route("/")
|
38 |
+
def index():
|
39 |
+
return render_template("index.html")
|
40 |
+
|
41 |
+
# Route to handle file upload and prediction
|
42 |
+
@app.route("/predict", methods=["POST"])
|
43 |
+
def predict():
|
44 |
+
if "audio" not in request.files:
|
45 |
+
print("β No file uploaded")
|
46 |
+
return jsonify({"error": "No file uploaded"}), 400
|
47 |
+
|
48 |
+
file = request.files["audio"]
|
49 |
+
print(f"π₯ Received file: {file.filename}, Type: {file.content_type}") # β
Debugging line
|
50 |
+
|
51 |
+
if file.filename == "":
|
52 |
+
return jsonify({"error": "No selected file"}), 400
|
53 |
+
|
54 |
+
if file and allowed_file(file.filename):
|
55 |
+
filename = secure_filename(file.filename)
|
56 |
+
filepath = os.path.join(app.config["UPLOAD_FOLDER"], filename)
|
57 |
+
file.save(filepath)
|
58 |
+
|
59 |
+
print(f"π’ Processing file: {filename}")
|
60 |
+
|
61 |
+
try:
|
62 |
+
# Extract features
|
63 |
+
features = extract_features(filepath)
|
64 |
+
if features is None:
|
65 |
+
return jsonify({"error": "Feature extraction failed"}), 500
|
66 |
+
|
67 |
+
print(f"π’ Extracted {len(features)} features.")
|
68 |
+
|
69 |
+
# Scale features
|
70 |
+
features_scaled = scaler.transform([features])
|
71 |
+
print("π’ Features scaled successfully.")
|
72 |
+
|
73 |
+
# Predict gender
|
74 |
+
prediction = model.predict(features_scaled)[0]
|
75 |
+
confidence = model.predict_proba(features_scaled)[0]
|
76 |
+
print("π’ Prediction completed.")
|
77 |
+
|
78 |
+
# Format response
|
79 |
+
result = {
|
80 |
+
"gender": "Female" if prediction == 1 else "Male",
|
81 |
+
"confidence": float(max(confidence)),
|
82 |
+
"age_group": "Unknown" # Temporary fix to avoid breaking frontend
|
83 |
+
}
|
84 |
+
|
85 |
+
print(f"β
Result: {result}")
|
86 |
+
|
87 |
+
return jsonify(result)
|
88 |
+
|
89 |
+
except Exception as e:
|
90 |
+
print(f"β Error: {e}")
|
91 |
+
return jsonify({"error": str(e)}), 500
|
92 |
+
finally:
|
93 |
+
os.remove(filepath) # Delete temp file
|
94 |
+
|
95 |
+
return jsonify({"error": "Invalid file format"}), 400
|
96 |
+
|
97 |
+
# Run Flask app
|
98 |
+
if __name__ == "__main__":
|
99 |
+
app.run(debug=True, use_reloader=False) # Disable reloader
|
extract.py
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import librosa
|
2 |
+
import numpy as np
|
3 |
+
|
4 |
+
def extract_features(file_path):
|
5 |
+
try:
|
6 |
+
print(f"π’ Processing file: {file_path}")
|
7 |
+
|
8 |
+
# Load audio with fixed sample rate
|
9 |
+
y, sr = librosa.load(file_path, sr=16000)
|
10 |
+
|
11 |
+
# Trim or pad audio to exactly 5 seconds
|
12 |
+
target_length = sr * 5
|
13 |
+
if len(y) > target_length:
|
14 |
+
start_sample = np.random.randint(0, len(y) - target_length)
|
15 |
+
y = y[start_sample:start_sample + target_length]
|
16 |
+
elif len(y) < target_length:
|
17 |
+
y = np.pad(y, (0, target_length - len(y)), mode='constant')
|
18 |
+
|
19 |
+
print("β
Audio loaded and standardized (5s, 16kHz)")
|
20 |
+
|
21 |
+
# Extract features
|
22 |
+
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
|
23 |
+
chroma = librosa.feature.chroma_stft(y=y, sr=sr)
|
24 |
+
spec_contrast = librosa.feature.spectral_contrast(y=y, sr=sr)
|
25 |
+
zcr = librosa.feature.zero_crossing_rate(y)
|
26 |
+
rms = librosa.feature.rms(y=y)
|
27 |
+
centroid = librosa.feature.spectral_centroid(y=y, sr=sr)
|
28 |
+
bandwidth = librosa.feature.spectral_bandwidth(y=y, sr=sr)
|
29 |
+
rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr)
|
30 |
+
hnr = librosa.effects.harmonic(y)
|
31 |
+
pitches, _ = librosa.piptrack(y=y, sr=sr)
|
32 |
+
|
33 |
+
# Aggregate features (mean + std for each feature)
|
34 |
+
features = {
|
35 |
+
"mfcc": np.concatenate([np.mean(mfcc, axis=1), np.std(mfcc, axis=1)]),
|
36 |
+
"chroma": np.concatenate([np.mean(chroma, axis=1), np.std(chroma, axis=1)]),
|
37 |
+
"spectral_contrast": np.concatenate([np.mean(spec_contrast, axis=1), np.std(spec_contrast, axis=1)]),
|
38 |
+
"zcr": [np.mean(zcr), np.std(zcr)],
|
39 |
+
"rms": [np.mean(rms), np.std(rms)],
|
40 |
+
"centroid": [np.mean(centroid), np.std(centroid)],
|
41 |
+
"bandwidth": [np.mean(bandwidth), np.std(bandwidth)],
|
42 |
+
"rolloff": [np.mean(rolloff), np.std(rolloff)],
|
43 |
+
"hnr": [np.mean(hnr), np.std(hnr)],
|
44 |
+
"pitch": [np.mean(pitches), np.std(pitches)]
|
45 |
+
}
|
46 |
+
|
47 |
+
# Flatten all feature arrays into a single list
|
48 |
+
feature_vector = []
|
49 |
+
for value in features.values():
|
50 |
+
feature_vector.extend(value)
|
51 |
+
|
52 |
+
print(f"β
Features successfully extracted and formatted (Total: {len(feature_vector)} features)")
|
53 |
+
return feature_vector
|
54 |
+
except Exception as e:
|
55 |
+
print(f"β Error extracting features from {file_path}: {e}")
|
56 |
+
return None
|
features.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
models/feature_list.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:89684fc58e221ad3db3297bb778f0d6e2e29a3a9cebf2aeadbb8b259dfde5db4
|
3 |
+
size 1050
|
models/gender_model_lr.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:9f41099b986d8c0257fb683b11ffd362db279a856050c1cc5f0a41affbbbe63e
|
3 |
+
size 1300
|
models/gender_model_rf.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:8b176461a30fd2ff9aa512efe5c47c0e68e8c98eced4f11361543dbc01625042
|
3 |
+
size 8615017
|
models/gender_model_svm.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:b63369fa0c52524cfe50643a330f4689eeca01b0073c9d035e1c4ef04c37608e
|
3 |
+
size 1050972
|
models/scaler_gender_model_lr.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:53dddf6e8eb645af405c4329c6cdbec249dff72cd1b747756b8808dad8ded445
|
3 |
+
size 3451
|
models/scaler_gender_model_rf.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:53dddf6e8eb645af405c4329c6cdbec249dff72cd1b747756b8808dad8ded445
|
3 |
+
size 3451
|
models/scaler_gender_model_svm.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:53dddf6e8eb645af405c4329c6cdbec249dff72cd1b747756b8808dad8ded445
|
3 |
+
size 3451
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
flask
|
2 |
+
flask-cors
|
3 |
+
numpy
|
4 |
+
librosa
|
5 |
+
joblib
|
6 |
+
scikit-learn
|
7 |
+
soundfile
|
8 |
+
praat-parselmouth
|
9 |
+
scipy
|
static/script.js
ADDED
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const API_URL = 'http://localhost:5000';
|
2 |
+
|
3 |
+
// Page Navigation
|
4 |
+
function showPage(pageId) {
|
5 |
+
document.querySelectorAll('.page').forEach(page => page.classList.add('hidden'));
|
6 |
+
document.getElementById(pageId).classList.remove('hidden');
|
7 |
+
}
|
8 |
+
|
9 |
+
// Drag & Drop Handling
|
10 |
+
const dropZone = document.getElementById('drop-zone');
|
11 |
+
const fileInput = document.getElementById('file-input');
|
12 |
+
const analyzeBtn = document.getElementById('analyze-btn');
|
13 |
+
const audioPlayback = document.getElementById('audio-playback');
|
14 |
+
let audioFile = null;
|
15 |
+
|
16 |
+
dropZone.addEventListener('dragover', (e) => {
|
17 |
+
e.preventDefault();
|
18 |
+
dropZone.classList.add('drag-over');
|
19 |
+
});
|
20 |
+
|
21 |
+
dropZone.addEventListener('dragleave', () => {
|
22 |
+
dropZone.classList.remove('drag-over');
|
23 |
+
});
|
24 |
+
|
25 |
+
dropZone.addEventListener('drop', (e) => {
|
26 |
+
e.preventDefault();
|
27 |
+
dropZone.classList.remove('drag-over');
|
28 |
+
|
29 |
+
if (e.dataTransfer.files.length > 0) {
|
30 |
+
handleFile(e.dataTransfer.files[0]);
|
31 |
+
}
|
32 |
+
});
|
33 |
+
|
34 |
+
fileInput.addEventListener('change', (e) => {
|
35 |
+
handleFile(e.target.files[0]);
|
36 |
+
});
|
37 |
+
|
38 |
+
function handleFile(file) {
|
39 |
+
if (file && file.type.startsWith('audio/')) {
|
40 |
+
audioFile = file;
|
41 |
+
analyzeBtn.disabled = false;
|
42 |
+
audioPlayback.src = URL.createObjectURL(file);
|
43 |
+
audioPlayback.classList.remove('hidden');
|
44 |
+
} else {
|
45 |
+
showError('Please upload a valid audio file');
|
46 |
+
}
|
47 |
+
}
|
48 |
+
|
49 |
+
// Voice Recording
|
50 |
+
let mediaRecorder;
|
51 |
+
let audioChunks = [];
|
52 |
+
const recordBtn = document.getElementById('record-btn');
|
53 |
+
const recordingStatus = document.getElementById('recording-status');
|
54 |
+
|
55 |
+
recordBtn.addEventListener('click', async () => {
|
56 |
+
try {
|
57 |
+
if (recordBtn.textContent === 'π€ Record Voice') {
|
58 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
59 |
+
mediaRecorder = new MediaRecorder(stream);
|
60 |
+
audioChunks = [];
|
61 |
+
|
62 |
+
mediaRecorder.ondataavailable = (e) => {
|
63 |
+
audioChunks.push(e.data);
|
64 |
+
};
|
65 |
+
|
66 |
+
mediaRecorder.onstop = () => {
|
67 |
+
audioFile = new Blob(audioChunks, { type: 'audio/wav' });
|
68 |
+
analyzeBtn.disabled = false;
|
69 |
+
audioPlayback.src = URL.createObjectURL(audioFile);
|
70 |
+
audioPlayback.classList.remove('hidden');
|
71 |
+
recordingStatus.textContent = 'Recording saved!';
|
72 |
+
};
|
73 |
+
|
74 |
+
mediaRecorder.start();
|
75 |
+
recordBtn.textContent = 'βΉ Stop Recording';
|
76 |
+
recordingStatus.textContent = 'Recording...';
|
77 |
+
} else {
|
78 |
+
mediaRecorder.stop();
|
79 |
+
recordBtn.textContent = 'π€ Record Voice';
|
80 |
+
}
|
81 |
+
} catch (error) {
|
82 |
+
showError('Unable to access microphone');
|
83 |
+
}
|
84 |
+
});
|
85 |
+
|
86 |
+
// Processing & Prediction
|
87 |
+
analyzeBtn.addEventListener('click', async () => {
|
88 |
+
showPage('processing-page');
|
89 |
+
const progressBar = document.getElementById('progress-bar');
|
90 |
+
|
91 |
+
try {
|
92 |
+
const formData = new FormData();
|
93 |
+
formData.append('audio', audioFile, "recorded_audio.wav");
|
94 |
+
|
95 |
+
const response = await fetch(`${API_URL}/predict`, {
|
96 |
+
method: 'POST',
|
97 |
+
body: formData
|
98 |
+
});
|
99 |
+
|
100 |
+
if (!response.ok) throw new Error('Prediction failed');
|
101 |
+
|
102 |
+
const results = await response.json();
|
103 |
+
progressBar.style.width = '100%';
|
104 |
+
displayResults(results);
|
105 |
+
} catch (error) {
|
106 |
+
showError('Analysis failed. Please try again.');
|
107 |
+
}
|
108 |
+
});
|
109 |
+
|
110 |
+
function displayResults(results) {
|
111 |
+
document.getElementById('age-result').textContent = results.age_group;
|
112 |
+
document.getElementById('gender-result').textContent = results.gender;
|
113 |
+
showPage('results-page');
|
114 |
+
}
|
115 |
+
|
116 |
+
function showError(message) {
|
117 |
+
document.getElementById('error-message').textContent = message;
|
118 |
+
showPage('error-page');
|
119 |
+
}
|
120 |
+
|
121 |
+
function downloadReport() {
|
122 |
+
const results = {
|
123 |
+
age: document.getElementById('age-result').textContent,
|
124 |
+
gender: document.getElementById('gender-result').textContent,
|
125 |
+
timestamp: new Date().toISOString()
|
126 |
+
};
|
127 |
+
|
128 |
+
const blob = new Blob([JSON.stringify(results, null, 2)], { type: 'application/json' });
|
129 |
+
const url = URL.createObjectURL(blob);
|
130 |
+
const a = document.createElement('a');
|
131 |
+
a.href = url;
|
132 |
+
a.download = 'voice-analysis-report.json';
|
133 |
+
a.click();
|
134 |
+
}
|
static/styles.css
ADDED
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* Color Theme */
|
2 |
+
:root {
|
3 |
+
--primary-color: #008080; /* Teal */
|
4 |
+
--secondary-color: #005f5f;
|
5 |
+
--error-color: #f44336;
|
6 |
+
}
|
7 |
+
|
8 |
+
/* Reset & Base Styles */
|
9 |
+
body {
|
10 |
+
font-family: 'Arial', sans-serif;
|
11 |
+
margin: 0;
|
12 |
+
padding: 0;
|
13 |
+
background-color: #f0f8f8;
|
14 |
+
}
|
15 |
+
|
16 |
+
/* Sticky Header */
|
17 |
+
.sticky-header {
|
18 |
+
position: sticky;
|
19 |
+
top: 0;
|
20 |
+
background-color: var(--primary-color);
|
21 |
+
color: white;
|
22 |
+
padding: 10px 20px;
|
23 |
+
display: flex;
|
24 |
+
justify-content: space-between;
|
25 |
+
align-items: center;
|
26 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
27 |
+
z-index: 1000;
|
28 |
+
}
|
29 |
+
|
30 |
+
.sticky-header h1 {
|
31 |
+
margin: 0;
|
32 |
+
font-size: 20px;
|
33 |
+
}
|
34 |
+
|
35 |
+
.sticky-header nav ul {
|
36 |
+
list-style: none;
|
37 |
+
margin: 0;
|
38 |
+
padding: 0;
|
39 |
+
display: flex;
|
40 |
+
}
|
41 |
+
|
42 |
+
.sticky-header nav ul li {
|
43 |
+
margin: 0 10px;
|
44 |
+
}
|
45 |
+
|
46 |
+
.sticky-header nav ul li a {
|
47 |
+
color: white;
|
48 |
+
text-decoration: none;
|
49 |
+
font-weight: bold;
|
50 |
+
}
|
51 |
+
|
52 |
+
.sticky-header nav ul li a:hover {
|
53 |
+
text-decoration: underline;
|
54 |
+
}
|
55 |
+
|
56 |
+
/* Page Layout */
|
57 |
+
.page {
|
58 |
+
max-width: 800px;
|
59 |
+
margin: 50px auto;
|
60 |
+
padding: 20px;
|
61 |
+
background: white;
|
62 |
+
border-radius: 8px;
|
63 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
64 |
+
}
|
65 |
+
|
66 |
+
/* Hidden Pages */
|
67 |
+
.hidden {
|
68 |
+
display: none;
|
69 |
+
}
|
70 |
+
|
71 |
+
/* Upload Section */
|
72 |
+
.upload-container {
|
73 |
+
border: 2px dashed var(--primary-color);
|
74 |
+
padding: 20px;
|
75 |
+
text-align: center;
|
76 |
+
margin: 20px 0;
|
77 |
+
cursor: pointer;
|
78 |
+
transition: all 0.3s ease;
|
79 |
+
}
|
80 |
+
|
81 |
+
.upload-container.drag-over {
|
82 |
+
background: rgba(0, 128, 128, 0.1);
|
83 |
+
}
|
84 |
+
|
85 |
+
/* Progress Bar */
|
86 |
+
.progress-container {
|
87 |
+
width: 100%;
|
88 |
+
height: 20px;
|
89 |
+
background-color: #f0f0f0;
|
90 |
+
border-radius: 10px;
|
91 |
+
overflow: hidden;
|
92 |
+
margin: 20px 0;
|
93 |
+
}
|
94 |
+
|
95 |
+
.progress-bar {
|
96 |
+
width: 0%;
|
97 |
+
height: 100%;
|
98 |
+
background-color: var(--primary-color);
|
99 |
+
transition: width 0.3s ease;
|
100 |
+
}
|
101 |
+
|
102 |
+
/* Results */
|
103 |
+
.results-container {
|
104 |
+
display: flex;
|
105 |
+
justify-content: space-around;
|
106 |
+
margin: 20px 0;
|
107 |
+
}
|
108 |
+
|
109 |
+
.result-card {
|
110 |
+
padding: 20px;
|
111 |
+
border: 1px solid #ddd;
|
112 |
+
border-radius: 8px;
|
113 |
+
text-align: center;
|
114 |
+
background: #e0f7f7;
|
115 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
116 |
+
}
|
117 |
+
|
118 |
+
/* Buttons */
|
119 |
+
button {
|
120 |
+
padding: 10px 20px;
|
121 |
+
border: none;
|
122 |
+
border-radius: 4px;
|
123 |
+
background-color: var(--primary-color);
|
124 |
+
color: white;
|
125 |
+
cursor: pointer;
|
126 |
+
transition: background-color 0.3s;
|
127 |
+
font-size: 16px;
|
128 |
+
}
|
129 |
+
|
130 |
+
button:hover {
|
131 |
+
background-color: var(--secondary-color);
|
132 |
+
}
|
133 |
+
|
134 |
+
button:disabled {
|
135 |
+
background-color: #cccccc;
|
136 |
+
cursor: not-allowed;
|
137 |
+
}
|
138 |
+
|
139 |
+
/* Charts */
|
140 |
+
.charts-container {
|
141 |
+
display: flex;
|
142 |
+
flex-wrap: wrap;
|
143 |
+
justify-content: space-around;
|
144 |
+
gap: 20px;
|
145 |
+
margin: 20px 0;
|
146 |
+
}
|
147 |
+
|
148 |
+
.chart-wrapper {
|
149 |
+
background: white;
|
150 |
+
padding: 15px;
|
151 |
+
border-radius: 8px;
|
152 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
153 |
+
}
|
templates/index.html
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
+
<title>Voice Age & Gender Predictor</title>
|
7 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
|
8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
|
12 |
+
<!-- Header -->
|
13 |
+
<header class="sticky-header">
|
14 |
+
<h1>Voice Age & Gender Predictor</h1>
|
15 |
+
<nav>
|
16 |
+
<ul>
|
17 |
+
<li><a href="#" onclick="showPage('landing-page')">Home</a></li>
|
18 |
+
<li><a href="#" onclick="showPage('upload-page')">Upload</a></li>
|
19 |
+
<li><a href="#" onclick="showPage('results-page')">Results</a></li>
|
20 |
+
</ul>
|
21 |
+
</nav>
|
22 |
+
</header>
|
23 |
+
|
24 |
+
<!-- Landing Page -->
|
25 |
+
<div class="page" id="landing-page">
|
26 |
+
<h2>Discover Your Voice Characteristics</h2>
|
27 |
+
<p>Analyze your voice with AI technology.</p>
|
28 |
+
<button onclick="showPage('upload-page')">Start Analysis</button>
|
29 |
+
</div>
|
30 |
+
|
31 |
+
<!-- Upload Page -->
|
32 |
+
<div class="page hidden" id="upload-page">
|
33 |
+
<h2>Upload or Record Audio</h2>
|
34 |
+
<div class="upload-container" id="drop-zone">
|
35 |
+
<input type="file" id="file-input" accept="audio/*" />
|
36 |
+
<p>Drag & Drop or Click to Upload</p>
|
37 |
+
</div>
|
38 |
+
<div class="record-container">
|
39 |
+
<button id="record-btn">π€ Record Voice</button>
|
40 |
+
<div id="recording-status"></div>
|
41 |
+
<audio id="audio-playback" controls class="hidden"></audio>
|
42 |
+
</div>
|
43 |
+
<button id="analyze-btn" disabled>Analyze Voice</button>
|
44 |
+
</div>
|
45 |
+
|
46 |
+
<!-- Processing Page -->
|
47 |
+
<div class="page hidden" id="processing-page">
|
48 |
+
<h2>Processing Audio...</h2>
|
49 |
+
<div class="progress-container">
|
50 |
+
<div class="progress-bar" id="progress-bar"></div>
|
51 |
+
</div>
|
52 |
+
<p id="processing-status">Analyzing your voice...</p>
|
53 |
+
</div>
|
54 |
+
|
55 |
+
<!-- Results Page -->
|
56 |
+
<div class="page hidden" id="results-page">
|
57 |
+
<h2>Analysis Results</h2>
|
58 |
+
<div class="results-container">
|
59 |
+
<div class="result-card">
|
60 |
+
<h3>Predicted Age</h3>
|
61 |
+
<p id="age-result"></p>
|
62 |
+
</div>
|
63 |
+
<div class="result-card">
|
64 |
+
<h3>Predicted Gender</h3>
|
65 |
+
<p id="gender-result"></p>
|
66 |
+
</div>
|
67 |
+
</div>
|
68 |
+
<div class="charts-container">
|
69 |
+
<canvas id="confidence-chart"></canvas>
|
70 |
+
<canvas id="gender-chart"></canvas>
|
71 |
+
<canvas id="age-chart"></canvas>
|
72 |
+
</div>
|
73 |
+
<div class="action-buttons">
|
74 |
+
<button onclick="showPage('upload-page')">π Try Again</button>
|
75 |
+
<button onclick="downloadReport()">π₯ Download Report</button>
|
76 |
+
</div>
|
77 |
+
</div>
|
78 |
+
|
79 |
+
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
80 |
+
</body>
|
81 |
+
</html>
|