SwiMate membantu perenang memantau performa latihan secara real-time menggunakan GPS, sensor gerak (accelerometer + gyroscope), dan model machine learning untuk mendeteksi gaya renang. README ini diperbarui untuk memberikan penjelasan mendetail mengenai arsitektur frontend (Flutter), backend (Supabase / PostgreSQL) dan pipeline machine learning (training → TFLite → inference), lengkap dengan contoh, checklist dan langkah debugging yang umum dijumpai saat ujian.
Video demo: https://drive.google.com/file/d/1MjcQMe2_oCaYc2Xd8yo435FslvocRyuB/view?usp=sharing
- Menghitung pace (real-time) menggunakan GPS dan stopwatch
- Deteksi stroke otomatis dari sensor gerak
- Estimasi kalori berbasis jarak, durasi, dan stroke
- Analisis efisiensi renang (Stroke Rate, Stroke Length, SWOLF)
- Deteksi gaya renang (Freestyle, Backstroke, Breaststroke, Butterfly) menggunakan model ML
- Penyimpanan sesi & breakdown gaya per sesi ke Supabase (PostgreSQL)
Frontend (Flutter) ⇄ Supabase SDK (supabase_flutter) ⇄ Supabase API (HTTP) ⇄ PostgreSQL (tabel: users, activity_table, activity_segments)
- Autentikasi: Supabase Auth (JWT) — client menerima session (access + refresh token) melalui SDK.
- Database: PostgreSQL yang dikelola Supabase. Gunakan Row Level Security (RLS) untuk membatasi akses berdasarkan
auth.uid(). - ML: Model dilatih di folder
machine_learning/(Keras). Hasil training diekspor ke.h5dan dikonversi ke.tfliteuntuk digunakan di aplikasi.
-
applications/— kode aplikasi Flutter (source utama:applications/lib/).applications/lib/main.dart— entry point app (pastikan Supabase di-initialize di sini).applications/lib/pages/— screens (signin, signup, record, history, age/height/weight, dll).applications/lib/services/— services sepertiactivity_service.dart(query Supabase) danswimming_style_detector.dart(tflite inference wrapper).applications/assets/models/— model tflite dan (opsional)labels.json/labels.npyyang dipaket ke app.
-
machine_learning/— skrip training, dataset, dan model hasil pelatihan (model.h5,labels.npy,runs/).swimate_train.py,swimate_train_with_unknown.py,swimate_train_default.py— skrip pelatihan dengan variasi augmentasi/setting.dataset/— CSV sumber per gaya renang.runs/&runs_no_aug/— artifacts pelatihan (model.h5, tflite, labels.npy).
- Inisialisasi Supabase
Sebelum kode lain yang mengakses Supabase.instance.client, inisialisasi Supabase harus dipanggil di main.dart:
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'https://<PROJECT_REF>.supabase.co',
anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'),
);
runApp(const SwimScienceApp());
}- Gunakan environment variables atau CI secrets (mis.
flutter build --dart-define=SUPABASE_ANON_KEY=xxx) untuk menghindari menyimpan kunci di repo.
- Auth flow (ringkas)
- Login email/password:
supabase.auth.signInWithPassword(email:..., password:...). - OAuth (Google): dapat menggunakan
signInWithIdTokensetelah mendapatkan idToken dari Google SDK. - SDK menyimpan session; SDK otomatis menyertakan Authorization header pada request ke API Supabase.
- Contoh CRUD (Dart)
- Ambil semua aktivitas user (history):
final userId = Supabase.instance.client.auth.currentUser?.id;
final res = await Supabase.instance.client
.from('activity_table')
.select('*')
.eq('user_id', userId)
.order('started_at', ascending: false);
// response biasanya List<Map<String,dynamic>>- Simpan activity dan segments (non-atomic example):
final insertRes = await Supabase.instance.client
.from('activity_table')
.insert({
'user_id': userId,
'started_at': DateTime.now().toIso8601String(),
'distance_m': 0,
})
.select()
.single();
final activityId = insertRes['id'];
await Supabase.instance.client.from('activity_segments').insert({
'activity_id': activityId,
'start_offset_seconds': 0,
'end_offset_seconds': 30,
'stroke_type': 'freestyle',
'confidence': 0.92,
});- Catatan: operasi multi-table di client tidak bersifat atomik — untuk atomicity buat RPC (Postgres function) di Supabase.
- Best-practices frontend
- Debounce/throttle penulisan ke DB (mis. saat user scroll untuk mengubah
age) — hindari terlalu banyak request. - Tangani tipe data dengan hati-hati: API bisa mengembalikan jenis yang berbeda tergantung versi SDK (
MapatauPostgrestResponse). Gunakan safe-casts. - Pastikan nama kolom di query sesuai dengan skema DB (
user_idvsid_user).
- Skema tabel (contoh)
-
users/profiles(terdiri dari data user tambahan):iduuid PRIMARY KEY (supabase auth UID)emailtextusernametextheightnumericweightnumericcreated_attimestamptz
-
activity_table:iduuid PRIMARY KEYuser_iduuid REFERENCES profiles(id)started_attimestamptzended_attimestamptzdistance_mnumericcaloriesnumericstrokes_countinttimestamptimestamptz -- (opsional) record created timestamp
-
activity_segments:iduuid PRIMARY KEYactivity_iduuid REFERENCES activity_table(id)start_offset_secondsnumericend_offset_secondsnumericstroke_typetextconfidencenumericcreated_attimestamptz
- Row Level Security (RLS) dan policy
- Supabase memberikan kemampuan RLS — sangat penting untuk aplikasi yang memanggil DB langsung dari client.
- Contoh kebijakan minimal (ganti
user_idsesuai kolom Anda):
ALTER TABLE public.activity_table ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can select own activities"
ON public.activity_table
FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own activities"
ON public.activity_table
FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own activities"
ON public.activity_table
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own activities"
ON public.activity_table
FOR DELETE
USING (auth.uid() = user_id);- Aktifkan kebijakan serupa untuk
activity_segmentsdanprofiles(jika perlu).
- Atomic insert (activity + segments)
- Buat fungsi PL/pgSQL (RPC) yang melakukan insert activity dan segments dalam satu transaksi, lalu panggil via
supabase.rpc('create_activity_with_segments', params)dari client.
Contoh ringkas:
CREATE FUNCTION public.create_activity_with_segments(p_activity json, p_segments json)
RETURNS json AS $$
DECLARE new_activity_id uuid;
BEGIN
INSERT INTO activity_table (user_id, started_at, distance_m)
VALUES ((p_activity->>'user_id')::uuid, (p_activity->>'started_at')::timestamptz, (p_activity->>'distance_m')::numeric)
RETURNING id INTO new_activity_id;
INSERT INTO activity_segments (activity_id, start_offset_seconds, end_offset_seconds, stroke_type, confidence)
SELECT new_activity_id, (s->>'start_offset_seconds')::numeric, (s->>'end_offset_seconds')::numeric, s->>'stroke_type', (s->>'confidence')::numeric
FROM json_array_elements(p_segments) AS s;
RETURN json_build_object('activity_id', new_activity_id);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;- Security notes
- Jangan simpan
service_rolekey di client. Gunakan hanyaanonKeyuntuk client. - Validasi input di DB (constraints) untuk pertahanan lapis kedua.
- Data & preprocessing
- Dataset CSV ada di
machine_learning/dataset/(file per gaya). Setiap CSV berisi kolom sensor sepertiaccel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_zdan label. - Preprocessing utama di skrip training:
- Windowing (mis. SAMPLING_HZ=10, WINDOW_SEC=4 → window_size=40 sampel)
- Stride (overlap)
- Normalisasi/standardisasi (pakai mean/std dari training set — simpan stat jika perlu)
- Augmentasi (opsional): gaussian noise, magnitude scaling, axis scaling, random dropout
- Training
- Skrip:
swimate_train.py,swimate_train_with_unknown.py,swimate_train_default.py. - Model: Transformer-like architecture (MultiHeadAttention, LayerNorm, FeedForward, GlobalAvgPool). Output: softmax K-class.
- Training menyimpan
labels.npy(urutan kelas) dan model.h5di folderruns/.
- Evaluasi
- Confusion matrix disimpan/ditampilkan: baris = ground truth, kolom = prediksi.
- Perhatikan metrik per kelas (accuracy, precision, recall, F1) dan latency inference untuk deployment.
- Export ke TFLite
- Konversi
.h5→.tflite(contoh):
import tensorflow as tf
model = tf.keras.models.load_model('swimate_model.h5')
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
open('swimate_model.tflite', 'wb').write(tflite_model)- Pertimbangkan quantization (post-training) untuk mengurangi ukuran dan latency.
- Integrasi ke Flutter
- Salin
swimate_model.tflitedanlabels.npy/labels.jsonkeapplications/assets/models/. - Tambahkan assets di
applications/pubspec.yaml(pastikan path benar). - Di runtime,
swimming_style_detector.dartmemuat model viatflite_flutter, menyiapkan input tensor sesuai shape, menjalankan interpreter, lalu memetakan indeks ke label.
- Hal-hal penting untuk inference
- Sampling rate dan window size harus sama dengan setting saat training.
- Preprocessing (order channel, normalization) harus identik.
- Labels ordering harus sinkron (simpan
labels.npyyang sama dengan model) — jika tidak, hasil label akan keliru.
Frontend (Flutter):
cd applications
flutter pub get
# Jalankan di device/emulator
flutter run
# Analisis kode statis
flutter analyze
# Build release (Android)
flutter build apk --releaseMachine learning (Python):
cd machine_learning
python3 -m venv .venv
source .venv/bin/activate.fish
pip install -r requirements.txt # jika tersedia
python swimate_train_default.pyJika hasil training menghasilkan runs/<timestamp>/swimate_model.h5 dan .tflite, salin .tflite ke applications/assets/models/ dan tambahkan path di applications/pubspec.yaml.