Spaces:
Running
Running
Michael Natanael
commited on
Commit
·
0c67b3e
1
Parent(s):
b6ae325
Add prediction history page (only for authenticated users)
Browse files- .gitattributes +1 -0
- app.py +245 -30
- requirements.txt +2 -0
- static/css/main.css +32 -22
- static/img/BLU SPEED LOGO.png +3 -0
- static/img/Logo Kampus Merdeka.png +3 -0
- static/img/Logo Kemendikbud.png +3 -0
- static/img/Logo PNJ.png +3 -0
- static/img/Logo Vokasi Menguatkan.png +3 -0
- templates/base.html +8 -1
- templates/dashboard.html +14 -0
- templates/history.html +313 -0
- templates/index.html +53 -30
- templates/login.html +121 -0
- templates/navbar.html +68 -0
- templates/register.html +81 -0
- templates/transcribe.html +14 -13
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
app.py
CHANGED
@@ -1,14 +1,37 @@
|
|
1 |
-
from flask import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
from faster_whisper import WhisperModel
|
3 |
import tempfile
|
4 |
import os
|
|
|
5 |
import time
|
6 |
import torch
|
7 |
import numpy as np
|
8 |
import requests
|
9 |
from tqdm import tqdm
|
10 |
from transformers import BertTokenizer
|
11 |
-
from model.multi_class_model import MultiClassModel
|
|
|
|
|
|
|
|
|
12 |
|
13 |
app = Flask(__name__)
|
14 |
|
@@ -16,7 +39,75 @@ app = Flask(__name__)
|
|
16 |
# CHECKPOINT_URL = "https://github.com/michael2002porto/bert_classification_indonesian_song_lyrics/releases/download/finetuned_checkpoints/original_split_synthesized.ckpt"
|
17 |
CHECKPOINT_URL = "https://huggingface.co/nenafem/original_split_synthesized/resolve/main/original_split_synthesized.ckpt?download=true"
|
18 |
CHECKPOINT_PATH = "final_checkpoint/original_split_synthesized.ckpt"
|
19 |
-
AGE_LABELS = ["
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
# === FUNCTION TO DOWNLOAD CKPT IF NEEDED ===
|
22 |
def download_checkpoint_if_needed(url, save_path):
|
@@ -26,7 +117,9 @@ def download_checkpoint_if_needed(url, save_path):
|
|
26 |
response = requests.get(url, stream=True, timeout=10)
|
27 |
if response.status_code == 200:
|
28 |
total = int(response.headers.get("content-length", 0))
|
29 |
-
with open(save_path,
|
|
|
|
|
30 |
for chunk in response.iter_content(1024):
|
31 |
f.write(chunk)
|
32 |
pbar.update(len(chunk))
|
@@ -34,18 +127,17 @@ def download_checkpoint_if_needed(url, save_path):
|
|
34 |
else:
|
35 |
raise Exception(f"❌ Failed to download: {response.status_code}")
|
36 |
|
|
|
37 |
# === INITIAL SETUP: Download & Load Model ===
|
|
|
38 |
download_checkpoint_if_needed(CHECKPOINT_URL, CHECKPOINT_PATH)
|
39 |
|
40 |
# Load tokenizer
|
41 |
-
tokenizer = BertTokenizer.from_pretrained(
|
42 |
|
43 |
# Load model from checkpoint
|
44 |
model = MultiClassModel.load_from_checkpoint(
|
45 |
-
CHECKPOINT_PATH,
|
46 |
-
n_out=4,
|
47 |
-
dropout=0.3,
|
48 |
-
lr=1e-5
|
49 |
)
|
50 |
model.eval()
|
51 |
|
@@ -60,9 +152,7 @@ faster_whisper_model_size = "turbo"
|
|
60 |
# model = WhisperModel(model_size, device="cuda", compute_type="int8_float16")
|
61 |
# or run on CPU with INT8
|
62 |
faster_whisper_model = WhisperModel(
|
63 |
-
faster_whisper_model_size,
|
64 |
-
device="cpu",
|
65 |
-
compute_type="int8"
|
66 |
)
|
67 |
|
68 |
|
@@ -70,10 +160,13 @@ def faster_whisper(temp_audio_path):
|
|
70 |
segments, info = faster_whisper_model.transcribe(
|
71 |
temp_audio_path,
|
72 |
language="id",
|
73 |
-
beam_size=1
|
74 |
)
|
75 |
|
76 |
-
print(
|
|
|
|
|
|
|
77 |
|
78 |
# for segment in segments:
|
79 |
# print("[%.2fs -> %.2fs] %s" % (segment.start, segment.end, segment.text))
|
@@ -86,18 +179,18 @@ def bert_predict(input_lyric):
|
|
86 |
input_lyric,
|
87 |
add_special_tokens=True,
|
88 |
max_length=512,
|
89 |
-
truncation=True,
|
90 |
return_token_type_ids=True,
|
91 |
padding="max_length",
|
92 |
return_attention_mask=True,
|
93 |
-
return_tensors=
|
94 |
)
|
95 |
|
96 |
with torch.no_grad():
|
97 |
prediction = model(
|
98 |
encoding["input_ids"],
|
99 |
encoding["attention_mask"],
|
100 |
-
encoding["token_type_ids"]
|
101 |
)
|
102 |
|
103 |
logits = prediction
|
@@ -105,18 +198,21 @@ def bert_predict(input_lyric):
|
|
105 |
predicted_class = np.argmax(probabilities)
|
106 |
predicted_label = AGE_LABELS[predicted_class]
|
107 |
|
108 |
-
prob_results = [
|
|
|
|
|
109 |
return predicted_label, prob_results
|
110 |
|
111 |
|
112 |
# === ROUTES ===
|
113 |
|
114 |
-
|
|
|
115 |
def index():
|
116 |
-
return render_template(
|
117 |
|
118 |
|
119 |
-
@app.route(
|
120 |
def transcribe():
|
121 |
try:
|
122 |
# Load Whisper with Indonesian language support (large / turbo)
|
@@ -126,7 +222,7 @@ def transcribe():
|
|
126 |
# Start measuring time
|
127 |
start_time = time.time()
|
128 |
|
129 |
-
audio_file = request.files[
|
130 |
if audio_file:
|
131 |
# Save uploaded audio to temp file
|
132 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as temp_audio:
|
@@ -134,7 +230,7 @@ def transcribe():
|
|
134 |
temp_audio_path = temp_audio.name
|
135 |
|
136 |
# Step 1: Transcribe
|
137 |
-
transcribed_text = faster_whisper(temp_audio_path)
|
138 |
os.remove(temp_audio_path)
|
139 |
|
140 |
# Step 2: BERT Prediction
|
@@ -145,12 +241,27 @@ def transcribe():
|
|
145 |
total_time = end_time - start_time
|
146 |
formatted_time = f"{total_time:.2f} seconds"
|
147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
148 |
return render_template(
|
149 |
-
|
150 |
task=transcribed_text,
|
151 |
prediction=predicted_label,
|
152 |
probabilities=prob_results,
|
153 |
-
total_time=formatted_time
|
154 |
)
|
155 |
|
156 |
except Exception as e:
|
@@ -158,10 +269,10 @@ def transcribe():
|
|
158 |
return str(e)
|
159 |
|
160 |
|
161 |
-
@app.route(
|
162 |
def predict_text():
|
163 |
try:
|
164 |
-
user_lyrics = request.form.get(
|
165 |
|
166 |
if not user_lyrics:
|
167 |
return "No lyrics provided.", 400
|
@@ -174,14 +285,29 @@ def predict_text():
|
|
174 |
|
175 |
# End timer
|
176 |
end_time = time.time()
|
177 |
-
total_time =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
178 |
|
179 |
return render_template(
|
180 |
-
|
181 |
task=user_lyrics,
|
182 |
prediction=predicted_label,
|
183 |
probabilities=prob_results,
|
184 |
-
total_time=
|
185 |
)
|
186 |
|
187 |
except Exception as e:
|
@@ -189,5 +315,94 @@ def predict_text():
|
|
189 |
return str(e), 500
|
190 |
|
191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
192 |
if __name__ == "__main__":
|
193 |
app.run(debug=True)
|
|
|
1 |
+
from flask import (
|
2 |
+
Flask,
|
3 |
+
render_template,
|
4 |
+
request,
|
5 |
+
url_for,
|
6 |
+
redirect,
|
7 |
+
flash,
|
8 |
+
get_flashed_messages,
|
9 |
+
)
|
10 |
+
from flask_login import (
|
11 |
+
LoginManager,
|
12 |
+
login_user,
|
13 |
+
logout_user,
|
14 |
+
login_required,
|
15 |
+
current_user,
|
16 |
+
)
|
17 |
+
from flask_sqlalchemy import SQLAlchemy
|
18 |
+
from flask_login import UserMixin
|
19 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
20 |
from faster_whisper import WhisperModel
|
21 |
import tempfile
|
22 |
import os
|
23 |
+
import datetime
|
24 |
import time
|
25 |
import torch
|
26 |
import numpy as np
|
27 |
import requests
|
28 |
from tqdm import tqdm
|
29 |
from transformers import BertTokenizer
|
30 |
+
from model.multi_class_model import MultiClassModel
|
31 |
+
|
32 |
+
# from model.database import db, User
|
33 |
+
from sqlalchemy.exc import OperationalError
|
34 |
+
from sqlalchemy import inspect
|
35 |
|
36 |
app = Flask(__name__)
|
37 |
|
|
|
39 |
# CHECKPOINT_URL = "https://github.com/michael2002porto/bert_classification_indonesian_song_lyrics/releases/download/finetuned_checkpoints/original_split_synthesized.ckpt"
|
40 |
CHECKPOINT_URL = "https://huggingface.co/nenafem/original_split_synthesized/resolve/main/original_split_synthesized.ckpt?download=true"
|
41 |
CHECKPOINT_PATH = "final_checkpoint/original_split_synthesized.ckpt"
|
42 |
+
AGE_LABELS = ["anak", "remaja", "dewasa", "semua usia"]
|
43 |
+
DATABASE_URI = "postgresql://postgres.tcqmmongiztvqkxxebnc:[email protected]:6543/postgres"
|
44 |
+
|
45 |
+
# === CONNECT DATABASE ===
|
46 |
+
app.config["SQLALCHEMY_DATABASE_URI"] = DATABASE_URI
|
47 |
+
app.config["SECRET_KEY"] = "I1Nnj0H72Z3mXWcp"
|
48 |
+
|
49 |
+
# init extensions
|
50 |
+
db = SQLAlchemy(app)
|
51 |
+
login_manager = LoginManager(app)
|
52 |
+
login_manager.login_view = "login"
|
53 |
+
|
54 |
+
try:
|
55 |
+
db.session.execute("SELECT 1")
|
56 |
+
print("✅ Database connected successfully.")
|
57 |
+
except OperationalError as e:
|
58 |
+
print(f"❌ Database connection failed: {e}")
|
59 |
+
|
60 |
+
|
61 |
+
def show_schema_info():
|
62 |
+
inspector = inspect(db.engine)
|
63 |
+
|
64 |
+
# Get current schema (by default it's 'public' unless set explicitly)
|
65 |
+
current_schema = db.engine.url.database
|
66 |
+
all_schemas = inspector.get_schema_names()
|
67 |
+
public_tables = inspector.get_table_names(schema="public")
|
68 |
+
|
69 |
+
return {
|
70 |
+
"current_schema": current_schema,
|
71 |
+
"available_schemas": all_schemas,
|
72 |
+
"public_tables": public_tables,
|
73 |
+
}
|
74 |
+
|
75 |
+
|
76 |
+
class User(db.Model, UserMixin):
|
77 |
+
__tablename__ = "user"
|
78 |
+
|
79 |
+
id = db.Column(db.Integer, primary_key=True)
|
80 |
+
email = db.Column(db.String(255), nullable=False)
|
81 |
+
password = db.Column(db.String(255))
|
82 |
+
created_date = db.Column(db.DateTime, default=datetime.datetime.now())
|
83 |
+
|
84 |
+
history = db.relationship("History", backref="user", lazy=True)
|
85 |
+
|
86 |
+
|
87 |
+
class History(db.Model):
|
88 |
+
__tablename__ = "history"
|
89 |
+
|
90 |
+
id = db.Column(db.Integer, primary_key=True)
|
91 |
+
lyric = db.Column(db.Text, nullable=False)
|
92 |
+
predicted_label = db.Column(db.String(255), nullable=False)
|
93 |
+
|
94 |
+
children_prob = db.Column(db.Float)
|
95 |
+
adolescents_prob = db.Column(db.Float)
|
96 |
+
adults_prob = db.Column(db.Float)
|
97 |
+
all_ages_prob = db.Column(db.Float)
|
98 |
+
|
99 |
+
processing_time = db.Column(db.Float) # store duration in seconds
|
100 |
+
created_date = db.Column(db.DateTime, default=datetime.datetime.now)
|
101 |
+
speech_to_text = db.Column(db.Boolean)
|
102 |
+
|
103 |
+
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
|
104 |
+
|
105 |
+
|
106 |
+
# Load user for Flask-Login
|
107 |
+
@login_manager.user_loader
|
108 |
+
def load_user(user_id):
|
109 |
+
return User.query.get(int(user_id))
|
110 |
+
|
111 |
|
112 |
# === FUNCTION TO DOWNLOAD CKPT IF NEEDED ===
|
113 |
def download_checkpoint_if_needed(url, save_path):
|
|
|
117 |
response = requests.get(url, stream=True, timeout=10)
|
118 |
if response.status_code == 200:
|
119 |
total = int(response.headers.get("content-length", 0))
|
120 |
+
with open(save_path, "wb") as f, tqdm(
|
121 |
+
total=total, unit="B", unit_scale=True, desc="Downloading"
|
122 |
+
) as pbar:
|
123 |
for chunk in response.iter_content(1024):
|
124 |
f.write(chunk)
|
125 |
pbar.update(len(chunk))
|
|
|
127 |
else:
|
128 |
raise Exception(f"❌ Failed to download: {response.status_code}")
|
129 |
|
130 |
+
|
131 |
# === INITIAL SETUP: Download & Load Model ===
|
132 |
+
print(show_schema_info())
|
133 |
download_checkpoint_if_needed(CHECKPOINT_URL, CHECKPOINT_PATH)
|
134 |
|
135 |
# Load tokenizer
|
136 |
+
tokenizer = BertTokenizer.from_pretrained("indolem/indobert-base-uncased")
|
137 |
|
138 |
# Load model from checkpoint
|
139 |
model = MultiClassModel.load_from_checkpoint(
|
140 |
+
CHECKPOINT_PATH, n_out=4, dropout=0.3, lr=1e-5
|
|
|
|
|
|
|
141 |
)
|
142 |
model.eval()
|
143 |
|
|
|
152 |
# model = WhisperModel(model_size, device="cuda", compute_type="int8_float16")
|
153 |
# or run on CPU with INT8
|
154 |
faster_whisper_model = WhisperModel(
|
155 |
+
faster_whisper_model_size, device="cpu", compute_type="int8"
|
|
|
|
|
156 |
)
|
157 |
|
158 |
|
|
|
160 |
segments, info = faster_whisper_model.transcribe(
|
161 |
temp_audio_path,
|
162 |
language="id",
|
163 |
+
beam_size=1, # Lower beam_size, faster but may miss words
|
164 |
)
|
165 |
|
166 |
+
print(
|
167 |
+
"Detected language '%s' with probability %f"
|
168 |
+
% (info.language, info.language_probability)
|
169 |
+
)
|
170 |
|
171 |
# for segment in segments:
|
172 |
# print("[%.2fs -> %.2fs] %s" % (segment.start, segment.end, segment.text))
|
|
|
179 |
input_lyric,
|
180 |
add_special_tokens=True,
|
181 |
max_length=512,
|
182 |
+
truncation=True, # Ensures input ≤512 tokens
|
183 |
return_token_type_ids=True,
|
184 |
padding="max_length",
|
185 |
return_attention_mask=True,
|
186 |
+
return_tensors="pt",
|
187 |
)
|
188 |
|
189 |
with torch.no_grad():
|
190 |
prediction = model(
|
191 |
encoding["input_ids"],
|
192 |
encoding["attention_mask"],
|
193 |
+
encoding["token_type_ids"],
|
194 |
)
|
195 |
|
196 |
logits = prediction
|
|
|
198 |
predicted_class = np.argmax(probabilities)
|
199 |
predicted_label = AGE_LABELS[predicted_class]
|
200 |
|
201 |
+
prob_results = [
|
202 |
+
(label, f"{prob:.4f}") for label, prob in zip(AGE_LABELS, probabilities)
|
203 |
+
]
|
204 |
return predicted_label, prob_results
|
205 |
|
206 |
|
207 |
# === ROUTES ===
|
208 |
|
209 |
+
|
210 |
+
@app.route("/", methods=["GET"])
|
211 |
def index():
|
212 |
+
return render_template("index.html")
|
213 |
|
214 |
|
215 |
+
@app.route("/transcribe", methods=["POST"])
|
216 |
def transcribe():
|
217 |
try:
|
218 |
# Load Whisper with Indonesian language support (large / turbo)
|
|
|
222 |
# Start measuring time
|
223 |
start_time = time.time()
|
224 |
|
225 |
+
audio_file = request.files["file"]
|
226 |
if audio_file:
|
227 |
# Save uploaded audio to temp file
|
228 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as temp_audio:
|
|
|
230 |
temp_audio_path = temp_audio.name
|
231 |
|
232 |
# Step 1: Transcribe
|
233 |
+
transcribed_text = faster_whisper(temp_audio_path).strip()
|
234 |
os.remove(temp_audio_path)
|
235 |
|
236 |
# Step 2: BERT Prediction
|
|
|
241 |
total_time = end_time - start_time
|
242 |
formatted_time = f"{total_time:.2f} seconds"
|
243 |
|
244 |
+
# Insert log prediction
|
245 |
+
new_prediction_history = History(
|
246 |
+
lyric=transcribed_text,
|
247 |
+
predicted_label=predicted_label,
|
248 |
+
children_prob=prob_results[AGE_LABELS.index("anak")][1],
|
249 |
+
adolescents_prob=prob_results[AGE_LABELS.index("remaja")][1],
|
250 |
+
adults_prob=prob_results[AGE_LABELS.index("dewasa")][1],
|
251 |
+
all_ages_prob=prob_results[AGE_LABELS.index("semua usia")][1],
|
252 |
+
processing_time=round(total_time, 2),
|
253 |
+
speech_to_text=True,
|
254 |
+
user_id=current_user.id if current_user.is_authenticated else None,
|
255 |
+
)
|
256 |
+
db.session.add(new_prediction_history)
|
257 |
+
db.session.commit()
|
258 |
+
|
259 |
return render_template(
|
260 |
+
"transcribe.html",
|
261 |
task=transcribed_text,
|
262 |
prediction=predicted_label,
|
263 |
probabilities=prob_results,
|
264 |
+
total_time=formatted_time,
|
265 |
)
|
266 |
|
267 |
except Exception as e:
|
|
|
269 |
return str(e)
|
270 |
|
271 |
|
272 |
+
@app.route("/predict-text", methods=["POST"])
|
273 |
def predict_text():
|
274 |
try:
|
275 |
+
user_lyrics = request.form.get("lyrics", "").strip()
|
276 |
|
277 |
if not user_lyrics:
|
278 |
return "No lyrics provided.", 400
|
|
|
285 |
|
286 |
# End timer
|
287 |
end_time = time.time()
|
288 |
+
total_time = end_time - start_time
|
289 |
+
formatted_time = f"{total_time:.2f} seconds"
|
290 |
+
|
291 |
+
# Insert log prediction
|
292 |
+
new_prediction_history = History(
|
293 |
+
lyric=user_lyrics,
|
294 |
+
predicted_label=predicted_label,
|
295 |
+
children_prob=prob_results[AGE_LABELS.index("anak")][1],
|
296 |
+
adolescents_prob=prob_results[AGE_LABELS.index("remaja")][1],
|
297 |
+
adults_prob=prob_results[AGE_LABELS.index("dewasa")][1],
|
298 |
+
all_ages_prob=prob_results[AGE_LABELS.index("semua usia")][1],
|
299 |
+
processing_time=round(total_time, 2),
|
300 |
+
user_id=current_user.id if current_user.is_authenticated else None,
|
301 |
+
)
|
302 |
+
db.session.add(new_prediction_history)
|
303 |
+
db.session.commit()
|
304 |
|
305 |
return render_template(
|
306 |
+
"transcribe.html",
|
307 |
task=user_lyrics,
|
308 |
prediction=predicted_label,
|
309 |
probabilities=prob_results,
|
310 |
+
total_time=formatted_time,
|
311 |
)
|
312 |
|
313 |
except Exception as e:
|
|
|
315 |
return str(e), 500
|
316 |
|
317 |
|
318 |
+
@app.route("/register", methods=["GET", "POST"])
|
319 |
+
def register():
|
320 |
+
if request.method == "POST":
|
321 |
+
email = request.form.get("email")
|
322 |
+
password = request.form.get("password")
|
323 |
+
confirm_password = request.form.get("confirm-password")
|
324 |
+
|
325 |
+
if User.query.filter_by(email=email).first():
|
326 |
+
return render_template(
|
327 |
+
"register.html",
|
328 |
+
error="Email already taken!",
|
329 |
+
email=email,
|
330 |
+
password=password,
|
331 |
+
confirm_password=confirm_password,
|
332 |
+
)
|
333 |
+
|
334 |
+
if password != confirm_password:
|
335 |
+
return render_template(
|
336 |
+
"register.html",
|
337 |
+
error="Password does not match!",
|
338 |
+
email=email,
|
339 |
+
password=password,
|
340 |
+
confirm_password=confirm_password,
|
341 |
+
)
|
342 |
+
|
343 |
+
hashed_password = generate_password_hash(password, method="pbkdf2:sha256")
|
344 |
+
|
345 |
+
new_user = User(email=email, password=hashed_password)
|
346 |
+
db.session.add(new_user)
|
347 |
+
db.session.commit()
|
348 |
+
|
349 |
+
flash(
|
350 |
+
"Sign up successful! Please log in.", "success"
|
351 |
+
) # Flash the success message
|
352 |
+
return redirect(url_for("login"))
|
353 |
+
|
354 |
+
return render_template("register.html")
|
355 |
+
|
356 |
+
|
357 |
+
@app.route("/login", methods=["GET", "POST"])
|
358 |
+
def login():
|
359 |
+
if request.method == "POST":
|
360 |
+
email = request.form.get("email")
|
361 |
+
password = request.form.get("password")
|
362 |
+
|
363 |
+
user = User.query.filter_by(email=email).first()
|
364 |
+
|
365 |
+
if user and check_password_hash(user.password, password):
|
366 |
+
login_user(user)
|
367 |
+
return dashboard(login_alert=True)
|
368 |
+
else:
|
369 |
+
return render_template("login.html", error="Invalid email or password")
|
370 |
+
|
371 |
+
return render_template("login.html")
|
372 |
+
|
373 |
+
|
374 |
+
def dashboard(login_alert=False):
|
375 |
+
if login_alert:
|
376 |
+
return render_template("index.html", email=current_user.email)
|
377 |
+
return redirect(url_for("index"))
|
378 |
+
|
379 |
+
|
380 |
+
@app.route("/logout")
|
381 |
+
@login_required
|
382 |
+
def logout():
|
383 |
+
logout_user()
|
384 |
+
return redirect(url_for("login"))
|
385 |
+
|
386 |
+
|
387 |
+
@app.route("/history")
|
388 |
+
@login_required
|
389 |
+
def history():
|
390 |
+
data_history = (
|
391 |
+
History.query.filter_by(user_id=current_user.id)
|
392 |
+
.order_by(History.created_date.desc())
|
393 |
+
.all()
|
394 |
+
)
|
395 |
+
|
396 |
+
for item in data_history:
|
397 |
+
item.probabilities = [
|
398 |
+
("anak", f"{item.children_prob:.4f}"),
|
399 |
+
("remaja", f"{item.adolescents_prob:.4f}"),
|
400 |
+
("dewasa", f"{item.adults_prob:.4f}"),
|
401 |
+
("semua usia", f"{item.all_ages_prob:.4f}"),
|
402 |
+
]
|
403 |
+
|
404 |
+
return render_template("history.html", data_history=data_history)
|
405 |
+
|
406 |
+
|
407 |
if __name__ == "__main__":
|
408 |
app.run(debug=True)
|
requirements.txt
CHANGED
@@ -1,6 +1,8 @@
|
|
1 |
Click==7.0
|
2 |
Flask==1.1.2
|
3 |
Flask-SQLAlchemy==2.4.4
|
|
|
|
|
4 |
gunicorn==19.9.0
|
5 |
itsdangerous==1.1.0
|
6 |
Jinja2==2.11.3
|
|
|
1 |
Click==7.0
|
2 |
Flask==1.1.2
|
3 |
Flask-SQLAlchemy==2.4.4
|
4 |
+
flask_login
|
5 |
+
werkzeug
|
6 |
gunicorn==19.9.0
|
7 |
itsdangerous==1.1.0
|
8 |
Jinja2==2.11.3
|
static/css/main.css
CHANGED
@@ -1,36 +1,46 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
|
|
5 |
}
|
6 |
|
7 |
-
|
8 |
-
|
9 |
-
width:
|
|
|
10 |
}
|
11 |
|
12 |
-
|
13 |
-
|
|
|
14 |
}
|
15 |
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
19 |
}
|
20 |
|
21 |
-
|
22 |
-
|
23 |
}
|
24 |
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
}
|
29 |
|
30 |
-
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
33 |
|
34 |
-
|
35 |
-
|
|
|
|
|
36 |
}
|
|
|
1 |
+
/* Custom Container Styles */
|
2 |
+
.container {
|
3 |
+
max-width: 90vw;
|
4 |
+
margin-top: 5rem;
|
5 |
+
margin-bottom: 3rem;
|
6 |
}
|
7 |
|
8 |
+
@media (min-width: 1280px) {
|
9 |
+
.container {
|
10 |
+
max-width: 80vw;
|
11 |
+
}
|
12 |
}
|
13 |
|
14 |
+
/* Audio Control Customization */
|
15 |
+
audio::-webkit-media-controls-panel {
|
16 |
+
background-color: white;
|
17 |
}
|
18 |
|
19 |
+
/* Custom Animation */
|
20 |
+
@keyframes spin {
|
21 |
+
to {
|
22 |
+
transform: rotate(360deg);
|
23 |
+
}
|
24 |
}
|
25 |
|
26 |
+
.animate-spin {
|
27 |
+
animation: spin 1s linear infinite;
|
28 |
}
|
29 |
|
30 |
+
/* Additional Custom Utilities */
|
31 |
+
.min-h-screen {
|
32 |
+
min-height: 90vh;
|
33 |
}
|
34 |
|
35 |
+
/* Responsive Text Sizes */
|
36 |
+
@media (min-width: 640px) {
|
37 |
+
.sm\:text-2xl {
|
38 |
+
font-size: 1.5rem;
|
39 |
+
line-height: 2rem;
|
40 |
+
}
|
41 |
|
42 |
+
.sm\:text-7xl {
|
43 |
+
font-size: 4.5rem;
|
44 |
+
line-height: 1;
|
45 |
+
}
|
46 |
}
|
static/img/BLU SPEED LOGO.png
ADDED
![]() |
Git LFS Details
|
static/img/Logo Kampus Merdeka.png
ADDED
![]() |
Git LFS Details
|
static/img/Logo Kemendikbud.png
ADDED
![]() |
Git LFS Details
|
static/img/Logo PNJ.png
ADDED
![]() |
Git LFS Details
|
static/img/Logo Vokasi Menguatkan.png
ADDED
![]() |
Git LFS Details
|
templates/base.html
CHANGED
@@ -1,14 +1,21 @@
|
|
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 |
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
7 |
-
<
|
|
|
|
|
8 |
{% block head %}{% endblock %}
|
9 |
</head>
|
|
|
10 |
<body>
|
11 |
{% block body %}{% endblock %}
|
|
|
|
|
12 |
</body>
|
|
|
13 |
</html>
|
14 |
{% block script %}{% endblock %}
|
|
|
1 |
<!DOCTYPE html>
|
2 |
<html lang="en">
|
3 |
+
|
4 |
<head>
|
5 |
<meta charset="UTF-8">
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
8 |
+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
9 |
+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/flowbite.min.css" rel="stylesheet" />
|
10 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
|
11 |
{% block head %}{% endblock %}
|
12 |
</head>
|
13 |
+
|
14 |
<body>
|
15 |
{% block body %}{% endblock %}
|
16 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/flowbite.min.js"></script>
|
17 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
18 |
</body>
|
19 |
+
|
20 |
</html>
|
21 |
{% block script %}{% endblock %}
|
templates/dashboard.html
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block head %}
|
4 |
+
<title>Indonesian Lyrics Classification By Age Group - Register</title>
|
5 |
+
{% endblock %}
|
6 |
+
|
7 |
+
{% block body %}
|
8 |
+
<nav>
|
9 |
+
<ul>
|
10 |
+
<li><a href="/logout">Logout</a></li>
|
11 |
+
</ul>
|
12 |
+
</nav>
|
13 |
+
<h1>You are logged in as {{ email }}!</h1>
|
14 |
+
{% endblock %}
|
templates/history.html
ADDED
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block head %}
|
4 |
+
<title>Indonesian Lyrics Classification By Age Group - Prediction</title>
|
5 |
+
{% endblock %}
|
6 |
+
|
7 |
+
{% block body %}
|
8 |
+
{% include 'navbar.html' %}
|
9 |
+
<main class="flex flex-row justify-center items-center min-h-screen">
|
10 |
+
<div class="container max-w-xl w-full bg-white rounded-2xl text-center space-y-6">
|
11 |
+
<div class="flex items-center justify-between p-4 bg-gray-100">
|
12 |
+
<h5 class="text-md font-bold text-gray-900 md:text-md uppercase">Prediction History</h5>
|
13 |
+
</div>
|
14 |
+
<table id="export-table" class="">
|
15 |
+
<thead>
|
16 |
+
<tr>
|
17 |
+
<th class="text-gray-900 text-center">Action</th>
|
18 |
+
<th>
|
19 |
+
<span class="flex items-center text-gray-900">
|
20 |
+
Lyric
|
21 |
+
<svg class="w-4 h-4 ms-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24"
|
22 |
+
height="24" fill="none" viewBox="0 0 24 24">
|
23 |
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
24 |
+
stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4" />
|
25 |
+
</svg>
|
26 |
+
</span>
|
27 |
+
</th>
|
28 |
+
<th>
|
29 |
+
<span class="flex items-center text-gray-900">
|
30 |
+
Predicted Age Group
|
31 |
+
<svg class="w-4 h-4 ms-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24"
|
32 |
+
height="24" fill="none" viewBox="0 0 24 24">
|
33 |
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
34 |
+
stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4" />
|
35 |
+
</svg>
|
36 |
+
</span>
|
37 |
+
</th>
|
38 |
+
<th>
|
39 |
+
<span class="flex items-center text-gray-900">
|
40 |
+
Processing Time (s)
|
41 |
+
<svg class="w-4 h-4 ms-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24"
|
42 |
+
height="24" fill="none" viewBox="0 0 24 24">
|
43 |
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
44 |
+
stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4" />
|
45 |
+
</svg>
|
46 |
+
</span>
|
47 |
+
</th>
|
48 |
+
<th data-type="date" data-format="YYYY-MM-DD HH:mm:ss">
|
49 |
+
<span class="flex items-center text-gray-900">
|
50 |
+
Created Date
|
51 |
+
<svg class="w-4 h-4 ms-1" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24"
|
52 |
+
height="24" fill="none" viewBox="0 0 24 24">
|
53 |
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
54 |
+
stroke-width="2" d="m8 15 4 4 4-4m0-6-4-4-4 4" />
|
55 |
+
</svg>
|
56 |
+
</span>
|
57 |
+
</th>
|
58 |
+
</tr>
|
59 |
+
</thead>
|
60 |
+
<tbody>
|
61 |
+
{% for item in data_history %}
|
62 |
+
<tr class="hover:bg-gray-50 cursor-pointer">
|
63 |
+
<td class="text-center">
|
64 |
+
<!-- Modal toggle -->
|
65 |
+
<button data-modal-target="select-modal-{{ item.id }}" data-modal-toggle="select-modal-{{ item.id }}"
|
66 |
+
class="text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-2 py-0.5 text-center"
|
67 |
+
type="button">
|
68 |
+
Detail
|
69 |
+
</button>
|
70 |
+
<!-- Main modal -->
|
71 |
+
<div id="select-modal-{{ item.id }}" data-modal-backdrop="static" tabindex="-1" aria-hidden="true"
|
72 |
+
class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
73 |
+
<div class="relative w-full max-w-7xl max-h-full">
|
74 |
+
<!-- Modal content -->
|
75 |
+
<div class="relative bg-white rounded-lg shadow-sm">
|
76 |
+
<!-- Modal header -->
|
77 |
+
<div
|
78 |
+
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
|
79 |
+
<h3 class="text-lg font-semibold text-gray-900">
|
80 |
+
{{ item.created_date.strftime('%A, %d %B %Y (%I:%M %p)') }}
|
81 |
+
</h3>
|
82 |
+
<button type="button"
|
83 |
+
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm h-8 w-8 ms-auto inline-flex justify-center items-center"
|
84 |
+
data-modal-toggle="select-modal-{{ item.id }}">
|
85 |
+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
|
86 |
+
fill="none" viewBox="0 0 14 14">
|
87 |
+
<path stroke="currentColor" stroke-linecap="round"
|
88 |
+
stroke-linejoin="round" stroke-width="2"
|
89 |
+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
90 |
+
</svg>
|
91 |
+
<span class="sr-only">Close modal</span>
|
92 |
+
</button>
|
93 |
+
</div>
|
94 |
+
<!-- Modal body -->
|
95 |
+
<div class="p-4 md:p-5">
|
96 |
+
<!-- Lyrics -->
|
97 |
+
<div class="mb-3">
|
98 |
+
<h1 class="text-lg font-semibold text-gray-800 sm:text-xl">🎵 Lyrics</h1>
|
99 |
+
<p class="text-center px-2 py-2 text-1xl sm:text-lg text-gray-800">{{ item.lyric }}</p>
|
100 |
+
</div>
|
101 |
+
|
102 |
+
<hr class="mb-3 border-t border-gray-200" />
|
103 |
+
|
104 |
+
<!-- Predicted Age Group -->
|
105 |
+
<div class="mt-2 mb-3">
|
106 |
+
<h2 class="text-lg font-semibold text-gray-800">Predicted Age Group</h2>
|
107 |
+
<span class="text-center text-lg font-extrabold text-slate-900 sm:text-2xl">
|
108 |
+
{{ item.predicted_label.upper() }}
|
109 |
+
</span>
|
110 |
+
</div>
|
111 |
+
|
112 |
+
<hr class="border-t border-gray-200" />
|
113 |
+
|
114 |
+
<!-- Class Probabilities -->
|
115 |
+
<div class="mt-2 mb-3 flex flex-col text-center items-center">
|
116 |
+
<h3 class="text-lg font-semibold text-gray-800 mb-2">📊 Class Probabilities
|
117 |
+
</h3>
|
118 |
+
<div class="space-y-3">
|
119 |
+
<div class="w-full">
|
120 |
+
{% for label, prob in item.probabilities %}
|
121 |
+
<div
|
122 |
+
class="flex gap-20 justify-between items-center px-4 py-0.5 text-gray-800 {{ 'font-medium bg-green-500' if label == item.predicted_label else '' }}">
|
123 |
+
<div
|
124 |
+
class="capitalize text-1xl sm:text-lg {{ 'font-medium' if label == item.predicted_label else '' }}">
|
125 |
+
{{ label.capitalize() }}
|
126 |
+
</div>
|
127 |
+
<div
|
128 |
+
class="text-1xl sm:text-lg {{ 'font-medium' if label == item.predicted_label else '' }}">
|
129 |
+
{{ prob }}
|
130 |
+
</div>
|
131 |
+
</div>
|
132 |
+
{% endfor %}
|
133 |
+
</div>
|
134 |
+
</div>
|
135 |
+
</div>
|
136 |
+
|
137 |
+
<hr class="border-t border-gray-200" />
|
138 |
+
|
139 |
+
<!-- Processing Time -->
|
140 |
+
{% if item.processing_time %}
|
141 |
+
<div class="text-sm mt-2 mb-3 text-gray-500">
|
142 |
+
⏱️ Total Processing Time: <strong>{{ "%.2f"|format(item.processing_time) }}
|
143 |
+
seconds</strong>
|
144 |
+
</div>
|
145 |
+
{% endif %}
|
146 |
+
</div>
|
147 |
+
<!-- Modal footer -->
|
148 |
+
<div class="flex items-center p-4 md:p-5 border-t border-gray-200 rounded-b">
|
149 |
+
<button data-modal-hide="select-modal-{{ item.id }}" type="button"
|
150 |
+
class="inline-flex w-full justify-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">Close</button>
|
151 |
+
</div>
|
152 |
+
</div>
|
153 |
+
</div>
|
154 |
+
</div>
|
155 |
+
</td>
|
156 |
+
<td class="font-medium text-gray-900 whitespace-nowrap">{{ item.lyric | truncate(60) }}</td>
|
157 |
+
<td class="text-gray-900">{{ item.predicted_label | capitalize }}</td>
|
158 |
+
<td class="text-right !pr-[5rem] text-gray-900">{{ "%.2f"|format(item.processing_time) }} seconds
|
159 |
+
</td>
|
160 |
+
<td class="text-gray-900">{{ item.created_date.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
161 |
+
</tr>
|
162 |
+
{% endfor %}
|
163 |
+
</tbody>
|
164 |
+
</table>
|
165 |
+
</div>
|
166 |
+
</main>
|
167 |
+
<footer class="flex flex-row justify-center items-center">
|
168 |
+
<div class="mb-3 text-gray-500">By Michael Natanael</div>
|
169 |
+
</footer>
|
170 |
+
{% endblock %}
|
171 |
+
|
172 |
+
{% block script %}
|
173 |
+
<script>
|
174 |
+
if (document.getElementById("export-table") && typeof simpleDatatables.DataTable !== 'undefined') {
|
175 |
+
|
176 |
+
const exportCustomCSV = function (dataTable, userOptions = {}) {
|
177 |
+
// A modified CSV export that includes a row of minuses at the start and end.
|
178 |
+
const clonedUserOptions = {
|
179 |
+
...userOptions
|
180 |
+
}
|
181 |
+
clonedUserOptions.download = false
|
182 |
+
const csv = simpleDatatables.exportCSV(dataTable, clonedUserOptions)
|
183 |
+
// If CSV didn't work, exit.
|
184 |
+
if (!csv) {
|
185 |
+
return false
|
186 |
+
}
|
187 |
+
const defaults = {
|
188 |
+
download: true,
|
189 |
+
lineDelimiter: "\n",
|
190 |
+
columnDelimiter: ";"
|
191 |
+
}
|
192 |
+
const options = {
|
193 |
+
...defaults,
|
194 |
+
...clonedUserOptions
|
195 |
+
}
|
196 |
+
const separatorRow = Array(dataTable.data.headings.filter((_heading, index) => !dataTable.columns.settings[index]?.hidden).length)
|
197 |
+
.fill("+")
|
198 |
+
.join("+"); // Use "+" as the delimiter
|
199 |
+
|
200 |
+
const str = separatorRow + options.lineDelimiter + csv + options.lineDelimiter + separatorRow;
|
201 |
+
|
202 |
+
if (userOptions.download) {
|
203 |
+
// Create a link to trigger the download
|
204 |
+
const link = document.createElement("a");
|
205 |
+
link.href = encodeURI("data:text/csv;charset=utf-8," + str);
|
206 |
+
link.download = (options.filename || "Prediction History") + ".txt";
|
207 |
+
// Append the link
|
208 |
+
document.body.appendChild(link);
|
209 |
+
// Trigger the download
|
210 |
+
link.click();
|
211 |
+
// Remove the link
|
212 |
+
document.body.removeChild(link);
|
213 |
+
}
|
214 |
+
|
215 |
+
return str
|
216 |
+
}
|
217 |
+
const table = new simpleDatatables.DataTable("#export-table", {
|
218 |
+
template: (options, dom) => "<div class='" + options.classes.top + "'>" +
|
219 |
+
"<div class='flex flex-col sm:flex-row sm:items-center space-y-4 sm:space-y-0 sm:space-x-3 rtl:space-x-reverse w-full sm:w-auto'>" +
|
220 |
+
(options.paging && options.perPageSelect ?
|
221 |
+
"<div class='" + options.classes.dropdown + "'>" +
|
222 |
+
"<label>" +
|
223 |
+
"<select class='" + options.classes.selector + "'></select> " + options.labels.perPage +
|
224 |
+
"</label>" +
|
225 |
+
"</div>" : ""
|
226 |
+
) + "<button id='exportDropdownButton' type='button' class='flex w-full items-center justify-center rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-900 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:outline-none focus:ring-4 focus:ring-gray-100 sm:w-auto'>" +
|
227 |
+
"Export as" +
|
228 |
+
"<svg class='-me-0.5 ms-1.5 h-4 w-4' aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' viewBox='0 0 24 24'>" +
|
229 |
+
"<path stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m19 9-7 7-7-7' />" +
|
230 |
+
"</svg>" +
|
231 |
+
"</button>" +
|
232 |
+
"<div id='exportDropdown' class='z-10 hidden w-52 divide-y divide-gray-100 rounded-lg bg-white shadow-sm data-popper-placement='bottom'>" +
|
233 |
+
"<ul class='p-2 text-left text-sm font-medium text-gray-500 aria-labelledby='exportDropdownButton'>" +
|
234 |
+
"<li>" +
|
235 |
+
"<button id='export-csv' class='group inline-flex w-full items-center rounded-md px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-900'>" +
|
236 |
+
"<svg class='me-1.5 h-4 w-4 text-gray-400 group-hover:text-gray-900 aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' viewBox='0 0 24 24'>" +
|
237 |
+
"<path fill-rule='evenodd' d='M9 2.221V7H4.221a2 2 0 0 1 .365-.5L8.5 2.586A2 2 0 0 1 9 2.22ZM11 2v5a2 2 0 0 1-2 2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2 2 2 0 0 0 2 2h12a2 2 0 0 0 2-2 2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2V4a2 2 0 0 0-2-2h-7Zm1.018 8.828a2.34 2.34 0 0 0-2.373 2.13v.008a2.32 2.32 0 0 0 2.06 2.497l.535.059a.993.993 0 0 0 .136.006.272.272 0 0 1 .263.367l-.008.02a.377.377 0 0 1-.018.044.49.49 0 0 1-.078.02 1.689 1.689 0 0 1-.297.021h-1.13a1 1 0 1 0 0 2h1.13c.417 0 .892-.05 1.324-.279.47-.248.78-.648.953-1.134a2.272 2.272 0 0 0-2.115-3.06l-.478-.052a.32.32 0 0 1-.285-.341.34.34 0 0 1 .344-.306l.94.02a1 1 0 1 0 .043-2l-.943-.02h-.003Zm7.933 1.482a1 1 0 1 0-1.902-.62l-.57 1.747-.522-1.726a1 1 0 0 0-1.914.578l1.443 4.773a1 1 0 0 0 1.908.021l1.557-4.773Zm-13.762.88a.647.647 0 0 1 .458-.19h1.018a1 1 0 1 0 0-2H6.647A2.647 2.647 0 0 0 4 13.647v1.706A2.647 2.647 0 0 0 6.647 18h1.018a1 1 0 1 0 0-2H6.647A.647.647 0 0 1 6 15.353v-1.706c0-.172.068-.336.19-.457Z' clip-rule='evenodd'/>" +
|
238 |
+
"</svg>" +
|
239 |
+
"<span>Export CSV</span>" +
|
240 |
+
"</button>" +
|
241 |
+
"</li>" +
|
242 |
+
"<li>" +
|
243 |
+
"<button id='export-json' class='group inline-flex w-full items-center rounded-md px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-900'>" +
|
244 |
+
"<svg class='me-1.5 h-4 w-4 text-gray-400 group-hover:text-gray-900 aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' viewBox='0 0 24 24'>" +
|
245 |
+
"<path fill-rule='evenodd' d='M9 2.221V7H4.221a2 2 0 0 1 .365-.5L8.5 2.586A2 2 0 0 1 9 2.22ZM11 2v5a2 2 0 0 1-2 2H4v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-7Zm-.293 9.293a1 1 0 0 1 0 1.414L9.414 14l1.293 1.293a1 1 0 0 1-1.414 1.414l-2-2a1 1 0 0 1 0-1.414l2-2a1 1 0 0 1 1.414 0Zm2.586 1.414a1 1 0 0 1 1.414-1.414l2 2a1 1 0 0 1 0 1.414l-2 2a1 1 0 0 1-1.414-1.414L14.586 14l-1.293-1.293Z' clip-rule='evenodd'/>" +
|
246 |
+
"</svg>" +
|
247 |
+
"<span>Export JSON</span>" +
|
248 |
+
"</button>" +
|
249 |
+
"</li>" +
|
250 |
+
"<li>" +
|
251 |
+
"<button id='export-txt' class='group inline-flex w-full items-center rounded-md px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-900'>" +
|
252 |
+
"<svg class='me-1.5 h-4 w-4 text-gray-400 group-hover:text-gray-900 aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' viewBox='0 0 24 24'>" +
|
253 |
+
"<path fill-rule='evenodd' d='M9 2.221V7H4.221a2 2 0 0 1 .365-.5L8.5 2.586A2 2 0 0 1 9 2.22ZM11 2v5a2 2 0 0 1-2 2H4v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-7ZM8 16a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H9a1 1 0 0 1-1-1Zm1-5a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H9Z' clip-rule='evenodd'/>" +
|
254 |
+
"</svg>" +
|
255 |
+
"<span>Export TXT</span>" +
|
256 |
+
"</button>" +
|
257 |
+
"</li>" +
|
258 |
+
"<li>" +
|
259 |
+
"<button id='export-sql' class='group inline-flex w-full items-center rounded-md px-3 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-900'>" +
|
260 |
+
"<svg class='me-1.5 h-4 w-4 text-gray-400 group-hover:text-gray-900 aria-hidden='true' xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor' viewBox='0 0 24 24'>" +
|
261 |
+
"<path d='M12 7.205c4.418 0 8-1.165 8-2.602C20 3.165 16.418 2 12 2S4 3.165 4 4.603c0 1.437 3.582 2.602 8 2.602ZM12 22c4.963 0 8-1.686 8-2.603v-4.404c-.052.032-.112.06-.165.09a7.75 7.75 0 0 1-.745.387c-.193.088-.394.173-.6.253-.063.024-.124.05-.189.073a18.934 18.934 0 0 1-6.3.998c-2.135.027-4.26-.31-6.3-.998-.065-.024-.126-.05-.189-.073a10.143 10.143 0 0 1-.852-.373 7.75 7.75 0 0 1-.493-.267c-.053-.03-.113-.058-.165-.09v4.404C4 20.315 7.037 22 12 22Zm7.09-13.928a9.91 9.91 0 0 1-.6.253c-.063.025-.124.05-.189.074a18.935 18.935 0 0 1-6.3.998c-2.135.027-4.26-.31-6.3-.998-.065-.024-.126-.05-.189-.074a10.163 10.163 0 0 1-.852-.372 7.816 7.816 0 0 1-.493-.268c-.055-.03-.115-.058-.167-.09V12c0 .917 3.037 2.603 8 2.603s8-1.686 8-2.603V7.596c-.052.031-.112.059-.165.09a7.816 7.816 0 0 1-.745.386Z'/>" +
|
262 |
+
"</svg>" +
|
263 |
+
"<span>Export SQL</span>" +
|
264 |
+
"</button>" +
|
265 |
+
"</li>" +
|
266 |
+
"</ul>" +
|
267 |
+
"</div>" + "</div>" +
|
268 |
+
(options.searchable ?
|
269 |
+
"<div class='" + options.classes.search + "'>" +
|
270 |
+
"<input class='" + options.classes.input + "' placeholder='" + options.labels.placeholder + "' type='search' title='" + options.labels.searchTitle + "'" + (dom.id ? " aria-controls='" + dom.id + "'" : "") + ">" +
|
271 |
+
"</div>" : ""
|
272 |
+
) +
|
273 |
+
"</div>" +
|
274 |
+
"<div class='" + options.classes.container + "'" + (options.scrollY.length ? " style='height: " + options.scrollY + "; overflow-Y: auto;'" : "") + "></div>" +
|
275 |
+
"<div class='" + options.classes.bottom + "'>" +
|
276 |
+
(options.paging ?
|
277 |
+
"<div class='" + options.classes.info + "'></div>" : ""
|
278 |
+
) +
|
279 |
+
"<nav class='" + options.classes.pagination + "'></nav>" +
|
280 |
+
"</div>"
|
281 |
+
})
|
282 |
+
const $exportButton = document.getElementById("exportDropdownButton");
|
283 |
+
const $exportDropdownEl = document.getElementById("exportDropdown");
|
284 |
+
const dropdown = new Dropdown($exportDropdownEl, $exportButton);
|
285 |
+
console.log(dropdown)
|
286 |
+
|
287 |
+
document.getElementById("export-csv").addEventListener("click", () => {
|
288 |
+
simpleDatatables.exportCSV(table, {
|
289 |
+
download: true,
|
290 |
+
lineDelimiter: "\n",
|
291 |
+
columnDelimiter: ";"
|
292 |
+
})
|
293 |
+
})
|
294 |
+
document.getElementById("export-sql").addEventListener("click", () => {
|
295 |
+
simpleDatatables.exportSQL(table, {
|
296 |
+
download: true,
|
297 |
+
tableName: "export_table"
|
298 |
+
})
|
299 |
+
})
|
300 |
+
document.getElementById("export-txt").addEventListener("click", () => {
|
301 |
+
simpleDatatables.exportTXT(table, {
|
302 |
+
download: true
|
303 |
+
})
|
304 |
+
})
|
305 |
+
document.getElementById("export-json").addEventListener("click", () => {
|
306 |
+
simpleDatatables.exportJSON(table, {
|
307 |
+
download: true,
|
308 |
+
space: 3
|
309 |
+
})
|
310 |
+
})
|
311 |
+
}
|
312 |
+
</script>
|
313 |
+
{% endblock %}
|
templates/index.html
CHANGED
@@ -5,44 +5,68 @@
|
|
5 |
{% endblock %}
|
6 |
|
7 |
{% block body %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
<main class="flex flex-row justify-center items-center min-h-screen">
|
9 |
<div class="container flex flex-col justify-center items-center">
|
10 |
-
<h1 class="mb-
|
11 |
Classification By Age Group</h1>
|
12 |
<h2 class="mt-4 mb-2 px-4 text-center text-1xl font-semibold tracking-tight text-slate-900 sm:text-2xl">
|
13 |
Upload or Record Your Lyrics:</h2>
|
14 |
<div
|
15 |
-
class="mb-
|
16 |
<div class="flex flex-row space-x-2 py-2 w-full px-2">
|
17 |
-
<button id="uploadButton"
|
18 |
-
class="flex items-center justify-center rounded-lg p-2 bg-blue text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 transition-all duration-200">
|
19 |
-
<
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
</button>
|
27 |
<div class="w-[1px] bg-slate-200"></div>
|
28 |
-
<button id="recordButton"
|
29 |
-
class="flex items-center justify-center rounded-lg p-2 bg-blue text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 transition-all duration-200">
|
30 |
-
<
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
<
|
37 |
</button>
|
38 |
</div>
|
39 |
-
<div class="w-full bg-gray-
|
40 |
<div class="bg-blue-600 h-1 rounded-full transition-all duration-100" style="width: 0%;"></div>
|
41 |
</div>
|
42 |
</div>
|
43 |
|
44 |
<!-- Hidden playback and transcribe sections -->
|
45 |
-
<div id="playback" class="mb-
|
46 |
<audio id="audioPlayer" controls
|
47 |
class="w-full h-14 rounded-lg bg-white shadow-xl shadow-black/5 ring-1 ring-slate-700/10">
|
48 |
<source id="audioSource" type="audio/mpeg" src="">
|
@@ -52,28 +76,27 @@
|
|
52 |
<form action="/transcribe" method="POST" enctype="multipart/form-data">
|
53 |
<input type="file" name="file" id="fileInput" accept=".mp3" class="hidden">
|
54 |
<button type="button" onclick="showInputText()"
|
55 |
-
class="text-white bg-red-500 hover:bg-red-600 font-medium rounded-lg text-
|
56 |
Cancel
|
57 |
</button>
|
58 |
<button type="submit"
|
59 |
-
class="text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-
|
60 |
Predict Age Group
|
61 |
</button>
|
62 |
</form>
|
63 |
</div>
|
64 |
|
65 |
<!-- Text Input for Manual Lyrics Prediction -->
|
66 |
-
<div class="mt-
|
67 |
<form action="/predict-text" method="POST" class="flex flex-col space-y-4">
|
68 |
<label for="lyricsInput"
|
69 |
class="mb-2 text-1xl font-semibold tracking-tight text-slate-900 sm:text-2xl">Or Input Your Lyrics
|
70 |
Here:</label>
|
71 |
<textarea name="lyrics" id="lyricsInput" rows="6" required
|
72 |
-
class="w-full rounded-lg border border-gray-300 px-2 py-2 text-
|
73 |
placeholder="Type or paste Indonesian lyrics..."></textarea>
|
74 |
-
<br>
|
75 |
<button type="submit"
|
76 |
-
class="text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-
|
77 |
Predict Age Group
|
78 |
</button>
|
79 |
</form>
|
@@ -81,7 +104,7 @@
|
|
81 |
</div>
|
82 |
</main>
|
83 |
<footer class="flex flex-row justify-center items-center">
|
84 |
-
<div class="mb-
|
85 |
</footer>
|
86 |
{% endblock %}
|
87 |
|
@@ -123,7 +146,7 @@
|
|
123 |
}
|
124 |
submitBtn.disabled = true;
|
125 |
submitBtn.innerHTML = `
|
126 |
-
<svg aria-hidden="true" role="status" class="inline w-
|
127 |
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
128 |
fill="#E5E7EB" />
|
129 |
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
|
5 |
{% endblock %}
|
6 |
|
7 |
{% block body %}
|
8 |
+
{% include 'navbar.html' %}
|
9 |
+
{% if email %}
|
10 |
+
<div id="toast-success" class="fixed top-2 left-1/2 transform -translate-x-1/2 z-50 flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow-sm"
|
11 |
+
role="alert">
|
12 |
+
<div class="inline-flex items-center justify-center shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg">
|
13 |
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
|
14 |
+
viewBox="0 0 20 20">
|
15 |
+
<path
|
16 |
+
d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z" />
|
17 |
+
</svg>
|
18 |
+
<span class="sr-only">Check icon</span>
|
19 |
+
</div>
|
20 |
+
<div class="ms-3 text-sm font-normal">You are logged in as <b>{{ email }}</b>!</div>
|
21 |
+
<button type="button"
|
22 |
+
class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8"
|
23 |
+
data-dismiss-target="#toast-success" aria-label="Close">
|
24 |
+
<span class="sr-only">Close</span>
|
25 |
+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
26 |
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
27 |
+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
28 |
+
</svg>
|
29 |
+
</button>
|
30 |
+
</div>
|
31 |
+
{% endif %}
|
32 |
<main class="flex flex-row justify-center items-center min-h-screen">
|
33 |
<div class="container flex flex-col justify-center items-center">
|
34 |
+
<h1 class="mb-4 text-5xl font-extrabold tracking-tight text-slate-900 sm:text-7xl text-center">Indonesian Lyrics
|
35 |
Classification By Age Group</h1>
|
36 |
<h2 class="mt-4 mb-2 px-4 text-center text-1xl font-semibold tracking-tight text-slate-900 sm:text-2xl">
|
37 |
Upload or Record Your Lyrics:</h2>
|
38 |
<div
|
39 |
+
class="mb-2 flex flex-col justify-center items-center rounded-lg bg-white shadow-xl shadow-black/5 ring-1 ring-slate-700/10">
|
40 |
<div class="flex flex-row space-x-2 py-2 w-full px-2">
|
41 |
+
<button id="uploadButton" type="button"
|
42 |
+
class="inline-flex items-center text-1xl sm:text-lg justify-center rounded-lg p-2 bg-blue text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 transition-all duration-200">
|
43 |
+
<svg class="w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
44 |
+
stroke-width="1.5" stroke="currentColor">
|
45 |
+
<path stroke-linecap="round" stroke-linejoin="round"
|
46 |
+
d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776">
|
47 |
+
</path>
|
48 |
+
</svg>
|
49 |
+
From File
|
50 |
</button>
|
51 |
<div class="w-[1px] bg-slate-200"></div>
|
52 |
+
<button id="recordButton" type="button"
|
53 |
+
class="inline-flex items-center text-1xl sm:text-lg justify-center rounded-lg p-2 bg-blue text-slate-500 hover:text-indigo-600 hover:bg-indigo-50 transition-all duration-200">
|
54 |
+
<svg class="w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
55 |
+
stroke-width="1.5" stroke="currentColor">
|
56 |
+
<path stroke-linecap="round" stroke-linejoin="round"
|
57 |
+
d="M12 18.75a6 6 0 006-6v-1.5m-6 7.5a6 6 0 01-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 01-3-3V4.5a3 3 0 116 0v8.25a3 3 0 01-3 3z">
|
58 |
+
</path>
|
59 |
+
</svg>
|
60 |
+
<span id="recordText">Record</span>
|
61 |
</button>
|
62 |
</div>
|
63 |
+
<div class="w-full bg-gray-700 rounded-full h-1">
|
64 |
<div class="bg-blue-600 h-1 rounded-full transition-all duration-100" style="width: 0%;"></div>
|
65 |
</div>
|
66 |
</div>
|
67 |
|
68 |
<!-- Hidden playback and transcribe sections -->
|
69 |
+
<div id="playback" class="mb-2 flex relative z-10 p-4 w-full hidden">
|
70 |
<audio id="audioPlayer" controls
|
71 |
class="w-full h-14 rounded-lg bg-white shadow-xl shadow-black/5 ring-1 ring-slate-700/10">
|
72 |
<source id="audioSource" type="audio/mpeg" src="">
|
|
|
76 |
<form action="/transcribe" method="POST" enctype="multipart/form-data">
|
77 |
<input type="file" name="file" id="fileInput" accept=".mp3" class="hidden">
|
78 |
<button type="button" onclick="showInputText()"
|
79 |
+
class="text-white bg-red-500 hover:bg-red-600 font-medium rounded-lg text-1xl sm:text-lg px-5 py-2.5 text-center">
|
80 |
Cancel
|
81 |
</button>
|
82 |
<button type="submit"
|
83 |
+
class="text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-1xl sm:text-lg px-5 py-2.5 text-center">
|
84 |
Predict Age Group
|
85 |
</button>
|
86 |
</form>
|
87 |
</div>
|
88 |
|
89 |
<!-- Text Input for Manual Lyrics Prediction -->
|
90 |
+
<div class="mt-2 w-full" id="input-text">
|
91 |
<form action="/predict-text" method="POST" class="flex flex-col space-y-4">
|
92 |
<label for="lyricsInput"
|
93 |
class="mb-2 text-1xl font-semibold tracking-tight text-slate-900 sm:text-2xl">Or Input Your Lyrics
|
94 |
Here:</label>
|
95 |
<textarea name="lyrics" id="lyricsInput" rows="6" required
|
96 |
+
class="w-full rounded-lg border border-gray-300 px-2 py-2 text-1xl sm:text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
97 |
placeholder="Type or paste Indonesian lyrics..."></textarea>
|
|
|
98 |
<button type="submit"
|
99 |
+
class="text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-1xl sm:text-lg px-5 py-2.5 text-center">
|
100 |
Predict Age Group
|
101 |
</button>
|
102 |
</form>
|
|
|
104 |
</div>
|
105 |
</main>
|
106 |
<footer class="flex flex-row justify-center items-center">
|
107 |
+
<div class="mb-3 text-gray-500">By Michael Natanael</div>
|
108 |
</footer>
|
109 |
{% endblock %}
|
110 |
|
|
|
146 |
}
|
147 |
submitBtn.disabled = true;
|
148 |
submitBtn.innerHTML = `
|
149 |
+
<svg aria-hidden="true" role="status" class="inline w-5 h-5 me-1 text-white animate-spin" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
150 |
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
151 |
fill="#E5E7EB" />
|
152 |
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
templates/login.html
ADDED
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block head %}
|
4 |
+
<title>Indonesian Lyrics Classification By Age Group - Login</title>
|
5 |
+
{% endblock %}
|
6 |
+
|
7 |
+
{% block body %}
|
8 |
+
<section class="bg-white">
|
9 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
10 |
+
{% if messages %}
|
11 |
+
{% for category, message in messages %}
|
12 |
+
<div id="toast-success"
|
13 |
+
class="fixed top-2 left-1/2 transform -translate-x-1/2 z-50 flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow-sm"
|
14 |
+
role="alert">
|
15 |
+
<div class="inline-flex items-center justify-center shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg">
|
16 |
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
|
17 |
+
viewBox="0 0 20 20">
|
18 |
+
<path
|
19 |
+
d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z" />
|
20 |
+
</svg>
|
21 |
+
<span class="sr-only">Check icon</span>
|
22 |
+
</div>
|
23 |
+
<div class="ms-3 text-sm font-normal">{{ message }}</div>
|
24 |
+
<button type="button"
|
25 |
+
class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8"
|
26 |
+
data-dismiss-target="#toast-success" aria-label="Close">
|
27 |
+
<span class="sr-only">Close</span>
|
28 |
+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
29 |
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
30 |
+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
31 |
+
</svg>
|
32 |
+
</button>
|
33 |
+
</div>
|
34 |
+
{% endfor %}
|
35 |
+
{% endif %}
|
36 |
+
{% endwith %}
|
37 |
+
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
38 |
+
<a href="/" class="flex flex-col gap-3 items-center mb-6 text-2xl font-semibold text-gray-900">
|
39 |
+
<div class="flex">
|
40 |
+
<img src="/static/img/Logo PNJ.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
41 |
+
<img src="/static/img/BLU SPEED LOGO.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
42 |
+
<img src="/static/img/Logo Kemendikbud.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
43 |
+
<img src="/static/img/Logo Kampus Merdeka.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
44 |
+
<img src="/static/img/Logo Vokasi Menguatkan.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
45 |
+
</div>
|
46 |
+
<h1
|
47 |
+
class="text-center text-2xl font-extrabold text-slate-900 leading-tight tracking-tight text-gray-900 md:text-3xl">
|
48 |
+
Indonesian Lyrics Classification By Age Group
|
49 |
+
</h1>
|
50 |
+
</a>
|
51 |
+
<div class="w-full rounded-lg shadow-sm inset-shadow-sm md:mt-0 sm:max-w-md xl:p-0">
|
52 |
+
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
|
53 |
+
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl">
|
54 |
+
Sign in to your account
|
55 |
+
</h1>
|
56 |
+
{% if error %}
|
57 |
+
<div id="toast-default" class="flex p-4 mb-4 rounded-lg bg-red-100" role="alert">
|
58 |
+
<div class="text-sm text-red-800" role="alert">
|
59 |
+
{{ error }}
|
60 |
+
</div>
|
61 |
+
<button type="button"
|
62 |
+
class="ms-auto -mx-1.5 -my-1.5 text-gray-400 hover:text-red-800 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:text-red-800 inline-flex items-center justify-center h-8 w-8"
|
63 |
+
data-dismiss-target="#toast-default" aria-label="Close">
|
64 |
+
<span class="sr-only">Close</span>
|
65 |
+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
66 |
+
viewBox="0 0 14 14">
|
67 |
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
68 |
+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
69 |
+
</svg>
|
70 |
+
</button>
|
71 |
+
</div>
|
72 |
+
{% endif %}
|
73 |
+
<form class="space-y-4 md:space-y-6" action="/login" method="post">
|
74 |
+
<div>
|
75 |
+
<label for="email" class="block mb-2 text-sm font-medium text-gray-900">Your
|
76 |
+
email</label>
|
77 |
+
<input type="email" name="email" id="email"
|
78 |
+
class="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
|
79 |
+
placeholder="[email protected]" required="">
|
80 |
+
</div>
|
81 |
+
<div>
|
82 |
+
<label for="password" class="block mb-2 text-sm font-medium text-gray-900">Password</label>
|
83 |
+
<input type="password" name="password" id="password" placeholder="••••••••"
|
84 |
+
class="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
|
85 |
+
required="">
|
86 |
+
</div>
|
87 |
+
<div class="flex items-center justify-between">
|
88 |
+
<div class="flex items-start">
|
89 |
+
<div class="flex items-center h-5">
|
90 |
+
<input id="remember" aria-describedby="remember" type="checkbox"
|
91 |
+
class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300">
|
92 |
+
</div>
|
93 |
+
<div class="ml-3 text-sm">
|
94 |
+
<label for="remember" class="text-gray-500">Remember me</label>
|
95 |
+
</div>
|
96 |
+
</div>
|
97 |
+
<a href="#" class="text-sm font-medium text-primary-600 hover:underline">Forgot
|
98 |
+
password?</a>
|
99 |
+
</div>
|
100 |
+
<div class="flex gap-3">
|
101 |
+
<button type="submit"
|
102 |
+
class="flex-1 w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
|
103 |
+
Sign in</button>
|
104 |
+
<a href="/"
|
105 |
+
class="flex-2 text-white bg-gray-400 hover:bg-gray-500 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
|
106 |
+
Continue without sign in</a>
|
107 |
+
</div>
|
108 |
+
<p class="text-sm font-light text-gray-500">
|
109 |
+
Don’t have an account yet? <a href="/register"
|
110 |
+
class="font-medium text-primary-600 hover:underline">Sign
|
111 |
+
up</a>
|
112 |
+
</p>
|
113 |
+
</form>
|
114 |
+
</div>
|
115 |
+
</div>
|
116 |
+
</div>
|
117 |
+
</section>
|
118 |
+
<footer class="flex flex-row justify-center items-center">
|
119 |
+
<div class="absolute bottom-4 text-gray-500">By Michael Natanael</div>
|
120 |
+
</footer>
|
121 |
+
{% endblock %}
|
templates/navbar.html
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<nav class="bg-white fixed w-full z-20 top-0 start-0 border-b border-gray-200">
|
2 |
+
<div class="flex flex-wrap items-center justify-between mx-auto p-4">
|
3 |
+
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
|
4 |
+
<img src="/static/img/Logo PNJ.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
5 |
+
<img src="/static/img/BLU SPEED LOGO.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
6 |
+
<img src="/static/img/Logo Kemendikbud.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
7 |
+
<img src="/static/img/Logo Kampus Merdeka.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
8 |
+
<img src="/static/img/Logo Vokasi Menguatkan.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
9 |
+
</a>
|
10 |
+
<div class="flex md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
|
11 |
+
<button data-collapse-toggle="navbar-sticky" type="button"
|
12 |
+
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200"
|
13 |
+
aria-controls="navbar-sticky" aria-expanded="false">
|
14 |
+
<span class="sr-only">Open main menu</span>
|
15 |
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
16 |
+
viewBox="0 0 17 14">
|
17 |
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
18 |
+
d="M1 1h15M1 7h15M1 13h15" />
|
19 |
+
</svg>
|
20 |
+
</button>
|
21 |
+
</div>
|
22 |
+
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1 ml-auto" id="navbar-sticky">
|
23 |
+
<ul
|
24 |
+
class="flex flex-col p-4 md:p-0 mt-4 font-medium border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white">
|
25 |
+
{% if current_user.is_authenticated %}
|
26 |
+
<li>
|
27 |
+
{% if request.path == url_for('index') %}
|
28 |
+
<a href="/"
|
29 |
+
class="block py-2 px-3 text-white bg-blue-700 rounded-sm md:bg-transparent md:text-blue-700 md:p-0"
|
30 |
+
aria-current="page">Home</a>
|
31 |
+
{% else %}
|
32 |
+
<a href="/"
|
33 |
+
class="block py-2 px-3 text-gray-900 rounded-sm hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0">Home</a>
|
34 |
+
{% endif %}
|
35 |
+
</li>
|
36 |
+
<li>
|
37 |
+
{% if request.path == url_for('history') %}
|
38 |
+
<a href="/history"
|
39 |
+
class="block py-2 px-3 text-white bg-blue-700 rounded-sm md:bg-transparent md:text-blue-700 md:p-0"
|
40 |
+
aria-current="page">History</a>
|
41 |
+
{% else %}
|
42 |
+
<a href="/history"
|
43 |
+
class="block py-2 px-3 text-gray-900 rounded-sm hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0">History</a>
|
44 |
+
{% endif %}
|
45 |
+
</li>
|
46 |
+
<li>
|
47 |
+
<a href="/logout"
|
48 |
+
class="flex items-center gap-1 block py-2 px-3 text-red-600 rounded-sm hover:bg-gray-100 md:hover:bg-transparent md:hover:text-red-700 md:p-0">
|
49 |
+
<svg class="block w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
50 |
+
viewBox="0 0 16 16">
|
51 |
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
52 |
+
d="M4 8h11m0 0-4-4m4 4-4 4m-5 3H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h3" />
|
53 |
+
</svg>
|
54 |
+
<span>Logout</span>
|
55 |
+
</a>
|
56 |
+
</li>
|
57 |
+
{% else %}
|
58 |
+
<li>
|
59 |
+
<a href="/login"
|
60 |
+
class="text-blue-700 hover:text-white border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 text-center me-2">Login</a>
|
61 |
+
<a href="/register"
|
62 |
+
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 text-center">Register</a>
|
63 |
+
</li>
|
64 |
+
{% endif %}
|
65 |
+
</ul>
|
66 |
+
</div>
|
67 |
+
</div>
|
68 |
+
</nav>
|
templates/register.html
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends 'base.html' %}
|
2 |
+
|
3 |
+
{% block head %}
|
4 |
+
<title>Indonesian Lyrics Classification By Age Group - Register</title>
|
5 |
+
{% endblock %}
|
6 |
+
|
7 |
+
{% block body %}
|
8 |
+
<section class="bg-white">
|
9 |
+
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
|
10 |
+
<a href="/" class="flex flex-col gap-3 items-center mb-6 text-2xl font-semibold text-gray-900">
|
11 |
+
<div class="flex">
|
12 |
+
<img src="/static/img/Logo PNJ.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
13 |
+
<img src="/static/img/BLU SPEED LOGO.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
14 |
+
<img src="/static/img/Logo Kemendikbud.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
15 |
+
<img src="/static/img/Logo Kampus Merdeka.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
16 |
+
<img src="/static/img/Logo Vokasi Menguatkan.png" class="h-8" alt="Politeknik Negeri Jakarta">
|
17 |
+
</div>
|
18 |
+
<h1
|
19 |
+
class="text-center text-2xl font-extrabold text-slate-900 leading-tight tracking-tight text-gray-900 md:text-3xl">
|
20 |
+
Indonesian Lyrics Classification By Age Group
|
21 |
+
</h1>
|
22 |
+
</a>
|
23 |
+
<div class="w-full rounded-lg shadow-sm inset-shadow-sm md:mt-0 sm:max-w-md xl:p-0">
|
24 |
+
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
|
25 |
+
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl">
|
26 |
+
Sign up your new account
|
27 |
+
</h1>
|
28 |
+
{% if error %}
|
29 |
+
<div id="toast-default" class="flex p-4 mb-4 rounded-lg bg-red-100" role="alert">
|
30 |
+
<div class="text-sm text-red-800" role="alert">
|
31 |
+
{{ error }}
|
32 |
+
</div>
|
33 |
+
<button type="button"
|
34 |
+
class="ms-auto -mx-1.5 -my-1.5 text-gray-400 hover:text-red-800 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:text-red-800 inline-flex items-center justify-center h-8 w-8"
|
35 |
+
data-dismiss-target="#toast-default" aria-label="Close">
|
36 |
+
<span class="sr-only">Close</span>
|
37 |
+
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
38 |
+
viewBox="0 0 14 14">
|
39 |
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
40 |
+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
41 |
+
</svg>
|
42 |
+
</button>
|
43 |
+
</div>
|
44 |
+
{% endif %}
|
45 |
+
<form class="space-y-4 md:space-y-6" action="/register" method="post">
|
46 |
+
<div>
|
47 |
+
<label for="email" class="block mb-2 text-sm font-medium text-gray-900">Email</label>
|
48 |
+
<input type="email" name="email" id="email"
|
49 |
+
class="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
|
50 |
+
placeholder="[email protected]" required="" value="{{ email }}">
|
51 |
+
</div>
|
52 |
+
<div>
|
53 |
+
<label for="password" class="block mb-2 text-sm font-medium text-gray-900">New Password</label>
|
54 |
+
<input type="password" name="password" id="password" placeholder="••••••••"
|
55 |
+
class="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
|
56 |
+
required="" value="{{ password }}">
|
57 |
+
</div>
|
58 |
+
<div>
|
59 |
+
<label for="confirm-password" class="block mb-2 text-sm font-medium text-gray-900">Confirm
|
60 |
+
Password</label>
|
61 |
+
<input type="password" name="confirm-password" id="confirm-password" placeholder="••••••••"
|
62 |
+
class="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
|
63 |
+
required="" value="{{ confirm_password }}">
|
64 |
+
</div>
|
65 |
+
<button type="submit"
|
66 |
+
class="flex-1 w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
|
67 |
+
Sign up</button>
|
68 |
+
<p class="text-sm font-light text-gray-500">
|
69 |
+
Already have an account? <a href="/login"
|
70 |
+
class="font-medium text-primary-600 hover:underline">Sign
|
71 |
+
in</a>
|
72 |
+
</p>
|
73 |
+
</form>
|
74 |
+
</div>
|
75 |
+
</div>
|
76 |
+
</div>
|
77 |
+
</section>
|
78 |
+
<footer class="flex flex-row justify-center items-center">
|
79 |
+
<div class="absolute bottom-4 text-gray-500">By Michael Natanael</div>
|
80 |
+
</footer>
|
81 |
+
{% endblock %}
|
templates/transcribe.html
CHANGED
@@ -5,36 +5,37 @@
|
|
5 |
{% endblock %}
|
6 |
|
7 |
{% block body %}
|
|
|
8 |
<main class="flex flex-row justify-center items-center min-h-screen">
|
9 |
-
<div class="container max-w-xl w-full bg-white rounded-2xl
|
10 |
|
11 |
<!-- Lyrics -->
|
12 |
-
<div class="mb-
|
13 |
<h1 class="text-lg font-semibold text-gray-800 sm:text-xl">🎵 Lyrics</h1>
|
14 |
<p class="text-center px-2 py-2 text-1xl sm:text-lg">{{ task }}</p>
|
15 |
</div>
|
16 |
|
17 |
-
<hr class="
|
18 |
|
19 |
<!-- Predicted Age Group -->
|
20 |
-
<div class="mt-
|
21 |
<h2 class="text-lg font-semibold text-gray-800">Predicted Age Group</h2>
|
22 |
<span class="text-center text-lg font-extrabold text-slate-900 sm:text-2xl">
|
23 |
{{ prediction.upper() }}
|
24 |
</span>
|
25 |
</div>
|
26 |
|
27 |
-
<hr class="
|
28 |
|
29 |
<!-- Class Probabilities -->
|
30 |
-
<div class="mt-
|
31 |
<h3 class="text-lg font-semibold text-gray-800 mb-2">📊 Class Probabilities</h3>
|
32 |
<div class="space-y-3">
|
33 |
<table>
|
34 |
{% for label, prob in probabilities %}
|
35 |
<tr class="{{ 'bg-green-500' if label == prediction else '' }}">
|
36 |
<td class="px-2 text-left">
|
37 |
-
<span class="capitalize text-gray-
|
38 |
</td>
|
39 |
<td class="px-5"></td>
|
40 |
<td class="px-2 text-right">
|
@@ -46,25 +47,25 @@
|
|
46 |
</div>
|
47 |
</div>
|
48 |
|
49 |
-
<hr class="
|
50 |
|
51 |
<!-- Processing Time -->
|
52 |
{% if total_time %}
|
53 |
-
<div class="mt-
|
54 |
⏱️ Total Processing Time: <strong>{{ total_time }}</strong>
|
55 |
</div>
|
56 |
{% endif %}
|
57 |
|
58 |
<!-- Action Button -->
|
59 |
-
<div class="w-full flex flex-col
|
60 |
-
<div class="mt-
|
61 |
-
<a href="/" class="bg-blue-700 hover:bg-blue-800
|
62 |
🔁 Try Another Lyrics
|
63 |
</a>
|
64 |
</div>
|
65 |
</div>
|
66 |
</main>
|
67 |
<footer class="flex flex-row justify-center items-center">
|
68 |
-
<div class="mb-
|
69 |
</footer>
|
70 |
{% endblock %}
|
|
|
5 |
{% endblock %}
|
6 |
|
7 |
{% block body %}
|
8 |
+
{% include 'navbar.html' %}
|
9 |
<main class="flex flex-row justify-center items-center min-h-screen">
|
10 |
+
<div class="container max-w-xl w-full bg-white rounded-2xl p-8 text-center space-y-6">
|
11 |
|
12 |
<!-- Lyrics -->
|
13 |
+
<div class="mb-3">
|
14 |
<h1 class="text-lg font-semibold text-gray-800 sm:text-xl">🎵 Lyrics</h1>
|
15 |
<p class="text-center px-2 py-2 text-1xl sm:text-lg">{{ task }}</p>
|
16 |
</div>
|
17 |
|
18 |
+
<hr class="mb-3 border-t border-gray-200" />
|
19 |
|
20 |
<!-- Predicted Age Group -->
|
21 |
+
<div class="mt-2 mb-3">
|
22 |
<h2 class="text-lg font-semibold text-gray-800">Predicted Age Group</h2>
|
23 |
<span class="text-center text-lg font-extrabold text-slate-900 sm:text-2xl">
|
24 |
{{ prediction.upper() }}
|
25 |
</span>
|
26 |
</div>
|
27 |
|
28 |
+
<hr class="border-t border-gray-200" />
|
29 |
|
30 |
<!-- Class Probabilities -->
|
31 |
+
<div class="mt-2 mb-3 flex flex-col text-center items-center">
|
32 |
<h3 class="text-lg font-semibold text-gray-800 mb-2">📊 Class Probabilities</h3>
|
33 |
<div class="space-y-3">
|
34 |
<table>
|
35 |
{% for label, prob in probabilities %}
|
36 |
<tr class="{{ 'bg-green-500' if label == prediction else '' }}">
|
37 |
<td class="px-2 text-left">
|
38 |
+
<span class="capitalize text-gray-800 text-1xl sm:text-lg {{ 'font-medium' if label == prediction else '' }}">{{ label.capitalize() }}</span>
|
39 |
</td>
|
40 |
<td class="px-5"></td>
|
41 |
<td class="px-2 text-right">
|
|
|
47 |
</div>
|
48 |
</div>
|
49 |
|
50 |
+
<hr class="border-t border-gray-200" />
|
51 |
|
52 |
<!-- Processing Time -->
|
53 |
{% if total_time %}
|
54 |
+
<div class="text-sm mt-2 mb-3 text-gray-500">
|
55 |
⏱️ Total Processing Time: <strong>{{ total_time }}</strong>
|
56 |
</div>
|
57 |
{% endif %}
|
58 |
|
59 |
<!-- Action Button -->
|
60 |
+
<div class="w-full flex flex-col mb-5 overflow-y-auto"></div>
|
61 |
+
<div class="mt-2">
|
62 |
+
<a href="/" class="text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-1xl sm:text-lg px-5 py-2.5 text-center">
|
63 |
🔁 Try Another Lyrics
|
64 |
</a>
|
65 |
</div>
|
66 |
</div>
|
67 |
</main>
|
68 |
<footer class="flex flex-row justify-center items-center">
|
69 |
+
<div class="mb-3 text-gray-500">By Michael Natanael</div>
|
70 |
</footer>
|
71 |
{% endblock %}
|