RimsJ commited on
Commit
6a441bb
·
verified ·
1 Parent(s): 707e134
Files changed (8) hide show
  1. .gitattributes +2 -32
  2. .gitignore +38 -0
  3. app.py +215 -0
  4. app_gradio.py +336 -0
  5. dockerfile +31 -0
  6. labels.txt +111 -0
  7. model_config.json +151 -0
  8. requirements.txt +9 -0
.gitattributes CHANGED
@@ -1,35 +1,5 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
  *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz 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
 
1
+ *.pth filter=lfs diff=lfs merge=lfs -text
2
+ *.pt filter=lfs diff=lfs merge=lfs -text
3
  *.bin filter=lfs diff=lfs merge=lfs -text
 
 
 
 
4
  *.h5 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
5
  *.pb filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ venv/
8
+ env/
9
+ ENV/
10
+ .venv
11
+
12
+ # Jupyter
13
+ .ipynb_checkpoints/
14
+ *.ipynb
15
+
16
+ # IDEs
17
+ .vscode/
18
+ .idea/
19
+ *.swp
20
+ *.swo
21
+ *~
22
+
23
+ # OS
24
+ .DS_Store
25
+ Thumbs.db
26
+
27
+ # Logs
28
+ *.log
29
+
30
+ # Local testing
31
+ test_*.py
32
+ temp/
33
+ tmp/
34
+
35
+ # Don't ignore models and config (needed for deployment)
36
+ !models/
37
+ !model_config.json
38
+ !labels.txt
app.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio UI application for Batik Classification using VGG16 model
3
+ Optimized for Hugging Face Spaces deployment
4
+ """
5
+ import gradio as gr
6
+ import torch
7
+ import torch.nn as nn
8
+ from torchvision import transforms, models
9
+ from PIL import Image
10
+ import json
11
+ import numpy as np
12
+ from typing import Tuple, Dict
13
+
14
+ # Global variables
15
+ model = None
16
+ class_names = []
17
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
18
+ transform = None
19
+
20
+
21
+ def load_model():
22
+ """Load VGG16 model and configuration"""
23
+ global model, class_names, transform
24
+
25
+ try:
26
+ # Load model configuration
27
+ with open('model_config.json', 'r') as f:
28
+ config = json.load(f)
29
+
30
+ num_classes = config['num_classes']
31
+ class_names = config['class_names']
32
+ image_size = config.get('image_size', 224)
33
+
34
+ # Initialize VGG16 model
35
+ model = models.vgg16(weights=None)
36
+ # Modify classifier to match saved model architecture
37
+ model.classifier[3] = nn.Linear(4096, num_classes)
38
+ model.classifier = nn.Sequential(*list(model.classifier.children())[:4])
39
+
40
+ # Load trained weights
41
+ checkpoint = torch.load('models/vgg16_batik_best.pth', map_location=device)
42
+
43
+ # Extract state_dict
44
+ if isinstance(checkpoint, dict) and 'model_state_dict' in checkpoint:
45
+ state_dict = checkpoint['model_state_dict']
46
+ else:
47
+ state_dict = checkpoint
48
+
49
+ # Remove '_orig_mod.' prefix if present
50
+ new_state_dict = {}
51
+ for key, value in state_dict.items():
52
+ if key.startswith('_orig_mod.'):
53
+ new_key = key.replace('_orig_mod.', '')
54
+ new_state_dict[new_key] = value
55
+ else:
56
+ new_state_dict[key] = value
57
+
58
+ model.load_state_dict(new_state_dict)
59
+ model = model.to(device)
60
+ model.eval()
61
+
62
+ # Define image preprocessing
63
+ transform = transforms.Compose([
64
+ transforms.Resize((image_size, image_size)),
65
+ transforms.ToTensor(),
66
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
67
+ std=[0.229, 0.224, 0.225])
68
+ ])
69
+
70
+ print(f"✅ Model loaded successfully on {device}")
71
+ print(f"📊 Number of classes: {num_classes}")
72
+
73
+ except Exception as e:
74
+ print(f"❌ Error loading model: {str(e)}")
75
+ raise
76
+
77
+
78
+ def predict_image(image: Image.Image) -> Tuple[Dict, str]:
79
+ """
80
+ Predict batik class from image
81
+
82
+ Args:
83
+ image: PIL Image
84
+
85
+ Returns:
86
+ Tuple of (top_k_dict, formatted_text)
87
+ """
88
+ try:
89
+ if image is None:
90
+ return {}, "❌ Silakan upload gambar batik terlebih dahulu"
91
+
92
+ # Convert to RGB if needed
93
+ if image.mode != 'RGB':
94
+ image = image.convert('RGB')
95
+
96
+ # Transform and predict
97
+ input_tensor = transform(image).unsqueeze(0).to(device)
98
+
99
+ with torch.no_grad():
100
+ outputs = model(input_tensor)
101
+ probabilities = torch.nn.functional.softmax(outputs, dim=1)
102
+ top_probs, top_indices = torch.topk(probabilities, min(5, len(class_names)), dim=1)
103
+
104
+ # Get top prediction
105
+ predicted_class = class_names[top_indices[0][0].item()]
106
+ confidence = top_probs[0][0].item() * 100
107
+
108
+ # Format top-5 results
109
+ results = {}
110
+ for i in range(min(5, len(class_names))):
111
+ class_name = class_names[top_indices[0][i].item()]
112
+ conf = top_probs[0][i].item()
113
+ results[class_name] = float(conf)
114
+
115
+ # Format output text
116
+ result_text = f"""
117
+ ## 🎯 Hasil Prediksi
118
+
119
+ **Motif Batik:** `{predicted_class}`
120
+ **Confidence:** `{confidence:.2f}%`
121
+
122
+ ---
123
+
124
+ ### 📊 Top 5 Prediksi:
125
+ """
126
+
127
+ for idx, (class_name, conf) in enumerate(list(results.items())[:5], 1):
128
+ bar = "█" * int(conf * 20)
129
+ result_text += f"\n{idx}. **{class_name}** - {conf*100:.2f}% \n {bar}"
130
+
131
+ return results, result_text
132
+
133
+ except Exception as e:
134
+ return {}, f"❌ Error: {str(e)}"
135
+
136
+
137
+ # Load model at startup
138
+ print("🔄 Loading model...")
139
+ load_model()
140
+ print("✅ Model ready!")
141
+
142
+ # Create Gradio interface
143
+ with gr.Blocks(
144
+ title="Batik Classification - VGG16",
145
+ theme=gr.themes.Soft(),
146
+ css=".gradio-container {max-width: 1200px; margin: auto;}"
147
+ ) as demo:
148
+
149
+ gr.Markdown("""
150
+ # 🎨 Klasifikasi Motif Batik Indonesia
151
+ ### Menggunakan Model VGG16 Deep Learning
152
+
153
+ Upload gambar batik untuk mengetahui motif dan asalnya!
154
+ **Total 111 motif batik** dari berbagai daerah di Indonesia 🇮🇩
155
+ """)
156
+
157
+ with gr.Row():
158
+ with gr.Column(scale=1):
159
+ input_image = gr.Image(
160
+ type="pil",
161
+ label="📤 Upload Gambar Batik",
162
+ height=400
163
+ )
164
+ predict_btn = gr.Button(
165
+ "🔍 Prediksi Motif Batik",
166
+ variant="primary",
167
+ size="lg"
168
+ )
169
+
170
+ gr.Markdown("""
171
+ ### 💡 Tips:
172
+ - Gunakan gambar dengan kualitas baik
173
+ - Pastikan motif batik terlihat jelas
174
+ - Format: JPG, PNG, JPEG
175
+ """)
176
+
177
+ with gr.Column(scale=1):
178
+ output_text = gr.Markdown(label="Hasil Prediksi")
179
+ output_label = gr.Label(
180
+ label="📊 Confidence Score",
181
+ num_top_classes=5
182
+ )
183
+
184
+ # Event handler
185
+ predict_btn.click(
186
+ fn=predict_image,
187
+ inputs=input_image,
188
+ outputs=[output_label, output_text]
189
+ )
190
+
191
+ # Also trigger on image upload
192
+ input_image.change(
193
+ fn=predict_image,
194
+ inputs=input_image,
195
+ outputs=[output_label, output_text]
196
+ )
197
+
198
+ gr.Markdown("""
199
+ ---
200
+ ### 📋 Tentang Model
201
+ - **Arsitektur:** VGG16 (Modified)
202
+ - **Dataset:** 111 Motif Batik Indonesia
203
+ - **Kategori:** Batik dari Jawa Tengah, Jawa Timur, Jawa Barat, Bali, Jakarta, Kalimantan, Lampung
204
+
205
+ ### 🎨 Contoh Motif:
206
+ Parang Kusumo, Megamendung, Kawung, Truntum, Semarangan, dan banyak lagi!
207
+
208
+ ---
209
+ **Made with ❤️ for Indonesian Batik Heritage**
210
+ """)
211
+
212
+
213
+ # Launch
214
+ if __name__ == "__main__":
215
+ demo.launch()
app_gradio.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio UI application for Batik Classification using VGG16 model
3
+ """
4
+ import gradio as gr
5
+ import torch
6
+ import torch.nn as nn
7
+ from torchvision import transforms, models
8
+ from PIL import Image
9
+ import json
10
+ import numpy as np
11
+ from typing import Tuple, List
12
+
13
+ # Global variables
14
+ model = None
15
+ class_names = []
16
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
17
+ transform = None
18
+
19
+
20
+ def load_model():
21
+ """Load VGG16 model and configuration"""
22
+ global model, class_names, transform
23
+
24
+ try:
25
+ # Load model configuration
26
+ with open('model_config.json', 'r') as f:
27
+ config = json.load(f)
28
+
29
+ num_classes = config['num_classes']
30
+ class_names = config['class_names']
31
+ image_size = config.get('image_size', 224)
32
+
33
+ # Initialize VGG16 model
34
+ model = models.vgg16(weights=None)
35
+ # Modify classifier to match saved model architecture
36
+ # The saved model has classifier.3 as output layer (111 classes)
37
+ model.classifier[3] = nn.Linear(4096, num_classes)
38
+ # Remove layers after classifier.3
39
+ model.classifier = nn.Sequential(*list(model.classifier.children())[:4])
40
+
41
+ # Load trained weights
42
+ checkpoint = torch.load('models/vgg16_batik_best.pth', map_location=device)
43
+
44
+ # Check if checkpoint is a dict with 'model_state_dict' key or direct state_dict
45
+ if isinstance(checkpoint, dict) and 'model_state_dict' in checkpoint:
46
+ state_dict = checkpoint['model_state_dict']
47
+ else:
48
+ state_dict = checkpoint
49
+
50
+ # Remove '_orig_mod.' prefix if present (from torch.compile)
51
+ new_state_dict = {}
52
+ for key, value in state_dict.items():
53
+ if key.startswith('_orig_mod.'):
54
+ new_key = key.replace('_orig_mod.', '')
55
+ new_state_dict[new_key] = value
56
+ else:
57
+ new_state_dict[key] = value
58
+
59
+ model.load_state_dict(new_state_dict)
60
+ model = model.to(device)
61
+ model.eval() # Define image preprocessing
62
+ transform = transforms.Compose([
63
+ transforms.Resize((image_size, image_size)),
64
+ transforms.ToTensor(),
65
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
66
+ std=[0.229, 0.224, 0.225])
67
+ ])
68
+
69
+ print(f"✅ Model loaded successfully on {device}")
70
+ print(f"📊 Number of classes: {num_classes}")
71
+
72
+ except Exception as e:
73
+ print(f"❌ Error loading model: {str(e)}")
74
+ raise
75
+
76
+
77
+ def predict_single(image: Image.Image) -> Tuple[str, float]:
78
+ """
79
+ Predict single class for an image
80
+
81
+ Args:
82
+ image: PIL Image
83
+
84
+ Returns:
85
+ Tuple of (predicted_class, confidence)
86
+ """
87
+ try:
88
+ # Preprocess image
89
+ if image is None:
90
+ return "Error: No image provided", 0.0
91
+
92
+ # Convert to RGB if needed
93
+ if image.mode != 'RGB':
94
+ image = image.convert('RGB')
95
+
96
+ # Transform and add batch dimension
97
+ input_tensor = transform(image).unsqueeze(0).to(device)
98
+
99
+ # Make prediction
100
+ with torch.no_grad():
101
+ outputs = model(input_tensor)
102
+ probabilities = torch.nn.functional.softmax(outputs, dim=1)
103
+ confidence, predicted = torch.max(probabilities, 1)
104
+
105
+ predicted_class = class_names[predicted.item()]
106
+ confidence_score = confidence.item() * 100 # Convert to percentage
107
+
108
+ return predicted_class, confidence_score
109
+
110
+ except Exception as e:
111
+ return f"Error: {str(e)}", 0.0
112
+
113
+
114
+ def predict_top_k(image: Image.Image, k: int = 5) -> dict:
115
+ """
116
+ Predict top-k classes for an image
117
+
118
+ Args:
119
+ image: PIL Image
120
+ k: Number of top predictions
121
+
122
+ Returns:
123
+ Dictionary of class names and their confidence scores
124
+ """
125
+ try:
126
+ # Preprocess image
127
+ if image is None:
128
+ return {"Error": 1.0}
129
+
130
+ # Convert to RGB if needed
131
+ if image.mode != 'RGB':
132
+ image = image.convert('RGB')
133
+
134
+ # Transform and add batch dimension
135
+ input_tensor = transform(image).unsqueeze(0).to(device)
136
+
137
+ # Make prediction
138
+ with torch.no_grad():
139
+ outputs = model(input_tensor)
140
+ probabilities = torch.nn.functional.softmax(outputs, dim=1)
141
+ top_probs, top_indices = torch.topk(probabilities, min(k, len(class_names)), dim=1)
142
+
143
+ # Format results as dictionary for Gradio
144
+ results = {}
145
+ for i in range(min(k, len(class_names))):
146
+ class_name = class_names[top_indices[0][i].item()]
147
+ confidence = top_probs[0][i].item()
148
+ results[class_name] = float(confidence)
149
+
150
+ return results
151
+
152
+ except Exception as e:
153
+ return {"Error": f"{str(e)}"}
154
+
155
+
156
+ def format_prediction(image: Image.Image) -> Tuple[str, dict]:
157
+ """
158
+ Format prediction output for Gradio interface
159
+
160
+ Args:
161
+ image: PIL Image
162
+
163
+ Returns:
164
+ Tuple of (formatted_text, top_k_dict)
165
+ """
166
+ try:
167
+ if image is None:
168
+ return "❌ Silakan upload gambar batik terlebih dahulu", {}
169
+
170
+ # Get single prediction
171
+ predicted_class, confidence = predict_single(image)
172
+
173
+ # Get top-5 predictions
174
+ top_k_results = predict_top_k(image, k=5)
175
+
176
+ # Format main result
177
+ result_text = f"""
178
+ ## 🎯 Hasil Prediksi
179
+
180
+ **Motif Batik:** `{predicted_class}`
181
+ **Confidence:** `{confidence:.2f}%`
182
+
183
+ ---
184
+
185
+ ### 📊 Top 5 Prediksi:
186
+ """
187
+
188
+ for idx, (class_name, conf) in enumerate(list(top_k_results.items())[:5], 1):
189
+ bar = "█" * int(conf * 20) # Simple bar visualization
190
+ result_text += f"\n{idx}. **{class_name}** - {conf*100:.2f}% \n {bar}"
191
+
192
+ return result_text, top_k_results
193
+
194
+ except Exception as e:
195
+ return f"❌ Error: {str(e)}", {}
196
+
197
+
198
+ def get_model_info() -> str:
199
+ """Get model information"""
200
+ info = f"""
201
+ ### 📋 Informasi Model
202
+
203
+ - **Arsitektur:** VGG16
204
+ - **Device:** {device}
205
+ - **Jumlah Kelas:** {len(class_names)}
206
+ - **Status:** ✅ Model siap digunakan
207
+
208
+ ### 🎨 Kategori Batik:
209
+ Total {len(class_names)} motif batik dari berbagai daerah di Indonesia
210
+ """
211
+ return info
212
+
213
+
214
+ # Load model at startup
215
+ load_model()
216
+
217
+ # Create Gradio interface
218
+ with gr.Blocks(title="Batik Classification - VGG16", theme=gr.themes.Soft()) as demo:
219
+
220
+ gr.Markdown("""
221
+ # 🎨 Klasifikasi Motif Batik Indonesia
222
+ ### Menggunakan Model VGG16 Deep Learning
223
+
224
+ Upload gambar batik untuk mengetahui motif dan asalnya!
225
+ """)
226
+
227
+ with gr.Tabs():
228
+
229
+ # Tab 1: Single Prediction
230
+ with gr.Tab("🖼️ Prediksi Tunggal"):
231
+ with gr.Row():
232
+ with gr.Column():
233
+ input_image = gr.Image(
234
+ type="pil",
235
+ label="Upload Gambar Batik",
236
+ height=400
237
+ )
238
+ predict_btn = gr.Button("🔍 Prediksi", variant="primary", size="lg")
239
+
240
+ gr.Examples(
241
+ examples=[], # Add example images if available
242
+ inputs=input_image,
243
+ label="Contoh Gambar (jika tersedia)"
244
+ )
245
+
246
+ with gr.Column():
247
+ output_text = gr.Markdown(label="Hasil Prediksi")
248
+ output_label = gr.Label(
249
+ label="Top 5 Prediksi",
250
+ num_top_classes=5
251
+ )
252
+
253
+ predict_btn.click(
254
+ fn=format_prediction,
255
+ inputs=input_image,
256
+ outputs=[output_text, output_label]
257
+ )
258
+
259
+ # Tab 2: Batch Prediction
260
+ with gr.Tab("📁 Prediksi Batch"):
261
+ gr.Markdown("### Upload multiple gambar batik sekaligus")
262
+
263
+ batch_input = gr.File(
264
+ file_count="multiple",
265
+ file_types=["image"],
266
+ label="Upload Gambar (Multiple)"
267
+ )
268
+ batch_btn = gr.Button("🔍 Prediksi Semua", variant="primary")
269
+ batch_output = gr.Dataframe(
270
+ headers=["Filename", "Predicted Class", "Confidence (%)"],
271
+ label="Hasil Prediksi Batch"
272
+ )
273
+
274
+ def predict_batch(files):
275
+ """Predict multiple images"""
276
+ if files is None or len(files) == 0:
277
+ return []
278
+
279
+ results = []
280
+ for file in files:
281
+ try:
282
+ image = Image.open(file.name)
283
+ pred_class, confidence = predict_single(image)
284
+ results.append([file.name.split('/')[-1], pred_class, f"{confidence:.2f}"])
285
+ except Exception as e:
286
+ results.append([file.name.split('/')[-1], "Error", str(e)])
287
+
288
+ return results
289
+
290
+ batch_btn.click(
291
+ fn=predict_batch,
292
+ inputs=batch_input,
293
+ outputs=batch_output
294
+ )
295
+
296
+ # Tab 3: Model Info
297
+ with gr.Tab("ℹ️ Info Model"):
298
+ gr.Markdown(get_model_info())
299
+
300
+ with gr.Accordion("📜 Daftar Semua Kelas Batik", open=False):
301
+ class_list = "\n".join([f"{i+1}. {name}" for i, name in enumerate(class_names)])
302
+ gr.Textbox(
303
+ value=class_list,
304
+ label=f"Total {len(class_names)} Kelas",
305
+ lines=20,
306
+ max_lines=30
307
+ )
308
+
309
+ gr.Markdown("""
310
+ ---
311
+ ### 📝 Cara Penggunaan:
312
+ 1. **Prediksi Tunggal:** Upload satu gambar batik dan klik tombol Prediksi
313
+ 2. **Prediksi Batch:** Upload beberapa gambar sekaligus untuk prediksi massal
314
+ 3. **Info Model:** Lihat informasi lengkap tentang model dan daftar kelas
315
+
316
+ ### 💡 Tips:
317
+ - Gunakan gambar dengan kualitas yang baik untuk hasil terbaik
318
+ - Pastikan gambar menunjukkan motif batik dengan jelas
319
+ - Model mendukung format JPG, PNG, dan format gambar umum lainnya
320
+ """)
321
+
322
+
323
+ # Launch the app
324
+ if __name__ == "__main__":
325
+ try:
326
+ demo.launch(
327
+ server_name="127.0.0.1",
328
+ server_port=7860,
329
+ share=False, # Ubah ke True jika mau public link
330
+ inbrowser=True,
331
+ quiet=False
332
+ )
333
+ except Exception as e:
334
+ print(f"Error launching Gradio: {e}")
335
+ # Fallback: try simpler launch
336
+ demo.launch()
dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gunakan image Python 3.9 atau 3.10
2
+ FROM python:3.13.11
3
+
4
+ # Set working directory awal
5
+ WORKDIR /code
6
+
7
+ # Copy requirements dan install dependencies
8
+ COPY ./requirements.txt /code/requirements.txt
9
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
10
+
11
+ # Buat user baru dengan ID 1000 (Syarat wajib Hugging Face Spaces)
12
+ RUN useradd -m -u 1000 user
13
+
14
+ # Pindah ke user tersebut
15
+ USER user
16
+
17
+ # Set environment variables
18
+ ENV HOME=/home/user \
19
+ PATH=/home/user/.local/bin:$PATH
20
+
21
+ # Set working directory ke folder aplikasi user
22
+ WORKDIR $HOME/app
23
+
24
+ # Copy seluruh file aplikasi ke dalam container dengan permission user
25
+ COPY --chown=user . $HOME/app
26
+
27
+ # Expose port default Gradio (7860)
28
+ EXPOSE 7860
29
+
30
+ # Jalankan aplikasi
31
+ CMD ["python", "app.py"]
labels.txt ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Bali_Barong
2
+ Bali_Merak
3
+ Jakarta_OndelOndel
4
+ Jakarta_Tumpal
5
+ JawaBarat_Megamendung
6
+ JawaTengah_Arumdalu
7
+ JawaTengah_AsemArang
8
+ JawaTengah_AsemSinom
9
+ JawaTengah_AsemWarak
10
+ JawaTengah_Blekok
11
+ JawaTengah_BlekokWarak
12
+ JawaTengah_CindeWilis
13
+ JawaTengah_Cipratan
14
+ JawaTengah_GambangSemarangan
15
+ JawaTengah_IkanKerang
16
+ JawaTengah_JagungLombok
17
+ JawaTengah_JambuBelimbing
18
+ JawaTengah_JambuCitra
19
+ JawaTengah_JayaKusuma
20
+ JawaTengah_Jlamprang
21
+ JawaTengah_KembangSepatu
22
+ JawaTengah_Kemukus
23
+ JawaTengah_Laut
24
+ JawaTengah_LurikSemangka
25
+ JawaTengah_MasjidAgungDemak
26
+ JawaTengah_Mawur
27
+ JawaTengah_Naga
28
+ JawaTengah_ParangKusumo
29
+ JawaTengah_ParangSlobog
30
+ JawaTengah_Rengganis
31
+ JawaTengah_SariMulat
32
+ JawaTengah_Semarangan
33
+ JawaTengah_Sidoluhur
34
+ JawaTengah_Sritaman
35
+ JawaTengah_TanjungGunung
36
+ JawaTengah_TebuBambu
37
+ JawaTengah_Tembakau
38
+ JawaTengah_Truntum
39
+ JawaTengah_TruntumKurung
40
+ JawaTengah_TuguMuda
41
+ JawaTengah_WarakBerasUtah
42
+ JawaTengah_WorawariRumpuk
43
+ JawaTengah_Yuyu
44
+ JawaTimur_Gentongan
45
+ JawaTimur_Pring
46
+ KalimantanBarat_Insang
47
+ Kalimantan_Dayak
48
+ Lampung_Bledheg
49
+ Lampung_Gajah
50
+ Lampung_KacangHijau
51
+ Maluku_Pala
52
+ NTB_Lumbung
53
+ Papua_Asmat
54
+ Papua_Cendrawasih
55
+ Papua_Tifa
56
+ SulawesiSelatan_Lontara
57
+ SumateraBarat_RumahMinang
58
+ SumateraUtara_Boraspati
59
+ SumateraUtara_PintuAceh
60
+ Yogyakarta_Brendi
61
+ Yogyakarta_CakarAyam
62
+ Yogyakarta_CeplokLiring
63
+ Yogyakarta_Gendhangan
64
+ Yogyakarta_JayaKirana
65
+ Yogyakarta_Karawitan
66
+ Yogyakarta_Kawung
67
+ Yogyakarta_KlampokArum
68
+ Yogyakarta_KuncupKanthil
69
+ Yogyakarta_Manggar
70
+ Yogyakarta_ParangBarong
71
+ Yogyakarta_ParangCurigo
72
+ Yogyakarta_ParangRusak
73
+ Yogyakarta_ParangTuding
74
+ Yogyakarta_SekarAndhong
75
+ Yogyakarta_SekarBlimbing
76
+ Yogyakarta_SekarCengkeh
77
+ Yogyakarta_SekarDangan
78
+ Yogyakarta_SekarDhuku
79
+ Yogyakarta_SekarDlima
80
+ Yogyakarta_SekarDuren
81
+ Yogyakarta_SekarGambir
82
+ Yogyakarta_SekarGayam
83
+ Yogyakarta_SekarJagung
84
+ Yogyakarta_SekarJali
85
+ Yogyakarta_SekarJeruk
86
+ Yogyakarta_SekarKeben
87
+ Yogyakarta_SekarKemuning
88
+ Yogyakarta_SekarKenanga
89
+ Yogyakarta_SekarKenikir
90
+ Yogyakarta_SekarKenthang
91
+ Yogyakarta_SekarKepel
92
+ Yogyakarta_SekarKetongkeng
93
+ Yogyakarta_SekarLintang
94
+ Yogyakarta_SekarManggis
95
+ Yogyakarta_SekarMenur
96
+ Yogyakarta_SekarMindi
97
+ Yogyakarta_SekarMlathi
98
+ Yogyakarta_SekarMrica
99
+ Yogyakarta_SekarMundhu
100
+ Yogyakarta_SekarNangka
101
+ Yogyakarta_SekarPacar
102
+ Yogyakarta_SekarPala
103
+ Yogyakarta_SekarPijetan
104
+ Yogyakarta_SekarPudhak
105
+ Yogyakarta_SekarRandhu
106
+ Yogyakarta_SekarSawo
107
+ Yogyakarta_SekarSoka
108
+ Yogyakarta_SekarSrengenge
109
+ Yogyakarta_SekarSrigadhing
110
+ Yogyakarta_SekarTanjung
111
+ Yogyakarta_SekarTebu
model_config.json ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model": "VGG16",
3
+ "image_size": 224,
4
+ "batch_size": 64,
5
+ "num_classes": 111,
6
+ "class_names": [
7
+ "Bali_Barong",
8
+ "Bali_Merak",
9
+ "Jakarta_OndelOndel",
10
+ "Jakarta_Tumpal",
11
+ "JawaBarat_Megamendung",
12
+ "JawaTengah_Arumdalu",
13
+ "JawaTengah_AsemArang",
14
+ "JawaTengah_AsemSinom",
15
+ "JawaTengah_AsemWarak",
16
+ "JawaTengah_Blekok",
17
+ "JawaTengah_BlekokWarak",
18
+ "JawaTengah_CindeWilis",
19
+ "JawaTengah_Cipratan",
20
+ "JawaTengah_GambangSemarangan",
21
+ "JawaTengah_IkanKerang",
22
+ "JawaTengah_JagungLombok",
23
+ "JawaTengah_JambuBelimbing",
24
+ "JawaTengah_JambuCitra",
25
+ "JawaTengah_JayaKusuma",
26
+ "JawaTengah_Jlamprang",
27
+ "JawaTengah_KembangSepatu",
28
+ "JawaTengah_Kemukus",
29
+ "JawaTengah_Laut",
30
+ "JawaTengah_LurikSemangka",
31
+ "JawaTengah_MasjidAgungDemak",
32
+ "JawaTengah_Mawur",
33
+ "JawaTengah_Naga",
34
+ "JawaTengah_ParangKusumo",
35
+ "JawaTengah_ParangSlobog",
36
+ "JawaTengah_Rengganis",
37
+ "JawaTengah_SariMulat",
38
+ "JawaTengah_Semarangan",
39
+ "JawaTengah_Sidoluhur",
40
+ "JawaTengah_Sritaman",
41
+ "JawaTengah_TanjungGunung",
42
+ "JawaTengah_TebuBambu",
43
+ "JawaTengah_Tembakau",
44
+ "JawaTengah_Truntum",
45
+ "JawaTengah_TruntumKurung",
46
+ "JawaTengah_TuguMuda",
47
+ "JawaTengah_WarakBerasUtah",
48
+ "JawaTengah_WorawariRumpuk",
49
+ "JawaTengah_Yuyu",
50
+ "JawaTimur_Gentongan",
51
+ "JawaTimur_Pring",
52
+ "KalimantanBarat_Insang",
53
+ "Kalimantan_Dayak",
54
+ "Lampung_Bledheg",
55
+ "Lampung_Gajah",
56
+ "Lampung_KacangHijau",
57
+ "Maluku_Pala",
58
+ "NTB_Lumbung",
59
+ "Papua_Asmat",
60
+ "Papua_Cendrawasih",
61
+ "Papua_Tifa",
62
+ "SulawesiSelatan_Lontara",
63
+ "SumateraBarat_RumahMinang",
64
+ "SumateraUtara_Boraspati",
65
+ "SumateraUtara_PintuAceh",
66
+ "Yogyakarta_Brendi",
67
+ "Yogyakarta_CakarAyam",
68
+ "Yogyakarta_CeplokLiring",
69
+ "Yogyakarta_Gendhangan",
70
+ "Yogyakarta_JayaKirana",
71
+ "Yogyakarta_Karawitan",
72
+ "Yogyakarta_Kawung",
73
+ "Yogyakarta_KlampokArum",
74
+ "Yogyakarta_KuncupKanthil",
75
+ "Yogyakarta_Manggar",
76
+ "Yogyakarta_ParangBarong",
77
+ "Yogyakarta_ParangCurigo",
78
+ "Yogyakarta_ParangRusak",
79
+ "Yogyakarta_ParangTuding",
80
+ "Yogyakarta_SekarAndhong",
81
+ "Yogyakarta_SekarBlimbing",
82
+ "Yogyakarta_SekarCengkeh",
83
+ "Yogyakarta_SekarDangan",
84
+ "Yogyakarta_SekarDhuku",
85
+ "Yogyakarta_SekarDlima",
86
+ "Yogyakarta_SekarDuren",
87
+ "Yogyakarta_SekarGambir",
88
+ "Yogyakarta_SekarGayam",
89
+ "Yogyakarta_SekarJagung",
90
+ "Yogyakarta_SekarJali",
91
+ "Yogyakarta_SekarJeruk",
92
+ "Yogyakarta_SekarKeben",
93
+ "Yogyakarta_SekarKemuning",
94
+ "Yogyakarta_SekarKenanga",
95
+ "Yogyakarta_SekarKenikir",
96
+ "Yogyakarta_SekarKenthang",
97
+ "Yogyakarta_SekarKepel",
98
+ "Yogyakarta_SekarKetongkeng",
99
+ "Yogyakarta_SekarLintang",
100
+ "Yogyakarta_SekarManggis",
101
+ "Yogyakarta_SekarMenur",
102
+ "Yogyakarta_SekarMindi",
103
+ "Yogyakarta_SekarMlathi",
104
+ "Yogyakarta_SekarMrica",
105
+ "Yogyakarta_SekarMundhu",
106
+ "Yogyakarta_SekarNangka",
107
+ "Yogyakarta_SekarPacar",
108
+ "Yogyakarta_SekarPala",
109
+ "Yogyakarta_SekarPijetan",
110
+ "Yogyakarta_SekarPudhak",
111
+ "Yogyakarta_SekarRandhu",
112
+ "Yogyakarta_SekarSawo",
113
+ "Yogyakarta_SekarSoka",
114
+ "Yogyakarta_SekarSrengenge",
115
+ "Yogyakarta_SekarSrigadhing",
116
+ "Yogyakarta_SekarTanjung",
117
+ "Yogyakarta_SekarTebu"
118
+ ],
119
+ "split_ratio": "70/15/15",
120
+ "training": {
121
+ "epochs": 30,
122
+ "best_epoch": 27,
123
+ "initial_lr": 0.001,
124
+ "optimizer": "Adam",
125
+ "scheduler": "ReduceLROnPlateau",
126
+ "total_time_hours": 1.98,
127
+ "avg_epoch_time_seconds": 234.88
128
+ },
129
+ "results": {
130
+ "best_val_acc": 99.3547,
131
+ "final_train_acc": 99.0093,
132
+ "final_val_acc": 99.3188,
133
+ "test_acc": 99.3461,
134
+ "test_precision": 0.9936,
135
+ "test_recall": 0.9935,
136
+ "test_f1": 0.9934,
137
+ "inference_speed_imgs_per_sec": 272.09
138
+ },
139
+ "class_statistics": {
140
+ "mean_class_accuracy": 99.3418,
141
+ "std_class_accuracy": 2.0216,
142
+ "min_class_accuracy": 88.0,
143
+ "max_class_accuracy": 100.0
144
+ },
145
+ "hardware": {
146
+ "device": "cuda",
147
+ "gpu_name": "NVIDIA GeForce RTX 3060",
148
+ "cuda_version": "12.8"
149
+ },
150
+ "timestamp": "2025-11-30 06:16:16"
151
+ }
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Requirements untuk FastAPI dan Gradio
2
+ fastapi==0.123.0
3
+ uvicorn[standard]==0.34.0
4
+ python-multipart==0.0.20
5
+ gradio==5.10.0
6
+ torch==2.6.0
7
+ torchvision==0.21.0
8
+ Pillow==11.1.0
9
+ numpy==2.2.2