doniramdani820 commited on
Commit
35c1540
·
verified ·
1 Parent(s): 775c80d

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +36 -0
  2. app.py +272 -0
  3. best_model_quantized.pth +3 -0
  4. mappings.json +357 -0
  5. requirements.txt +12 -0
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gunakan base image Python yang ringan
2
+ FROM python:3.9-slim as builder
3
+
4
+ # Instal library sistem yang dibutuhkan untuk proses build
5
+ RUN apt-get update && apt-get install -y \
6
+ build-essential \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ WORKDIR /code
10
+
11
+ # Salin dan instal requirements dalam tahap builder
12
+ COPY ./requirements.txt /code/requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
14
+
15
+ # --- Tahap Final ---
16
+ # Mulai lagi dari base image yang bersih untuk runtime
17
+ FROM python:3.9-slim
18
+
19
+ # Instal HANYA library sistem yang dibutuhkan untuk menjalankan aplikasi
20
+ RUN apt-get update && apt-get install -y \
21
+ libgl1-mesa-glx \
22
+ libglib2.0-0 \
23
+ && rm -rf /var/lib/apt/lists/*
24
+
25
+ WORKDIR /code
26
+
27
+ # Salin paket python yang sudah terinstal dari tahap builder
28
+ COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
29
+
30
+ # Salin kode aplikasi Anda
31
+ COPY . /code/
32
+
33
+ EXPOSE 7860
34
+
35
+ # CMD tetap sama, memastikan aplikasi dijalankan dengan Gunicorn
36
+ CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:7860", "--timeout", "120", "app:app"]
app.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # server.py
2
+ # Server Flask untuk prediksi multi-head captcha yang disesuaikan untuk Hugging Face Spaces.
3
+ # Versi ini sudah dioptimalkan untuk memuat model yang terkuantisasi (INT8).
4
+
5
+ from flask import Flask, request, jsonify
6
+ from flask_cors import CORS
7
+ import torch
8
+ import torch.nn as nn
9
+ import timm
10
+ import albumentations as A
11
+ from albumentations.pytorch import ToTensorV2
12
+ from PIL import Image, UnidentifiedImageError
13
+ import numpy as np
14
+ import cv2
15
+ import os
16
+ import json
17
+ import base64
18
+ from io import BytesIO
19
+ import sys
20
+ import logging
21
+
22
+ torch.set_num_threads(1)
23
+
24
+ # ==============================================================================
25
+ # BAGIAN 1: PENGATURAN DASAR FLASK & LOGGING
26
+ # ==============================================================================
27
+
28
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', stream=sys.stdout)
29
+ app = Flask(__name__)
30
+ CORS(app)
31
+
32
+ MODEL = None
33
+ MAPPINGS = None
34
+ DEVICE = None
35
+ TRANSFORMS = None
36
+
37
+ # ==============================================================================
38
+ # BAGIAN 2: DEFINISI MODEL (TIDAK ADA PERUBAHAN)
39
+ # ==============================================================================
40
+
41
+ class TextHeadCTC(nn.Module):
42
+ def __init__(self, input_dim, hidden_dim, ctc_vocab_size):
43
+ super().__init__()
44
+ self.rnn = nn.LSTM(input_dim, hidden_dim, num_layers=1, bidirectional=True, batch_first=True)
45
+ self.fc = nn.Linear(hidden_dim * 2, ctc_vocab_size)
46
+ def forward(self, x):
47
+ rnn_out, _ = self.rnn(x)
48
+ output_logits = self.fc(rnn_out)
49
+ return nn.functional.log_softmax(output_logits, dim=2).permute(1, 0, 2)
50
+
51
+ class MultiHeadModel(nn.Module):
52
+ def __init__(self, backbone_name, ctc_vocab_size, num_object_classes, num_types):
53
+ super().__init__()
54
+ self.backbone = timm.create_model(
55
+ backbone_name,
56
+ pretrained=False,
57
+ num_classes=0,
58
+ drop_path_rate=0.1
59
+ )
60
+ backbone_features_dim = self.backbone.num_features
61
+ rnn_hidden_dim, projected_embed_dim = 256, 256
62
+ self.type_head = nn.Linear(backbone_features_dim, num_types)
63
+ self.object_head = nn.Linear(backbone_features_dim, num_object_classes)
64
+ self.input_proj = nn.Conv2d(backbone_features_dim, projected_embed_dim, kernel_size=1)
65
+ self.text_head_ctc = TextHeadCTC(projected_embed_dim, rnn_hidden_dim, ctc_vocab_size)
66
+ self.pool = nn.AdaptiveAvgPool2d((1, 1))
67
+
68
+ def forward(self, x):
69
+ features = self.backbone.forward_features(x)
70
+ pooled_features = self.pool(features).flatten(1)
71
+ type_logits = self.type_head(pooled_features)
72
+ object_logits = self.object_head(pooled_features)
73
+ proj_features = self.input_proj(features)
74
+ bs, c_proj, h_feat, w_feat = proj_features.size()
75
+ image_features_seq = proj_features.view(bs, c_proj, h_feat * w_feat).permute(0, 2, 1)
76
+ text_log_probs = self.text_head_ctc(image_features_seq)
77
+ return type_logits, object_logits, text_log_probs
78
+
79
+ # ==============================================================================
80
+ # BAGIAN 3: FUNGSI HELPER (TIDAK ADA PERUBAHAN)
81
+ # ==============================================================================
82
+
83
+ def get_transforms(img_height, img_width):
84
+ interpolation_method = cv2.INTER_AREA
85
+ return A.Compose([
86
+ A.Resize(height=img_height, width=img_width, interpolation=interpolation_method),
87
+ A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
88
+ ToTensorV2()
89
+ ])
90
+
91
+ def ctc_decoder_with_confidence(log_probs, idx_to_char_map, blank_idx):
92
+ probs = torch.exp(log_probs)
93
+ max_probs, pred_indices = torch.max(probs, dim=-1)
94
+ max_probs = max_probs.squeeze(1).cpu().numpy()
95
+ pred_indices = pred_indices.squeeze(1).cpu().numpy()
96
+
97
+ decoded_sequence = []
98
+ confidence_values = []
99
+ last_idx = -1
100
+
101
+ for i, idx in enumerate(pred_indices):
102
+ if idx == blank_idx or idx == last_idx:
103
+ last_idx = blank_idx if idx == blank_idx else last_idx
104
+ continue
105
+
106
+ decoded_sequence.append(idx_to_char_map.get(str(idx), '?'))
107
+ confidence_values.append(max_probs[i])
108
+ last_idx = idx
109
+
110
+ final_text = "".join(decoded_sequence)
111
+ avg_confidence = np.mean(confidence_values) if confidence_values else 0.0
112
+
113
+ return final_text, avg_confidence
114
+
115
+ # ==============================================================================
116
+ # BAGIAN 4: INISIALISASI SERVER (VERSI UNTUK MODEL KUANTISASI)
117
+ # ==============================================================================
118
+
119
+ def initialize_server(model_path, mappings_path):
120
+ global MODEL, MAPPINGS, DEVICE, TRANSFORMS
121
+ logging.info("Memulai inisialisasi server dengan model terkuantisasi...")
122
+ DEVICE = torch.device("cpu") # Kuantisasi INT8 dioptimalkan untuk CPU
123
+ logging.info(f"Menggunakan device: {DEVICE}")
124
+
125
+ try:
126
+ if not os.path.exists(mappings_path):
127
+ raise FileNotFoundError(f"File mappings tidak ditemukan di: {mappings_path}")
128
+ with open(mappings_path, 'r', encoding='utf-8') as f:
129
+ MAPPINGS = json.load(f)
130
+ logging.info("File mappings berhasil dimuat.")
131
+ except Exception as e:
132
+ logging.error(f"FATAL: Gagal memuat file mappings: {e}")
133
+ sys.exit(1)
134
+
135
+ TRANSFORMS = get_transforms(MAPPINGS['img_height'], MAPPINGS['img_width'])
136
+
137
+ # Langkah 1: Buat instance model dengan arsitektur asli
138
+ try:
139
+ m = MAPPINGS
140
+ model_to_quantize = MultiHeadModel(
141
+ backbone_name=m['backbone'],
142
+ ctc_vocab_size=len(m['ctc_char_to_idx']),
143
+ num_object_classes=len(m['object_to_idx']),
144
+ num_types=len(m['type_to_idx'])
145
+ )
146
+ logging.info(f"Instance model '{MAPPINGS['backbone']}' berhasil dibuat.")
147
+ except Exception as e:
148
+ logging.error(f"FATAL: Gagal membuat instance model. Error: {e}")
149
+ sys.exit(1)
150
+
151
+ # Langkah 2: Siapkan model untuk menerima weights terkuantisasi
152
+ MODEL = torch.quantization.quantize_dynamic(
153
+ model_to_quantize, {nn.Linear, nn.LSTM}, dtype=torch.qint8
154
+ )
155
+ logging.info("Arsitektur model disiapkan untuk kuantisasi dinamis.")
156
+
157
+ # Langkah 3: Muat state_dict dari file .pth yang sudah terkuantisasi
158
+ try:
159
+ if not os.path.exists(model_path):
160
+ raise FileNotFoundError(f"File model tidak ditemukan di: {model_path}")
161
+
162
+ # Langsung load state_dict karena kita sudah menyiapkan arsitekturnya
163
+ MODEL.load_state_dict(torch.load(model_path, map_location=DEVICE))
164
+ MODEL.to(DEVICE)
165
+ MODEL.eval()
166
+ logging.info("Model weights terkuantisasi berhasil dimuat dan siap digunakan.")
167
+
168
+ except Exception as e:
169
+ logging.error(f"FATAL: Gagal memuat model weights terkuantisasi. Error: {e}", exc_info=True)
170
+ sys.exit(1)
171
+
172
+ logging.info("Inisialisasi server selesai. Siap menerima permintaan.")
173
+
174
+ # ==============================================================================
175
+ # BAGIAN 5: ENDPOINT FLASK (TIDAK ADA PERUBAHAN)
176
+ # ==============================================================================
177
+ @app.route('/', methods=['GET'])
178
+ def home():
179
+ """Endpoint dasar untuk memeriksa apakah server berjalan."""
180
+ return "<h1>Captcha Prediction Server is running (Quantized Model).</h1><p>Gunakan endpoint /predict untuk melakukan prediksi.</p>", 200
181
+
182
+ @app.route('/predict', methods=['POST'])
183
+ def predict_endpoint():
184
+ """Endpoint untuk menerima gambar base64 dan mengembalikan prediksi."""
185
+
186
+ # Otentikasi
187
+ expected_api_key = os.environ.get('API_KEY_SECRET')
188
+ if not expected_api_key:
189
+ logging.error("FATAL: Secret 'API_KEY_SECRET' tidak diatur di server.")
190
+ return jsonify({"error": "Konfigurasi server error."}), 500
191
+
192
+ auth_header = request.headers.get('Authorization')
193
+ if not auth_header or auth_header != f"Bearer {expected_api_key}":
194
+ logging.warning(f"Akses ditolak untuk IP: {request.remote_addr}. Alasan: Kunci API tidak valid.")
195
+ return jsonify({"error": "Akses ditolak."}), 403
196
+
197
+ # Proses prediksi
198
+ if not request.is_json:
199
+ return jsonify({"error": "Request harus berupa JSON"}), 400
200
+
201
+ data = request.get_json()
202
+ base64_string = data.get('image_base64')
203
+
204
+ if not base64_string:
205
+ return jsonify({"error": "Key 'image_base64' tidak ditemukan atau kosong"}), 400
206
+
207
+ try:
208
+ if ',' in base64_string:
209
+ _, encoded = base64_string.split(',', 1)
210
+ else:
211
+ encoded = base64_string
212
+ image_data = base64.b64decode(encoded)
213
+ img_pil = Image.open(BytesIO(image_data)).convert("RGB")
214
+
215
+ except (base64.binascii.Error, UnidentifiedImageError) as e:
216
+ logging.error(f"Error memproses gambar base64: {e}")
217
+ return jsonify({"error": f"Data base64 tidak valid atau format gambar tidak didukung."}), 400
218
+ except Exception as e:
219
+ logging.error(f"Error tak terduga saat memproses gambar: {e}")
220
+ return jsonify({"error": "Gagal memproses gambar."}), 500
221
+
222
+ try:
223
+ image_rgb = np.array(img_pil)
224
+ img_tensor = TRANSFORMS(image=image_rgb)['image'].unsqueeze(0).to(DEVICE)
225
+
226
+ with torch.no_grad():
227
+ type_logits, object_logits, text_log_probs = MODEL(img_tensor)
228
+
229
+ type_prob = torch.softmax(type_logits, dim=1)
230
+ type_conf, type_pred_idx = torch.max(type_prob, dim=1)
231
+ pred_type = MAPPINGS['idx_to_type'].get(str(type_pred_idx.item()), 'Tipe Tidak Dikenal')
232
+
233
+ response = {
234
+ "predicted_type": pred_type,
235
+ "type_confidence": f"{type_conf.item():.2%}",
236
+ "prediction": None,
237
+ "prediction_confidence": None,
238
+ "error": None
239
+ }
240
+
241
+ if pred_type == 'object':
242
+ obj_prob = torch.softmax(object_logits, dim=1)
243
+ obj_conf, obj_pred_idx = torch.max(obj_prob, dim=1)
244
+ pred_obj = MAPPINGS['idx_to_object'].get(str(obj_pred_idx.item()), 'Objek Tidak Dikenal')
245
+ response["prediction"] = pred_obj
246
+ response["prediction_confidence"] = f"{obj_conf.item():.2%}"
247
+ elif pred_type == 'text':
248
+ pred_text, confidence = ctc_decoder_with_confidence(text_log_probs, MAPPINGS['ctc_idx_to_char'], MAPPINGS['ctc_blank_idx'])
249
+ response["prediction"] = pred_text
250
+ response["prediction_confidence"] = f"{confidence:.2%}"
251
+
252
+ logging.info(f"Prediksi berhasil: Tipe='{response['predicted_type']}', Hasil='{response['prediction']}', Conf='{response['prediction_confidence']}'")
253
+ return jsonify(response), 200
254
+
255
+ except Exception as e:
256
+ logging.error(f"Error saat inferensi model: {e}", exc_info=True)
257
+ return jsonify({"error": "Terjadi kesalahan pada server saat melakukan prediksi."}), 500
258
+
259
+ # ==============================================================================
260
+ # BAGIAN 6: MENJALANKAN SERVER (UNTUK HUGGING FACE SPACES)
261
+ # ==============================================================================
262
+
263
+ # Menggunakan file model baru yang sudah terkuantisasi
264
+ MODEL_FILE_PATH = "best_model_quantized.pth"
265
+ MAPPINGS_FILE_PATH = "mappings.json"
266
+
267
+ # Inisialisasi server saat aplikasi dimulai
268
+ initialize_server(MODEL_FILE_PATH, MAPPINGS_FILE_PATH)
269
+
270
+ # Berguna untuk pengujian lokal
271
+ if __name__ == '__main__':
272
+ app.run(host='0.0.0.0', port=5111, debug=True)
best_model_quantized.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:355509fbab22f2773d1c5ca340feb261e5682f69cfb688b481787ae77da9e4d0
3
+ size 46128300
mappings.json ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backbone": "tf_efficientnet_b3.ns_jft_in1k",
3
+ "img_height": 416,
4
+ "img_width": 416,
5
+ "type_to_idx": {
6
+ "object": 0,
7
+ "text": 1
8
+ },
9
+ "idx_to_type": {
10
+ "0": "object",
11
+ "1": "text"
12
+ },
13
+ "object_to_idx": {
14
+ "2": 0,
15
+ "Apple": 1,
16
+ "Bear": 2,
17
+ "Bee": 3,
18
+ "Blank": 4,
19
+ "Bus": 5,
20
+ "Butterfly": 6,
21
+ "Car": 7,
22
+ "Cat": 8,
23
+ "Cow": 9,
24
+ "Crab": 10,
25
+ "Crown": 11,
26
+ "DOG": 12,
27
+ "Dinosaurs": 13,
28
+ "Dog": 14,
29
+ "Factory": 15,
30
+ "Guitar": 16,
31
+ "Helicopter": 17,
32
+ "Horse": 18,
33
+ "House": 19,
34
+ "Koala": 20,
35
+ "Motorcycle": 21,
36
+ "Penguin": 22,
37
+ "Rabbit": 23,
38
+ "Shark": 24,
39
+ "Speaker": 25,
40
+ "Statue": 26,
41
+ "Watermelon": 27,
42
+ "Wine": 28,
43
+ "Wolf": 29,
44
+ "atv": 30,
45
+ "bee": 31,
46
+ "bicycle": 32,
47
+ "bird": 33,
48
+ "butterfly": 34,
49
+ "cart": 35,
50
+ "chicken": 36,
51
+ "crocodile": 37,
52
+ "fish": 38,
53
+ "flower": 39,
54
+ "koala": 40,
55
+ "monster car": 41,
56
+ "motorcycle": 42,
57
+ "mouse": 43,
58
+ "panda": 44,
59
+ "snowman": 45,
60
+ "tiger": 46,
61
+ "tractor": 47
62
+ },
63
+ "idx_to_object": {
64
+ "0": "2",
65
+ "1": "Apple",
66
+ "2": "Bear",
67
+ "3": "Bee",
68
+ "4": "Blank",
69
+ "5": "Bus",
70
+ "6": "Butterfly",
71
+ "7": "Car",
72
+ "8": "Cat",
73
+ "9": "Cow",
74
+ "10": "Crab",
75
+ "11": "Crown",
76
+ "12": "DOG",
77
+ "13": "Dinosaurs",
78
+ "14": "Dog",
79
+ "15": "Factory",
80
+ "16": "Guitar",
81
+ "17": "Helicopter",
82
+ "18": "Horse",
83
+ "19": "House",
84
+ "20": "Koala",
85
+ "21": "Motorcycle",
86
+ "22": "Penguin",
87
+ "23": "Rabbit",
88
+ "24": "Shark",
89
+ "25": "Speaker",
90
+ "26": "Statue",
91
+ "27": "Watermelon",
92
+ "28": "Wine",
93
+ "29": "Wolf",
94
+ "30": "atv",
95
+ "31": "bee",
96
+ "32": "bicycle",
97
+ "33": "bird",
98
+ "34": "butterfly",
99
+ "35": "cart",
100
+ "36": "chicken",
101
+ "37": "crocodile",
102
+ "38": "fish",
103
+ "39": "flower",
104
+ "40": "koala",
105
+ "41": "monster car",
106
+ "42": "motorcycle",
107
+ "43": "mouse",
108
+ "44": "panda",
109
+ "45": "snowman",
110
+ "46": "tiger",
111
+ "47": "tractor"
112
+ },
113
+ "ctc_char_to_idx": {
114
+ " ": 1,
115
+ "!": 2,
116
+ "#": 3,
117
+ "$": 4,
118
+ "%": 5,
119
+ "&": 6,
120
+ "(": 7,
121
+ ")": 8,
122
+ "*": 9,
123
+ "+": 10,
124
+ ",": 11,
125
+ "-": 12,
126
+ ".": 13,
127
+ "0": 14,
128
+ "1": 15,
129
+ "2": 16,
130
+ "3": 17,
131
+ "4": 18,
132
+ "5": 19,
133
+ "6": 20,
134
+ "7": 21,
135
+ "8": 22,
136
+ "9": 23,
137
+ ":": 24,
138
+ "=": 25,
139
+ "?": 26,
140
+ "@": 27,
141
+ "A": 28,
142
+ "B": 29,
143
+ "C": 30,
144
+ "D": 31,
145
+ "E": 32,
146
+ "F": 33,
147
+ "G": 34,
148
+ "H": 35,
149
+ "I": 36,
150
+ "J": 37,
151
+ "K": 38,
152
+ "L": 39,
153
+ "M": 40,
154
+ "N": 41,
155
+ "O": 42,
156
+ "P": 43,
157
+ "Q": 44,
158
+ "R": 45,
159
+ "S": 46,
160
+ "T": 47,
161
+ "U": 48,
162
+ "V": 49,
163
+ "W": 50,
164
+ "X": 51,
165
+ "Y": 52,
166
+ "Z": 53,
167
+ "[": 54,
168
+ "\\": 55,
169
+ "^": 56,
170
+ "_": 57,
171
+ "a": 58,
172
+ "b": 59,
173
+ "c": 60,
174
+ "d": 61,
175
+ "e": 62,
176
+ "f": 63,
177
+ "g": 64,
178
+ "h": 65,
179
+ "i": 66,
180
+ "j": 67,
181
+ "k": 68,
182
+ "l": 69,
183
+ "m": 70,
184
+ "n": 71,
185
+ "o": 72,
186
+ "p": 73,
187
+ "q": 74,
188
+ "r": 75,
189
+ "s": 76,
190
+ "t": 77,
191
+ "u": 78,
192
+ "v": 79,
193
+ "w": 80,
194
+ "x": 81,
195
+ "y": 82,
196
+ "z": 83,
197
+ "{": 84,
198
+ "}": 85,
199
+ "÷": 86,
200
+ "Б": 87,
201
+ "Г": 88,
202
+ "Д": 89,
203
+ "Ж": 90,
204
+ "З": 91,
205
+ "И": 92,
206
+ "Й": 93,
207
+ "Л": 94,
208
+ "О": 95,
209
+ "Ф": 96,
210
+ "Ч": 97,
211
+ "Ш": 98,
212
+ "Ю": 99,
213
+ "Я": 100,
214
+ "д": 101,
215
+ "ж": 102,
216
+ "ш": 103,
217
+ "٣": 104,
218
+ "٤": 105,
219
+ "٩": 106,
220
+ "ข": 107,
221
+ "ถ": 108,
222
+ "บ": 109,
223
+ "ร": 110,
224
+ "ว": 111,
225
+ "ห": 112,
226
+ "与": 113,
227
+ "养": 114,
228
+ "決": 115,
229
+ "海": 116,
230
+ "的": 117,
231
+ "窄": 118,
232
+ "<blank>": 0
233
+ },
234
+ "ctc_idx_to_char": {
235
+ "1": " ",
236
+ "2": "!",
237
+ "3": "#",
238
+ "4": "$",
239
+ "5": "%",
240
+ "6": "&",
241
+ "7": "(",
242
+ "8": ")",
243
+ "9": "*",
244
+ "10": "+",
245
+ "11": ",",
246
+ "12": "-",
247
+ "13": ".",
248
+ "14": "0",
249
+ "15": "1",
250
+ "16": "2",
251
+ "17": "3",
252
+ "18": "4",
253
+ "19": "5",
254
+ "20": "6",
255
+ "21": "7",
256
+ "22": "8",
257
+ "23": "9",
258
+ "24": ":",
259
+ "25": "=",
260
+ "26": "?",
261
+ "27": "@",
262
+ "28": "A",
263
+ "29": "B",
264
+ "30": "C",
265
+ "31": "D",
266
+ "32": "E",
267
+ "33": "F",
268
+ "34": "G",
269
+ "35": "H",
270
+ "36": "I",
271
+ "37": "J",
272
+ "38": "K",
273
+ "39": "L",
274
+ "40": "M",
275
+ "41": "N",
276
+ "42": "O",
277
+ "43": "P",
278
+ "44": "Q",
279
+ "45": "R",
280
+ "46": "S",
281
+ "47": "T",
282
+ "48": "U",
283
+ "49": "V",
284
+ "50": "W",
285
+ "51": "X",
286
+ "52": "Y",
287
+ "53": "Z",
288
+ "54": "[",
289
+ "55": "\\",
290
+ "56": "^",
291
+ "57": "_",
292
+ "58": "a",
293
+ "59": "b",
294
+ "60": "c",
295
+ "61": "d",
296
+ "62": "e",
297
+ "63": "f",
298
+ "64": "g",
299
+ "65": "h",
300
+ "66": "i",
301
+ "67": "j",
302
+ "68": "k",
303
+ "69": "l",
304
+ "70": "m",
305
+ "71": "n",
306
+ "72": "o",
307
+ "73": "p",
308
+ "74": "q",
309
+ "75": "r",
310
+ "76": "s",
311
+ "77": "t",
312
+ "78": "u",
313
+ "79": "v",
314
+ "80": "w",
315
+ "81": "x",
316
+ "82": "y",
317
+ "83": "z",
318
+ "84": "{",
319
+ "85": "}",
320
+ "86": "÷",
321
+ "87": "Б",
322
+ "88": "Г",
323
+ "89": "Д",
324
+ "90": "Ж",
325
+ "91": "З",
326
+ "92": "И",
327
+ "93": "Й",
328
+ "94": "Л",
329
+ "95": "О",
330
+ "96": "Ф",
331
+ "97": "Ч",
332
+ "98": "Ш",
333
+ "99": "Ю",
334
+ "100": "Я",
335
+ "101": "д",
336
+ "102": "ж",
337
+ "103": "ш",
338
+ "104": "٣",
339
+ "105": "٤",
340
+ "106": "٩",
341
+ "107": "ข",
342
+ "108": "ถ",
343
+ "109": "บ",
344
+ "110": "ร",
345
+ "111": "ว",
346
+ "112": "ห",
347
+ "113": "与",
348
+ "114": "养",
349
+ "115": "決",
350
+ "116": "海",
351
+ "117": "的",
352
+ "118": "窄",
353
+ "0": "<blank>"
354
+ },
355
+ "ctc_blank_idx": 0,
356
+ "max_text_len": 30
357
+ }
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Versi paket dikunci untuk stabilitas build
2
+ flask==2.3.2
3
+ flask-cors
4
+ gunicorn==21.2.0
5
+ numpy==1.24.3
6
+ Pillow==10.0.0
7
+ # Versi PyTorch tanpa flag spesifik, lebih portabel
8
+ torch==2.0.1
9
+ torchvision==0.15.2
10
+ timm
11
+ albumentations
12
+ opencv-python-headless==4.8.0.74