Maftuuh1922 commited on
Commit
aa5c395
·
0 Parent(s):

Initial commit - BatikLens API v2 (clean)

Browse files
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.tflite filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ batik-env/
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ *.log
25.3 ADDED
File without changes
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.11 (TensorFlow compatible)
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y \
9
+ gcc \
10
+ g++ \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy requirements first (for caching)
14
+ COPY requirements.txt .
15
+
16
+ # Install Python dependencies
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy application files
20
+ COPY app.py .
21
+ COPY models/ models/
22
+
23
+ # Expose port
24
+ EXPOSE 5000
25
+
26
+ # Set environment variables
27
+ ENV FLASK_APP=app.py
28
+ ENV PYTHONUNBUFFERED=1
29
+
30
+ # Health check
31
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
32
+ CMD python -c "import requests; requests.get('http://localhost:5000/health')"
33
+
34
+ # Run with gunicorn
35
+ CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--timeout", "120", "app:app"]
Procfile ADDED
@@ -0,0 +1 @@
 
 
1
+ web: gunicorn --bind 0.0.0.0:$PORT --workers 1 --timeout 120 app_mobilenet:app
README.md ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🎯 Batik Classifier API
2
+
3
+ REST API untuk klasifikasi motif batik menggunakan **InceptionV3 + KNN** dengan akurasi **95%**.
4
+
5
+ ## 📊 Model Info
6
+ - **Accuracy**: 95.00%
7
+ - **Model**: InceptionV3 (feature extraction) + KNN (classification)
8
+ - **Classes**: 20 motif batik
9
+ - **Training Data**: 17,000 images
10
+
11
+ ## 🚀 Quick Start
12
+
13
+ ### 1. Setup Environment
14
+
15
+ ```bash
16
+ # Buat virtual environment
17
+ python -m venv venv
18
+ source venv/bin/activate # Linux/Mac
19
+ # atau
20
+ venv\Scripts\activate # Windows
21
+
22
+ # Install dependencies
23
+ pip install -r requirements.txt
24
+ ```
25
+
26
+ ### 2. Download Model Files
27
+
28
+ Download model files dari Google Drive dan letakkan di folder `models/`:
29
+ - `batik_knn_model_95acc.pkl`
30
+ - `batik_classes.pkl`
31
+ - `batik_model_metadata.pkl`
32
+
33
+ ```
34
+ batik-classifier/
35
+ └── api/
36
+ ├── app.py
37
+ ├── requirements.txt
38
+ ├── test_api.py
39
+ └── models/
40
+ ├── batik_knn_model_95acc.pkl
41
+ ├── batik_classes.pkl
42
+ └── batik_model_metadata.pkl
43
+ ```
44
+
45
+ ### 3. Run Server
46
+
47
+ ```bash
48
+ python app.py
49
+ ```
50
+
51
+ Server akan berjalan di: `http://localhost:5000`
52
+
53
+ ## 📋 API Endpoints
54
+
55
+ ### GET `/`
56
+ Get API information
57
+
58
+ **Response:**
59
+ ```json
60
+ {
61
+ "message": "Batik Classifier API",
62
+ "version": "1.0",
63
+ "model": "InceptionV3 + KNN",
64
+ "accuracy": "95.00%",
65
+ "classes": 20
66
+ }
67
+ ```
68
+
69
+ ### POST `/predict`
70
+ Predict batik motif from image
71
+
72
+ **Request:**
73
+ - Method: POST
74
+ - Content-Type: multipart/form-data
75
+ - Body: `image` (file)
76
+
77
+ **Example using curl:**
78
+ ```bash
79
+ curl -X POST http://localhost:5000/predict \
80
+ -F "image=@path/to/batik.jpg"
81
+ ```
82
+
83
+ **Example using Python:**
84
+ ```python
85
+ import requests
86
+
87
+ with open('batik.jpg', 'rb') as f:
88
+ files = {'image': f}
89
+ response = requests.post('http://localhost:5000/predict', files=files)
90
+ print(response.json())
91
+ ```
92
+
93
+ **Response:**
94
+ ```json
95
+ {
96
+ "success": true,
97
+ "prediction": "batik-parang",
98
+ "confidence": 0.95,
99
+ "percentage": "95.00%",
100
+ "top_5_predictions": [
101
+ {
102
+ "class": "batik-parang",
103
+ "confidence": 0.95,
104
+ "percentage": "95.00%"
105
+ },
106
+ {
107
+ "class": "batik-kawung",
108
+ "confidence": 0.03,
109
+ "percentage": "3.00%"
110
+ }
111
+ ]
112
+ }
113
+ ```
114
+
115
+ ### GET `/classes`
116
+ Get list of all batik classes
117
+
118
+ **Response:**
119
+ ```json
120
+ {
121
+ "success": true,
122
+ "total": 20,
123
+ "classes": [
124
+ "batik-bali",
125
+ "batik-betawi",
126
+ "batik-celup",
127
+ ...
128
+ ]
129
+ }
130
+ ```
131
+
132
+ ### GET `/info`
133
+ Get model information
134
+
135
+ **Response:**
136
+ ```json
137
+ {
138
+ "success": true,
139
+ "model_info": {
140
+ "accuracy": "95.00%",
141
+ "model_type": "InceptionV3 + KNN",
142
+ "n_classes": 20,
143
+ "total_training_data": 17000,
144
+ "trained_date": "2025-12-07 11:41:28"
145
+ }
146
+ }
147
+ ```
148
+
149
+ ### GET `/health`
150
+ Health check endpoint
151
+
152
+ **Response:**
153
+ ```json
154
+ {
155
+ "status": "healthy",
156
+ "model_loaded": true
157
+ }
158
+ ```
159
+
160
+ ## 🧪 Testing
161
+
162
+ Run test script:
163
+ ```bash
164
+ # Test endpoints
165
+ python test_api.py
166
+
167
+ # Test with image prediction
168
+ python test_api.py path/to/batik.jpg
169
+ ```
170
+
171
+ ## 📦 Deployment
172
+
173
+ ### Docker (Recommended)
174
+
175
+ ```dockerfile
176
+ FROM python:3.10-slim
177
+
178
+ WORKDIR /app
179
+
180
+ COPY requirements.txt .
181
+ RUN pip install --no-cache-dir -r requirements.txt
182
+
183
+ COPY . .
184
+
185
+ EXPOSE 5000
186
+
187
+ CMD ["python", "app.py"]
188
+ ```
189
+
190
+ Build and run:
191
+ ```bash
192
+ docker build -t batik-classifier-api .
193
+ docker run -p 5000:5000 batik-classifier-api
194
+ ```
195
+
196
+ ### Heroku
197
+
198
+ ```bash
199
+ # Login
200
+ heroku login
201
+
202
+ # Create app
203
+ heroku create batik-classifier-api
204
+
205
+ # Deploy
206
+ git push heroku main
207
+ ```
208
+
209
+ ### Railway / Render
210
+
211
+ 1. Connect GitHub repository
212
+ 2. Set build command: `pip install -r requirements.txt`
213
+ 3. Set start command: `python app.py`
214
+ 4. Deploy
215
+
216
+ ## 🔧 Configuration
217
+
218
+ Environment variables:
219
+ ```bash
220
+ FLASK_ENV=production # production or development
221
+ PORT=5000 # Server port
222
+ ```
223
+
224
+ ## 📝 20 Batik Classes
225
+
226
+ 1. batik-bali
227
+ 2. batik-betawi
228
+ 3. batik-celup
229
+ 4. batik-cendrawasih
230
+ 5. batik-ceplok
231
+ 6. batik-ciamis
232
+ 7. batik-garutan
233
+ 8. batik-gentongan
234
+ 9. batik-kawung
235
+ 10. batik-keraton
236
+ 11. batik-lasem
237
+ 12. batik-megamendung
238
+ 13. batik-parang
239
+ 14. batik-pekalongan
240
+ 15. batik-priangan
241
+ 16. batik-sekar
242
+ 17. batik-sidoluhur
243
+ 18. batik-sidomukti
244
+ 19. batik-sogan
245
+ 20. batik-tambal
246
+
247
+ ## 🤝 Support
248
+
249
+ For issues or questions:
250
+ - GitHub: https://github.com/Maftuuh1922/warisan-digital
251
+ - Email: rizkiuya12@gmail.com
252
+
253
+ ## 📄 License
254
+
255
+ MIT License - feel free to use for commercial or personal projects.
256
+
257
+ ---
258
+
259
+ **Made with ❤️ by Maftuuh1922**
app_mobilenet.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import numpy as np
4
+ # Using ai-edge-litert (Google's new TFLite runtime)
5
+ from ai_edge_litert.interpreter import Interpreter
6
+
7
+ from flask import Flask, request, jsonify
8
+ from flask_cors import CORS
9
+ from PIL import Image
10
+ import io
11
+
12
+ app = Flask(__name__)
13
+ CORS(app)
14
+
15
+ # --- KONFIGURASI ---
16
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
17
+ # Pastikan path ini sesuai struktur folder kamu
18
+ MODEL_PATH = os.path.join(BASE_DIR, 'models', 'batik_model.tflite')
19
+ CLASSES_PATH = os.path.join(BASE_DIR, 'models', 'batik_classes_mobilenet_ultimate.json')
20
+
21
+ print("==================================================")
22
+ print("🚀 MEMULAI BATIK CLASSIFIER (TFLITE ENGINE V2)")
23
+ print("⚡ Mode: ai-edge-litert Runtime (38 Batik Classes)")
24
+ print("==================================================")
25
+
26
+ # --- 1. LOAD MODEL TFLITE ---
27
+ if not os.path.exists(MODEL_PATH):
28
+ print(f"❌ ERROR: File model TFLite tidak ditemukan: {MODEL_PATH}")
29
+ exit()
30
+
31
+ try:
32
+ # Using ai-edge-litert Interpreter
33
+ interpreter = Interpreter(model_path=MODEL_PATH)
34
+ interpreter.allocate_tensors()
35
+
36
+ # Dapat info input/output
37
+ input_details = interpreter.get_input_details()
38
+ output_details = interpreter.get_output_details()
39
+
40
+ input_shape = input_details[0]['shape']
41
+ output_shape = output_details[0]['shape']
42
+ print(f"✅ Model TFLite V2 berhasil dimuat!")
43
+ print(f"📊 Input Shape: {input_shape}, Output Classes: {output_shape[1]}")
44
+ except Exception as e:
45
+ print(f"❌ Gagal load TFLite: {e}")
46
+ exit()
47
+
48
+ # --- 2. LOAD CLASSES ---
49
+ if not os.path.exists(CLASSES_PATH):
50
+ print(f"❌ ERROR: File label tidak ditemukan di {CLASSES_PATH}")
51
+ exit()
52
+
53
+ try:
54
+ with open(CLASSES_PATH, 'r') as f:
55
+ class_data = json.load(f)
56
+
57
+ # Logika parsing JSON kamu sudah bagus!
58
+ if isinstance(class_data, dict) and "classes" in class_data:
59
+ class_names = class_data["classes"]
60
+ elif isinstance(class_data, dict):
61
+ try:
62
+ sorted_keys = sorted(class_data.keys(), key=lambda x: int(x))
63
+ class_names = [class_data[k] for k in sorted_keys]
64
+ except ValueError:
65
+ class_names = list(class_data.values())
66
+ elif isinstance(class_data, list):
67
+ class_names = class_data
68
+ else:
69
+ raise ValueError("Format JSON tidak dikenali.")
70
+
71
+ print(f"✅ Berhasil memuat {len(class_names)} nama motif batik.")
72
+ # print(f"📝 Contoh: {class_names[:3]}...")
73
+
74
+ except Exception as e:
75
+ print(f"❌ Gagal membaca file JSON Classes: {e}")
76
+ exit()
77
+
78
+ # --- PREPROCESSING ---
79
+ def prepare_image(image, target_size=(224, 224)):
80
+ if image.mode != "RGB":
81
+ image = image.convert("RGB")
82
+ image = image.resize(target_size)
83
+ img_array = np.array(image, dtype=np.float32)
84
+
85
+ # Normalisasi (Pastikan saat training kamu juga dibagi 255.0)
86
+ img_array = img_array / 255.0
87
+
88
+ # Tambah dimensi batch
89
+ img_array = np.expand_dims(img_array, axis=0)
90
+ return img_array
91
+
92
+ @app.route('/', methods=['GET'])
93
+ def home():
94
+ return jsonify({
95
+ "status": "Online",
96
+ "mode": "ai-edge-litert Runtime (BatikLens V2)",
97
+ "model_version": "v2",
98
+ "classes_loaded": len(class_names)
99
+ })
100
+
101
+ @app.route('/predict', methods=['POST'])
102
+ def predict():
103
+ # Support both 'file' and 'image' field names
104
+ file = request.files.get('file') or request.files.get('image')
105
+
106
+ if not file:
107
+ return jsonify({"error": "No file part"}), 400
108
+
109
+ if file.filename == '':
110
+ return jsonify({"error": "No selected file"}), 400
111
+
112
+ try:
113
+ # 1. Baca & Proses Gambar
114
+ image = Image.open(io.BytesIO(file.read()))
115
+ input_data = prepare_image(image)
116
+
117
+ # 2. Masukkan data ke Interpreter
118
+ interpreter.set_tensor(input_details[0]['index'], input_data)
119
+
120
+ # 3. Jalankan Prediksi
121
+ interpreter.invoke()
122
+
123
+ # 4. Ambil Hasil
124
+ output_data = interpreter.get_tensor(output_details[0]['index'])
125
+ predictions = output_data[0]
126
+
127
+ # 5. Cari skor tertinggi
128
+ predicted_index = np.argmax(predictions)
129
+ predicted_label = class_names[predicted_index]
130
+ confidence = float(predictions[predicted_index])
131
+
132
+ # Get top 5 predictions
133
+ top_5_indices = np.argsort(predictions)[-5:][::-1]
134
+ top_5_predictions = [
135
+ {
136
+ "class": class_names[idx],
137
+ "confidence": float(predictions[idx]),
138
+ "percentage": f"{float(predictions[idx]):.2%}"
139
+ }
140
+ for idx in top_5_indices
141
+ ]
142
+
143
+ return jsonify({
144
+ "success": True,
145
+ "prediction": predicted_label,
146
+ "confidence": confidence,
147
+ "percentage": f"{confidence:.2%}",
148
+ "top_5_predictions": top_5_predictions
149
+ })
150
+
151
+ except Exception as e:
152
+ return jsonify({"success": False, "error": str(e)}), 500
153
+
154
+ if __name__ == '__main__':
155
+ app.run(host='0.0.0.0', port=5000)
models/batik_classes_mobilenet_ultimate.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "classes": [
3
+ "batik-aceh",
4
+ "batik-bali",
5
+ "batik-bali_barong",
6
+ "batik-bali_merak",
7
+ "batik-betawi",
8
+ "batik-celup",
9
+ "batik-ceplok",
10
+ "batik-ciamis",
11
+ "batik-corak_insang",
12
+ "batik-garutan",
13
+ "batik-gentongan",
14
+ "batik-ikat_celup",
15
+ "batik-jakarta_ondel_ondel",
16
+ "batik-jawa_barat_megamendung",
17
+ "batik-jawa_timur_pring",
18
+ "batik-kalimantan_dayak",
19
+ "batik-keraton",
20
+ "batik-lampung_gajah",
21
+ "batik-lasem",
22
+ "batik-madura_mataketeran",
23
+ "batik-maluku_pala",
24
+ "batik-ntb_lumbung",
25
+ "batik-papua_asmat",
26
+ "batik-papua_cendrawasih",
27
+ "batik-papua_tifa",
28
+ "batik-pekalongan",
29
+ "batik-priangan",
30
+ "batik-sekar",
31
+ "batik-sidoluhur",
32
+ "batik-sidomukti",
33
+ "batik-sogan",
34
+ "batik-solo_parang",
35
+ "batik-sulawesi_selatan_lontara",
36
+ "batik-sumatera_barat_rumah_minang",
37
+ "batik-sumatera_utara_boraspati",
38
+ "batik-tambal",
39
+ "batik-yogyakarta_kawung",
40
+ "batik-yogyakarta_parang"
41
+ ],
42
+ "class_counts": {
43
+ "batik-aceh": 80,
44
+ "batik-bali": 100,
45
+ "batik-bali_barong": 88,
46
+ "batik-bali_merak": 94,
47
+ "batik-betawi": 98,
48
+ "batik-celup": 100,
49
+ "batik-ceplok": 92,
50
+ "batik-ciamis": 102,
51
+ "batik-corak_insang": 156,
52
+ "batik-garutan": 106,
53
+ "batik-gentongan": 96,
54
+ "batik-ikat_celup": 134,
55
+ "batik-jakarta_ondel_ondel": 86,
56
+ "batik-jawa_barat_megamendung": 432,
57
+ "batik-jawa_timur_pring": 80,
58
+ "batik-kalimantan_dayak": 460,
59
+ "batik-keraton": 150,
60
+ "batik-lampung_gajah": 160,
61
+ "batik-lasem": 150,
62
+ "batik-madura_mataketeran": 156,
63
+ "batik-maluku_pala": 158,
64
+ "batik-ntb_lumbung": 160,
65
+ "batik-papua_asmat": 156,
66
+ "batik-papua_cendrawasih": 256,
67
+ "batik-papua_tifa": 156,
68
+ "batik-pekalongan": 150,
69
+ "batik-priangan": 150,
70
+ "batik-sekar": 141,
71
+ "batik-sidoluhur": 150,
72
+ "batik-sidomukti": 138,
73
+ "batik-sogan": 150,
74
+ "batik-solo_parang": 260,
75
+ "batik-sulawesi_selatan_lontara": 160,
76
+ "batik-sumatera_barat_rumah_minang": 160,
77
+ "batik-sumatera_utara_boraspati": 160,
78
+ "batik-tambal": 150,
79
+ "batik-yogyakarta_kawung": 250,
80
+ "batik-yogyakarta_parang": 160
81
+ }
82
+ }
models/batik_config_mobilenet_ultimate.json ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "img_size": 224,
3
+ "classes": [
4
+ "batik-aceh",
5
+ "batik-bali",
6
+ "batik-bali_barong",
7
+ "batik-bali_merak",
8
+ "batik-betawi",
9
+ "batik-celup",
10
+ "batik-ceplok",
11
+ "batik-ciamis",
12
+ "batik-corak_insang",
13
+ "batik-garutan",
14
+ "batik-gentongan",
15
+ "batik-ikat_celup",
16
+ "batik-jakarta_ondel_ondel",
17
+ "batik-jawa_barat_megamendung",
18
+ "batik-jawa_timur_pring",
19
+ "batik-kalimantan_dayak",
20
+ "batik-keraton",
21
+ "batik-lampung_gajah",
22
+ "batik-lasem",
23
+ "batik-madura_mataketeran",
24
+ "batik-maluku_pala",
25
+ "batik-ntb_lumbung",
26
+ "batik-papua_asmat",
27
+ "batik-papua_cendrawasih",
28
+ "batik-papua_tifa",
29
+ "batik-pekalongan",
30
+ "batik-priangan",
31
+ "batik-sekar",
32
+ "batik-sidoluhur",
33
+ "batik-sidomukti",
34
+ "batik-sogan",
35
+ "batik-solo_parang",
36
+ "batik-sulawesi_selatan_lontara",
37
+ "batik-sumatera_barat_rumah_minang",
38
+ "batik-sumatera_utara_boraspati",
39
+ "batik-tambal",
40
+ "batik-yogyakarta_kawung",
41
+ "batik-yogyakarta_parang"
42
+ ],
43
+ "num_classes": 38,
44
+ "accuracy": 0.9180107712745667,
45
+ "top3": 0.961693525314331,
46
+ "top5": 0.9704301357269287,
47
+ "loss": 1.1378017663955688,
48
+ "model": "MobileNetV2",
49
+ "training_strategy": "Two-stage (frozen + fine-tuning)",
50
+ "stage1_epochs": 30,
51
+ "stage2_epochs": 40,
52
+ "total_time_minutes": 151.78491247495015,
53
+ "class_counts": {
54
+ "batik-aceh": 80,
55
+ "batik-bali": 100,
56
+ "batik-bali_barong": 88,
57
+ "batik-bali_merak": 94,
58
+ "batik-betawi": 98,
59
+ "batik-celup": 100,
60
+ "batik-ceplok": 92,
61
+ "batik-ciamis": 102,
62
+ "batik-corak_insang": 156,
63
+ "batik-garutan": 106,
64
+ "batik-gentongan": 96,
65
+ "batik-ikat_celup": 134,
66
+ "batik-jakarta_ondel_ondel": 86,
67
+ "batik-jawa_barat_megamendung": 432,
68
+ "batik-jawa_timur_pring": 80,
69
+ "batik-kalimantan_dayak": 460,
70
+ "batik-keraton": 150,
71
+ "batik-lampung_gajah": 160,
72
+ "batik-lasem": 150,
73
+ "batik-madura_mataketeran": 156,
74
+ "batik-maluku_pala": 158,
75
+ "batik-ntb_lumbung": 160,
76
+ "batik-papua_asmat": 156,
77
+ "batik-papua_cendrawasih": 256,
78
+ "batik-papua_tifa": 156,
79
+ "batik-pekalongan": 150,
80
+ "batik-priangan": 150,
81
+ "batik-sekar": 141,
82
+ "batik-sidoluhur": 150,
83
+ "batik-sidomukti": 138,
84
+ "batik-sogan": 150,
85
+ "batik-solo_parang": 260,
86
+ "batik-sulawesi_selatan_lontara": 160,
87
+ "batik-sumatera_barat_rumah_minang": 160,
88
+ "batik-sumatera_utara_boraspati": 160,
89
+ "batik-tambal": 150,
90
+ "batik-yogyakarta_kawung": 250,
91
+ "batik-yogyakarta_parang": 160
92
+ }
93
+ }
models/batik_model.tflite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d01ed7935c58f2dcc30b5222f9690f6f42162aa0b0e07d465ad78260971f09e3
3
+ size 2848432
package-lock.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "name": "api",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {}
6
+ }
railway.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "build": {
3
+ "builder": "nixpacks"
4
+ },
5
+ "deploy": {
6
+ "numReplicas": 1,
7
+ "sleepApplication": false,
8
+ "restartPolicyType": "always"
9
+ }
10
+ }
render.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ - type: web
3
+ name: batiklens-api
4
+ env: python
5
+ region: singapore
6
+ plan: free
7
+ buildCommand: pip install -r requirements.txt
8
+ startCommand: gunicorn --bind 0.0.0.0:$PORT --workers 1 --timeout 120 app_mobilenet:app
9
+ envVars:
10
+ - key: PYTHON_VERSION
11
+ value: "3.10.12"
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask==3.0.0
2
+ flask-cors==4.0.0
3
+ ai-edge-litert>=1.0.0
4
+ pillow>=10.0.0
5
+ numpy>=1.26.0
6
+ gunicorn==21.2.0
requirements_mobilenet.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask==3.0.0
2
+ flask-cors==4.0.0
3
+ tensorflow-cpu==2.15.0
4
+ pillow==10.2.0
5
+ numpy==1.26.4
6
+ gunicorn==21.2.0
test_web.html ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Batik Classifier - Test Web Interface</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ justify-content: center;
20
+ align-items: center;
21
+ padding: 20px;
22
+ }
23
+
24
+ .container {
25
+ background: white;
26
+ border-radius: 20px;
27
+ padding: 40px;
28
+ max-width: 600px;
29
+ width: 100%;
30
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
31
+ }
32
+
33
+ h1 {
34
+ text-align: center;
35
+ color: #333;
36
+ margin-bottom: 10px;
37
+ }
38
+
39
+ .subtitle {
40
+ text-align: center;
41
+ color: #666;
42
+ margin-bottom: 30px;
43
+ }
44
+
45
+ .upload-area {
46
+ border: 3px dashed #667eea;
47
+ border-radius: 10px;
48
+ padding: 40px;
49
+ text-align: center;
50
+ cursor: pointer;
51
+ transition: all 0.3s;
52
+ margin-bottom: 20px;
53
+ }
54
+
55
+ .upload-area:hover {
56
+ border-color: #764ba2;
57
+ background: #f8f9ff;
58
+ }
59
+
60
+ .upload-area.dragover {
61
+ background: #e8ecff;
62
+ border-color: #5568d3;
63
+ }
64
+
65
+ input[type="file"] {
66
+ display: none;
67
+ }
68
+
69
+ .upload-icon {
70
+ font-size: 48px;
71
+ margin-bottom: 10px;
72
+ }
73
+
74
+ .btn {
75
+ width: 100%;
76
+ padding: 15px;
77
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
78
+ color: white;
79
+ border: none;
80
+ border-radius: 10px;
81
+ font-size: 16px;
82
+ font-weight: bold;
83
+ cursor: pointer;
84
+ transition: transform 0.2s;
85
+ }
86
+
87
+ .btn:hover {
88
+ transform: translateY(-2px);
89
+ }
90
+
91
+ .btn:disabled {
92
+ opacity: 0.6;
93
+ cursor: not-allowed;
94
+ }
95
+
96
+ .preview {
97
+ text-align: center;
98
+ margin: 20px 0;
99
+ }
100
+
101
+ .preview img {
102
+ max-width: 100%;
103
+ max-height: 300px;
104
+ border-radius: 10px;
105
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
106
+ }
107
+
108
+ .result {
109
+ margin-top: 20px;
110
+ padding: 20px;
111
+ background: #f8f9ff;
112
+ border-radius: 10px;
113
+ display: none;
114
+ }
115
+
116
+ .result.show {
117
+ display: block;
118
+ }
119
+
120
+ .prediction {
121
+ font-size: 24px;
122
+ font-weight: bold;
123
+ color: #667eea;
124
+ margin-bottom: 10px;
125
+ }
126
+
127
+ .confidence {
128
+ font-size: 18px;
129
+ color: #666;
130
+ margin-bottom: 20px;
131
+ }
132
+
133
+ .top-predictions {
134
+ margin-top: 20px;
135
+ }
136
+
137
+ .top-predictions h3 {
138
+ margin-bottom: 10px;
139
+ color: #333;
140
+ }
141
+
142
+ .pred-item {
143
+ display: flex;
144
+ justify-content: space-between;
145
+ padding: 10px;
146
+ background: white;
147
+ border-radius: 5px;
148
+ margin-bottom: 5px;
149
+ }
150
+
151
+ .loading {
152
+ text-align: center;
153
+ display: none;
154
+ }
155
+
156
+ .loading.show {
157
+ display: block;
158
+ }
159
+
160
+ .spinner {
161
+ border: 4px solid #f3f3f3;
162
+ border-top: 4px solid #667eea;
163
+ border-radius: 50%;
164
+ width: 40px;
165
+ height: 40px;
166
+ animation: spin 1s linear infinite;
167
+ margin: 20px auto;
168
+ }
169
+
170
+ @keyframes spin {
171
+ 0% { transform: rotate(0deg); }
172
+ 100% { transform: rotate(360deg); }
173
+ }
174
+
175
+ .error {
176
+ color: #e74c3c;
177
+ background: #ffe8e8;
178
+ padding: 15px;
179
+ border-radius: 10px;
180
+ margin-top: 20px;
181
+ display: none;
182
+ }
183
+
184
+ .error.show {
185
+ display: block;
186
+ }
187
+ </style>
188
+ </head>
189
+ <body>
190
+ <div class="container">
191
+ <h1>🎨 Batik Classifier</h1>
192
+ <p class="subtitle">AI-Powered Batik Motif Recognition (95% Accuracy)</p>
193
+
194
+ <div class="upload-area" id="uploadArea">
195
+ <div class="upload-icon">📸</div>
196
+ <p><strong>Click to upload</strong> or drag and drop</p>
197
+ <p style="font-size: 14px; color: #999; margin-top: 5px;">PNG, JPG up to 10MB</p>
198
+ <input type="file" id="fileInput" accept="image/*">
199
+ </div>
200
+
201
+ <div class="preview" id="preview"></div>
202
+
203
+ <button class="btn" id="predictBtn" disabled>🔍 Analyze Batik</button>
204
+
205
+ <div class="loading" id="loading">
206
+ <div class="spinner"></div>
207
+ <p>Analyzing image...</p>
208
+ </div>
209
+
210
+ <div class="error" id="error"></div>
211
+
212
+ <div class="result" id="result">
213
+ <div class="prediction">
214
+ <span id="predictionText"></span>
215
+ </div>
216
+ <div class="confidence">
217
+ Confidence: <strong id="confidenceText"></strong>
218
+ </div>
219
+ <div class="top-predictions">
220
+ <h3>📊 Top 5 Predictions:</h3>
221
+ <div id="topPredictions"></div>
222
+ </div>
223
+ </div>
224
+ </div>
225
+
226
+ <script>
227
+ const API_URL = 'http://192.168.0.106:5000';
228
+
229
+ const uploadArea = document.getElementById('uploadArea');
230
+ const fileInput = document.getElementById('fileInput');
231
+ const predictBtn = document.getElementById('predictBtn');
232
+ const preview = document.getElementById('preview');
233
+ const loading = document.getElementById('loading');
234
+ const result = document.getElementById('result');
235
+ const error = document.getElementById('error');
236
+
237
+ let selectedFile = null;
238
+
239
+ // Click to upload
240
+ uploadArea.addEventListener('click', () => fileInput.click());
241
+
242
+ // File selection
243
+ fileInput.addEventListener('change', (e) => {
244
+ const file = e.target.files[0];
245
+ if (file) {
246
+ handleFile(file);
247
+ }
248
+ });
249
+
250
+ // Drag and drop
251
+ uploadArea.addEventListener('dragover', (e) => {
252
+ e.preventDefault();
253
+ uploadArea.classList.add('dragover');
254
+ });
255
+
256
+ uploadArea.addEventListener('dragleave', () => {
257
+ uploadArea.classList.remove('dragover');
258
+ });
259
+
260
+ uploadArea.addEventListener('drop', (e) => {
261
+ e.preventDefault();
262
+ uploadArea.classList.remove('dragover');
263
+ const file = e.dataTransfer.files[0];
264
+ if (file && file.type.startsWith('image/')) {
265
+ handleFile(file);
266
+ }
267
+ });
268
+
269
+ // Handle file
270
+ function handleFile(file) {
271
+ selectedFile = file;
272
+
273
+ // Show preview
274
+ const reader = new FileReader();
275
+ reader.onload = (e) => {
276
+ preview.innerHTML = `<img src="${e.target.result}" alt="Preview">`;
277
+ };
278
+ reader.readAsDataURL(file);
279
+
280
+ // Enable predict button
281
+ predictBtn.disabled = false;
282
+
283
+ // Hide previous results
284
+ result.classList.remove('show');
285
+ error.classList.remove('show');
286
+ }
287
+
288
+ // Predict
289
+ predictBtn.addEventListener('click', async () => {
290
+ if (!selectedFile) return;
291
+
292
+ // Show loading
293
+ loading.classList.add('show');
294
+ result.classList.remove('show');
295
+ error.classList.remove('show');
296
+ predictBtn.disabled = true;
297
+
298
+ try {
299
+ const formData = new FormData();
300
+ formData.append('image', selectedFile);
301
+
302
+ const response = await fetch(`${API_URL}/predict`, {
303
+ method: 'POST',
304
+ body: formData
305
+ });
306
+
307
+ const data = await response.json();
308
+
309
+ if (data.success) {
310
+ // Show result
311
+ document.getElementById('predictionText').textContent = data.prediction;
312
+ document.getElementById('confidenceText').textContent = data.percentage;
313
+
314
+ const topPredDiv = document.getElementById('topPredictions');
315
+ topPredDiv.innerHTML = data.top_5_predictions.map((pred, idx) => `
316
+ <div class="pred-item">
317
+ <span>${idx + 1}. ${pred.class}</span>
318
+ <strong>${pred.percentage}</strong>
319
+ </div>
320
+ `).join('');
321
+
322
+ result.classList.add('show');
323
+ } else {
324
+ error.textContent = data.error || 'Prediction failed';
325
+ error.classList.add('show');
326
+ }
327
+ } catch (err) {
328
+ error.textContent = `Error: ${err.message}. Make sure the API server is running.`;
329
+ error.classList.add('show');
330
+ } finally {
331
+ loading.classList.remove('show');
332
+ predictBtn.disabled = false;
333
+ }
334
+ });
335
+ </script>
336
+ </body>
337
+ </html>