Michael Natanael commited on
Commit
0c67b3e
·
1 Parent(s): b6ae325

Add prediction history page (only for authenticated users)

Browse files
.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 Flask, render_template, request
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 # Adjust if needed
 
 
 
 
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 = ["semua usia", "anak", "remaja", "dewasa"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, 'wb') as f, tqdm(total=total, unit='B', unit_scale=True, desc="Downloading") as pbar:
 
 
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('indolem/indobert-base-uncased')
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 # Lower beam_size, faster but may miss words
74
  )
75
 
76
- print("Detected language '%s' with probability %f" % (info.language, info.language_probability))
 
 
 
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, # Ensures input ≤512 tokens
90
  return_token_type_ids=True,
91
  padding="max_length",
92
  return_attention_mask=True,
93
- return_tensors='pt',
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 = [(label, f"{prob:.4f}") for label, prob in zip(AGE_LABELS, probabilities)]
 
 
109
  return predicted_label, prob_results
110
 
111
 
112
  # === ROUTES ===
113
 
114
- @app.route('/', methods=['GET'])
 
115
  def index():
116
- return render_template('index.html')
117
 
118
 
119
- @app.route('/transcribe', methods=['POST'])
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['file']
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
- 'transcribe.html',
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('/predict-text', methods=['POST'])
162
  def predict_text():
163
  try:
164
- user_lyrics = request.form.get('lyrics', '').strip()
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 = f"{end_time - start_time:.2f} seconds"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  return render_template(
180
- 'transcribe.html',
181
  task=user_lyrics,
182
  prediction=predicted_label,
183
  probabilities=prob_results,
184
- total_time=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
- body, html {
2
- margin: 0;
3
- font-family: sans-serif;
4
- background-color: lightblue;
 
5
  }
6
 
7
- .content {
8
- margin: 0 auto;
9
- width: 400px;
 
10
  }
11
 
12
- table, td, th {
13
- border: 1px solid #aaa;
 
14
  }
15
 
16
- table {
17
- border-collapse: collapse;
18
- width: 100%;
 
 
19
  }
20
 
21
- th {
22
- height: 30px;
23
  }
24
 
25
- td {
26
- text-align: center;
27
- padding: 5px;
28
  }
29
 
30
- .form {
31
- margin-top: 20px;
32
- }
 
 
 
33
 
34
- #content {
35
- width: 70%;
 
 
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

  • SHA256: 865c7e81ac633c6b1a7b9f59cd4779cc7fb13affe9a1d28707c7432ceeb3367b
  • Pointer size: 131 Bytes
  • Size of remote file: 218 kB
static/img/Logo Kampus Merdeka.png ADDED

Git LFS Details

  • SHA256: 2716c2d17922473d714fa9c04a2f62be99e98b55fa0bcc039e64c21e982b9405
  • Pointer size: 131 Bytes
  • Size of remote file: 109 kB
static/img/Logo Kemendikbud.png ADDED

Git LFS Details

  • SHA256: a0707d983ab66b85283db84c6486128881a84e473fc147d9bd673afad23e30d5
  • Pointer size: 131 Bytes
  • Size of remote file: 436 kB
static/img/Logo PNJ.png ADDED

Git LFS Details

  • SHA256: 53c90eabd58b43af9829786ab18e0fae1db662ac944d689338139d37bfa76eab
  • Pointer size: 131 Bytes
  • Size of remote file: 131 kB
static/img/Logo Vokasi Menguatkan.png ADDED

Git LFS Details

  • SHA256: a4ecaee5da2f468425fcc2521d19c7fcca039ad1907dc382b8ba71304d3ef6ea
  • Pointer size: 131 Bytes
  • Size of remote file: 116 kB
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
- <link rel="stylesheet" href="{{ url_for('static', filename='css/tailwind.css') }}">
 
 
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-5 text-5xl font-extrabold tracking-tight text-slate-900 sm:text-7xl text-center">Indonesian Lyrics
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-3 flex flex-col justify-center items-center rounded-lg bg-white shadow-xl shadow-black/5 ring-1 ring-slate-700/10">
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
- <div class="w-7 h-7"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
20
- stroke-width="1.5" stroke="currentColor">
21
- <path stroke-linecap="round" stroke-linejoin="round"
22
- 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">
23
- </path>
24
- </svg></div>
25
- <div class="ml-2 break-text text-center text-md w-30">From File</div>
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
- <div class="w-7 h-7"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
31
- stroke-width="1.5" stroke="currentColor">
32
- <path stroke-linecap="round" stroke-linejoin="round"
33
- 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">
34
- </path>
35
- </svg></div>
36
- <div id="recordText" class="ml-2 break-text text-center text-md w-30">Record</div>
37
  </button>
38
  </div>
39
- <div class="w-full bg-gray-200 rounded-full h-1 dark:bg-gray-700">
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-3 flex relative z-10 p-4 w-full hidden">
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-2xl px-5 py-2.5 text-center">
56
  Cancel
57
  </button>
58
  <button type="submit"
59
- class="text-white bg-blue-700 hover:bg-blue-800 font-medium rounded-lg text-2xl px-5 py-2.5 text-center">
60
  Predict Age Group
61
  </button>
62
  </form>
63
  </div>
64
 
65
  <!-- Text Input for Manual Lyrics Prediction -->
66
- <div class="mt-4 w-full mt-8" id="input-text">
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-2xl focus:outline-none focus:ring-2 focus:ring-blue-500"
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-2xl px-5 py-2.5 text-center">
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-5 text-gray-500">By Michael Natanael</div>
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-4 h-4 me-3 text-white animate-spin" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
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 shadow-lg p-8 text-center space-y-6">
10
 
11
  <!-- Lyrics -->
12
- <div class="mb-5">
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="my-12 h-0.5 border-t-0 bg-neutral-100 dark:bg-white/10" />
18
 
19
  <!-- Predicted Age Group -->
20
- <div class="mt-4 mb-5">
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="my-12 h-0.5 border-t-0 bg-neutral-100 dark:bg-white/10" />
28
 
29
  <!-- Class Probabilities -->
30
- <div class="mt-4 mb-5 flex flex-col text-center items-center">
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-600 text-1xl sm:text-lg {{ 'font-medium' if label == prediction else '' }}">{{ label.capitalize() }}</span>
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="my-12 h-0.5 border-t-0 bg-neutral-100 dark:bg-white/10" />
50
 
51
  <!-- Processing Time -->
52
  {% if total_time %}
53
- <div class="mt-4 mb-5 text-gray-500">
54
  ⏱️ Total Processing Time: <strong>{{ total_time }}</strong>
55
  </div>
56
  {% endif %}
57
 
58
  <!-- Action Button -->
59
- <div class="w-full flex flex-col my-2 overflow-y-auto"></div>
60
- <div class="mt-4">
61
- <a href="/" class="bg-blue-700 hover:bg-blue-800 text-white font-medium py-2 px-4 rounded-lg text-2xl">
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-5 text-gray-500">By Michael Natanael</div>
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 %}