{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":16017,"databundleVersionId":708189}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":449.347207,"end_time":"2026-05-05T15:45:47.212100+00:00","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-05-05T15:38:17.864893+00:00","version":"2.7.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"299233cf","cell_type":"markdown","source":"# Kannada-MNIST: Small CNN from Scratch\n\n**Approach:** A compact 3-layer CNN (~300k parameters) trained from scratch to reach **97%+ accuracy** in under 5 minutes on a Colab T4 GPU.\n\n- **Skills:** Small CNNs, training from scratch\n- **No transfer learning** — the model is built and trained entirely from scratch\n- **10 classes, ~65k images** (60k train + 5k test)\n\n## Datasets\n| File | Description |\n|---|---|\n| `train.csv` | 60,000 training samples (label + 784 pixels) |\n| `test.csv` | 5,000 competition test samples (id + 784 pixels, no labels) |\n| `sample_submission.csv` | Submission format (id + label) |","metadata":{"papermill":{"duration":0.003731,"end_time":"2026-05-05T15:38:20.372250+00:00","exception":false,"start_time":"2026-05-05T15:38:20.368519+00:00","status":"completed"},"tags":[]}},{"id":"52f7cc2e","cell_type":"code","source":"!pip install -q onnx tf2onnx","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:13:42.095679Z","iopub.execute_input":"2026-05-05T19:13:42.096446Z","iopub.status.idle":"2026-05-05T19:13:45.896221Z","shell.execute_reply.started":"2026-05-05T19:13:42.096414Z","shell.execute_reply":"2026-05-05T19:13:45.895022Z"}},"outputs":[],"execution_count":null},{"id":"f0a99a12","cell_type":"markdown","source":"## 1. Import Libraries","metadata":{"papermill":{"duration":0.002566,"end_time":"2026-05-05T15:38:20.377573+00:00","exception":false,"start_time":"2026-05-05T15:38:20.375007+00:00","status":"completed"},"tags":[]}},{"id":"75aede48","cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport os\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'\nimport tensorflow as tf\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import (\n    Conv2D, BatchNormalization, MaxPooling2D,\n    Dropout, Flatten, Dense\n)\nfrom tensorflow.keras.utils import to_categorical\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping\nfrom tensorflow.keras.optimizers import Adam\nfrom sklearn.model_selection import train_test_split\n\nprint('TensorFlow version:', tf.__version__)\nprint('GPU available:', tf.config.list_physical_devices('GPU'))","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:13:45.898419Z","iopub.execute_input":"2026-05-05T19:13:45.898863Z","iopub.status.idle":"2026-05-05T19:13:45.907185Z","shell.execute_reply.started":"2026-05-05T19:13:45.898828Z","shell.execute_reply":"2026-05-05T19:13:45.906203Z"},"papermill":{"duration":30.046724,"end_time":"2026-05-05T15:38:50.426924+00:00","exception":false,"start_time":"2026-05-05T15:38:20.380200+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"646c67a6","cell_type":"markdown","source":"## 2. Load and Explore Data","metadata":{"papermill":{"duration":0.002775,"end_time":"2026-05-05T15:38:50.432719+00:00","exception":false,"start_time":"2026-05-05T15:38:50.429944+00:00","status":"completed"},"tags":[]}},{"id":"75489479","cell_type":"code","source":"BASE_PATH = '/kaggle/input/competitions/Kannada-MNIST/'\ntrain_df = pd.read_csv(BASE_PATH + 'train.csv')\ntest_df = pd.read_csv(BASE_PATH + 'test.csv')\nsample_sub = pd.read_csv(BASE_PATH + 'sample_submission.csv')\n\nprint('train_df shape:', train_df.shape)\nprint('test_df shape:', test_df.shape)\nprint('sample_sub shape:', sample_sub.shape)","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:13:45.908453Z","iopub.execute_input":"2026-05-05T19:13:45.908827Z","iopub.status.idle":"2026-05-05T19:13:49.248278Z","shell.execute_reply.started":"2026-05-05T19:13:45.908799Z","shell.execute_reply":"2026-05-05T19:13:49.247242Z"},"papermill":{"duration":4.282701,"end_time":"2026-05-05T15:38:54.719307+00:00","exception":false,"start_time":"2026-05-05T15:38:50.436606+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"25495214","cell_type":"code","source":"# Peek at the data\nprint('\\n--- train_df ---')\nprint(train_df.head())\nprint('\\nLabel distribution (train):')\nprint(train_df['label'].value_counts().sort_index())","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:13:49.249521Z","iopub.execute_input":"2026-05-05T19:13:49.249843Z","iopub.status.idle":"2026-05-05T19:13:49.262674Z","shell.execute_reply.started":"2026-05-05T19:13:49.249817Z","shell.execute_reply":"2026-05-05T19:13:49.261964Z"},"papermill":{"duration":0.035905,"end_time":"2026-05-05T15:38:54.758990+00:00","exception":false,"start_time":"2026-05-05T15:38:54.723085+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"65bf4ab9","cell_type":"markdown","source":"## 3. Data Preprocessing","metadata":{"papermill":{"duration":0.003155,"end_time":"2026-05-05T15:38:55.252829+00:00","exception":false,"start_time":"2026-05-05T15:38:55.249674+00:00","status":"completed"},"tags":[]}},{"id":"49378897","cell_type":"code","source":"# --- Training data ---\ny_full = train_df['label'].values\nX_full = train_df.drop('label', axis=1).values.astype('float32') / 255.0\n\n# --- Competition test data (no labels) ---\nX_test_comp = test_df.drop('id', axis=1).values.astype('float32') / 255.0\n\n# Train / validation split (80/20)\nX_train, X_val, y_train, y_val = train_test_split(\n    X_full, y_full, test_size=0.2, random_state=42, stratify=y_full\n)\n\nprint(f'X_train: {X_train.shape}  y_train: {y_train.shape}')\nprint(f'X_val  : {X_val.shape}  y_val  : {y_val.shape}')\nprint(f'X_test_comp: {X_test_comp.shape}')","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:13:49.265442Z","iopub.execute_input":"2026-05-05T19:13:49.265729Z","iopub.status.idle":"2026-05-05T19:13:50.111635Z","shell.execute_reply.started":"2026-05-05T19:13:49.265702Z","shell.execute_reply":"2026-05-05T19:13:50.110470Z"},"papermill":{"duration":0.811874,"end_time":"2026-05-05T15:38:56.067815+00:00","exception":false,"start_time":"2026-05-05T15:38:55.255941+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"7cf410a4","cell_type":"code","source":"# Reshape to (N, 28, 28, 1) for CNN input\nX_train = X_train.reshape(-1, 28, 28, 1)\nX_val   = X_val.reshape(-1, 28, 28, 1)\nX_test_comp = X_test_comp.reshape(-1, 28, 28, 1)\n\n# One-hot encode labels\nnum_classes = 10\ny_train_cat = to_categorical(y_train, num_classes)\ny_val_cat   = to_categorical(y_val,   num_classes)\n\nprint('Shapes after reshape:')\nprint(f'  X_train: {X_train.shape}  y_train_cat: {y_train_cat.shape}')\nprint(f'  X_val  : {X_val.shape}  y_val_cat  : {y_val_cat.shape}')","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:13:50.112914Z","iopub.execute_input":"2026-05-05T19:13:50.113270Z","iopub.status.idle":"2026-05-05T19:13:50.121073Z","shell.execute_reply.started":"2026-05-05T19:13:50.113234Z","shell.execute_reply":"2026-05-05T19:13:50.120184Z"},"papermill":{"duration":0.013673,"end_time":"2026-05-05T15:38:56.085279+00:00","exception":false,"start_time":"2026-05-05T15:38:56.071606+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"38fbb6f2","cell_type":"markdown","source":"## 4. Build the 3-Layer CNN (~300k params)\n\nThe architecture is intentionally kept **small and efficient**:\n\n| Block | Layers |\n|---|---|\n| Conv Block 1 | Conv2D(32, 3×3) → BN → ReLU → Conv2D(32, 3×3) → BN → ReLU → MaxPool(2×2) → Dropout(0.25) |\n| Conv Block 2 | Conv2D(64, 3×3) → BN → ReLU → Conv2D(64, 3×3) → BN → ReLU → MaxPool(2×2) → Dropout(0.25) |\n| Conv Block 3 | Conv2D(128, 3×3) → BN → ReLU → MaxPool(2×2) → Dropout(0.25) |\n| Head | Flatten → Dense(128) → BN → ReLU → Dropout(0.5) → Dense(10, softmax) |\n\nThis yields roughly **300k trainable parameters** — well within the small-CNN budget.","metadata":{"papermill":{"duration":0.009858,"end_time":"2026-05-05T15:38:56.098764+00:00","exception":false,"start_time":"2026-05-05T15:38:56.088906+00:00","status":"completed"},"tags":[]}},{"id":"de0c5759","cell_type":"code","source":"def build_model(input_shape=(28, 28, 1), num_classes=10):\n    model = Sequential([\n        # --- Block 1 ---\n        Conv2D(32, (3, 3), padding='same', activation='relu',\n               input_shape=input_shape),\n        BatchNormalization(),\n        Conv2D(32, (3, 3), padding='same', activation='relu'),\n        BatchNormalization(),\n        MaxPooling2D((2, 2)),\n        Dropout(0.25),\n\n        # --- Block 2 ---\n        Conv2D(64, (3, 3), padding='same', activation='relu'),\n        BatchNormalization(),\n        Conv2D(64, (3, 3), padding='same', activation='relu'),\n        BatchNormalization(),\n        MaxPooling2D((2, 2)),\n        Dropout(0.25),\n\n        # --- Block 3 ---\n        Conv2D(128, (3, 3), padding='same', activation='relu'),\n        BatchNormalization(),\n        MaxPooling2D((2, 2)),\n        Dropout(0.25),\n\n        # --- Classification head ---\n        Flatten(),\n        Dense(128, activation='relu'),\n        BatchNormalization(),\n        Dropout(0.5),\n        Dense(num_classes, activation='softmax'),\n    ])\n    return model\n\nmodel = build_model()\nmodel.summary()","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:13:50.122564Z","iopub.execute_input":"2026-05-05T19:13:50.122926Z","iopub.status.idle":"2026-05-05T19:13:50.325037Z","shell.execute_reply.started":"2026-05-05T19:13:50.122850Z","shell.execute_reply":"2026-05-05T19:13:50.324287Z"},"papermill":{"duration":1.906091,"end_time":"2026-05-05T15:38:58.008346+00:00","exception":false,"start_time":"2026-05-05T15:38:56.102255+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"263ad61c","cell_type":"markdown","source":"## 5. Compile the Model","metadata":{"papermill":{"duration":0.004298,"end_time":"2026-05-05T15:38:58.017153+00:00","exception":false,"start_time":"2026-05-05T15:38:58.012855+00:00","status":"completed"},"tags":[]}},{"id":"eb8be9d7","cell_type":"code","source":"optimizer = Adam(learning_rate=1e-3)\n\nmodel.compile(\n    loss='categorical_crossentropy',\n    optimizer=optimizer,\n    metrics=['accuracy']\n)","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:13:50.325975Z","iopub.execute_input":"2026-05-05T19:13:50.326312Z","iopub.status.idle":"2026-05-05T19:13:50.336699Z","shell.execute_reply.started":"2026-05-05T19:13:50.326286Z","shell.execute_reply":"2026-05-05T19:13:50.335696Z"},"papermill":{"duration":0.018069,"end_time":"2026-05-05T15:38:58.039171+00:00","exception":false,"start_time":"2026-05-05T15:38:58.021102+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"e037957c","cell_type":"markdown","source":"## 6. Data Augmentation & Callbacks","metadata":{"papermill":{"duration":0.004009,"end_time":"2026-05-05T15:38:58.047175+00:00","exception":false,"start_time":"2026-05-05T15:38:58.043166+00:00","status":"completed"},"tags":[]}},{"id":"a2dd114c","cell_type":"code","source":"# On-the-fly data augmentation for training\ndatagen_train = ImageDataGenerator(\n    rotation_range=10,\n    width_shift_range=0.1,\n    height_shift_range=0.1,\n    shear_range=0.1,\n    zoom_range=0.1,\n)\ndatagen_train.fit(X_train)\n\n# Callbacks\nreduce_lr = ReduceLROnPlateau(\n    monitor='val_loss', factor=0.5, patience=3,\n    verbose=1, min_lr=1e-6\n)\n\nearly_stop = EarlyStopping(\n    monitor='val_loss', patience=10,\n    restore_best_weights=True, verbose=1\n)\n\ncallbacks = [reduce_lr, early_stop]","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:13:50.337931Z","iopub.execute_input":"2026-05-05T19:13:50.338706Z","iopub.status.idle":"2026-05-05T19:13:50.405996Z","shell.execute_reply.started":"2026-05-05T19:13:50.338675Z","shell.execute_reply":"2026-05-05T19:13:50.405228Z"},"papermill":{"duration":0.064108,"end_time":"2026-05-05T15:38:58.115219+00:00","exception":false,"start_time":"2026-05-05T15:38:58.051111+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"39de73a5","cell_type":"markdown","source":"## 7. Train the Model\n\nWith a batch size of 256 and up to 20 epochs (early stopping will kick in), the entire training should finish in **< 5 minutes on a Colab T4**.","metadata":{"papermill":{"duration":0.004321,"end_time":"2026-05-05T15:38:58.124235+00:00","exception":false,"start_time":"2026-05-05T15:38:58.119914+00:00","status":"completed"},"tags":[]}},{"id":"0dbd0f37","cell_type":"code","source":"batch_size = 256\nepochs = 20\n\nhistory = model.fit(\n    datagen_train.flow(X_train, y_train_cat, batch_size=batch_size),\n    steps_per_epoch=len(X_train) // batch_size,\n    epochs=epochs,\n    validation_data=(X_val, y_val_cat),\n    callbacks=callbacks,\n    verbose=1\n)","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:13:50.406861Z","iopub.execute_input":"2026-05-05T19:13:50.407172Z","iopub.status.idle":"2026-05-05T19:17:13.543162Z","shell.execute_reply.started":"2026-05-05T19:13:50.407149Z","shell.execute_reply":"2026-05-05T19:17:13.542004Z"},"papermill":{"duration":395.873968,"end_time":"2026-05-05T15:45:34.002331+00:00","exception":false,"start_time":"2026-05-05T15:38:58.128363+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"40dcc2c3","cell_type":"markdown","source":"## 8. Evaluate the Model","metadata":{"papermill":{"duration":0.176119,"end_time":"2026-05-05T15:45:34.356562+00:00","exception":false,"start_time":"2026-05-05T15:45:34.180443+00:00","status":"completed"},"tags":[]}},{"id":"818d216b","cell_type":"code","source":"df = pd.DataFrame(history.history)\ndf.insert(0, 'epoch', range(1, len(df) + 1))\nprint(df)","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:17:13.544492Z","iopub.execute_input":"2026-05-05T19:17:13.544903Z","iopub.status.idle":"2026-05-05T19:17:13.555776Z","shell.execute_reply.started":"2026-05-05T19:17:13.544837Z","shell.execute_reply":"2026-05-05T19:17:13.554787Z"},"papermill":{"duration":0.461601,"end_time":"2026-05-05T15:45:34.994469+00:00","exception":false,"start_time":"2026-05-05T15:45:34.532868+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"bfbb8bc7","cell_type":"code","source":"# --- Validation set accuracy ---\nval_loss, val_acc = model.evaluate(X_val, y_val_cat, verbose=0)\nprint(f'Validation Loss: {val_loss:.4f}')\nprint(f'Validation Accuracy: {val_acc:.4f}  ({val_acc*100:.2f}%)')","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:17:13.557052Z","iopub.execute_input":"2026-05-05T19:17:13.557511Z","iopub.status.idle":"2026-05-05T19:17:14.825118Z","shell.execute_reply.started":"2026-05-05T19:17:13.557464Z","shell.execute_reply":"2026-05-05T19:17:14.823790Z"},"papermill":{"duration":1.374309,"end_time":"2026-05-05T15:45:36.544137+00:00","exception":false,"start_time":"2026-05-05T15:45:35.169828+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2f923709","cell_type":"code","source":"from sklearn.metrics import confusion_matrix, classification_report\nimport pandas as pd\nimport numpy as np\n\ny_val_pred = model.predict(X_val, verbose=0)\ny_val_pred_classes = np.argmax(y_val_pred, axis=1)\n\n# Confusion Matrix\ncm = confusion_matrix(y_val, y_val_pred_classes)\n\n# Convert to DataFrame for nicer printing\ncm_df = pd.DataFrame(cm,\n                     index=[f'True_{i}' for i in range(10)],\n                     columns=[f'Pred_{i}' for i in range(10)])\n\nprint(\"\\nConfusion Matrix (Validation Set):\")\nprint(cm_df)\n\n# Classification Report\nprint(\"\\nClassification Report (Validation):\")\nreport = classification_report(y_val, y_val_pred_classes, output_dict=True)\n\n# Convert to DataFrame for table format\nreport_df = pd.DataFrame(report).transpose()\nprint(report_df.round(4))","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:17:14.826393Z","iopub.execute_input":"2026-05-05T19:17:14.826783Z","iopub.status.idle":"2026-05-05T19:17:16.663649Z","shell.execute_reply.started":"2026-05-05T19:17:14.826744Z","shell.execute_reply":"2026-05-05T19:17:16.662572Z"},"papermill":{"duration":2.724697,"end_time":"2026-05-05T15:45:40.944941+00:00","exception":false,"start_time":"2026-05-05T15:45:38.220244+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b8ada7d3","cell_type":"markdown","source":"## 9. Generate Competition Submission","metadata":{"papermill":{"duration":0.174158,"end_time":"2026-05-05T15:45:41.296303+00:00","exception":false,"start_time":"2026-05-05T15:45:41.122145+00:00","status":"completed"},"tags":[]}},{"id":"cac09111","cell_type":"code","source":"# Predict on competition test set\ntest_pred = model.predict(X_test_comp, verbose=0)\ntest_pred_classes = np.argmax(test_pred, axis=1)\n\nsubmission = pd.DataFrame({\n    'id': test_df['id'],\n    'label': test_pred_classes\n})\n\nsubmission.to_csv('submission.csv', index=False)\nprint('Submission saved!')\nprint(submission.head(10))","metadata":{"execution":{"iopub.status.busy":"2026-05-05T19:17:16.666135Z","iopub.execute_input":"2026-05-05T19:17:16.666698Z","iopub.status.idle":"2026-05-05T19:17:17.893008Z","shell.execute_reply.started":"2026-05-05T19:17:16.666671Z","shell.execute_reply":"2026-05-05T19:17:17.892099Z"},"papermill":{"duration":1.585961,"end_time":"2026-05-05T15:45:43.056867+00:00","exception":false,"start_time":"2026-05-05T15:45:41.470906+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2ba1edc1","cell_type":"markdown","source":"## 10. Conclusion\n\nA small 3-layer CNN with ~300k parameters achieves **97%+ validation accuracy** on the Kannada-MNIST dataset, trained from scratch in under 5 minutes on a Colab T4 GPU. No transfer learning was used.\n\nKey design decisions:\n- **3 convolutional blocks** with increasing filter counts (32 → 64 → 128)\n- **Batch Normalization** after each Conv2D for stable, fast training\n- **Dropout** (0.25 in conv blocks, 0.5 in dense head) for regularization\n- **On-the-fly data augmentation** (rotation, shift, shear, zoom) to improve generalization\n- **ReduceLROnPlateau + EarlyStopping** callbacks for efficient training\n- **Adam optimizer** with learning rate 1e-3","metadata":{"papermill":{"duration":0.175555,"end_time":"2026-05-05T15:45:43.405692+00:00","exception":false,"start_time":"2026-05-05T15:45:43.230137+00:00","status":"completed"},"tags":[]}},{"id":"96909dd9","cell_type":"markdown","source":"## 11. Export Model and Config for Web App","metadata":{}},{"id":"21639b3c","cell_type":"code","source":"import tf2onnx\nimport onnx\nimport tensorflow as tf\n\n# In Keras 3, converting directly from Keras model sometimes fails with KeyError.\n# A robust workaround is to convert from a tf.function trace instead:\n@tf.function\ndef serve(x):\n    return model(x)\n\nspec = (tf.TensorSpec((None, 28, 28, 1), tf.float32, name=\"input\"),)\noutput_path = \"kannada_cnn.onnx\"\nmodel_proto, _ = tf2onnx.convert.from_function(serve, input_signature=spec, opset=13, output_path=output_path)\nprint(\"ONNX model saved!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:18:53.237653Z","iopub.execute_input":"2026-05-05T19:18:53.238922Z","iopub.status.idle":"2026-05-05T19:18:53.883439Z","shell.execute_reply.started":"2026-05-05T19:18:53.238857Z","shell.execute_reply":"2026-05-05T19:18:53.882583Z"}},"outputs":[],"execution_count":null},{"id":"a7be1cec","cell_type":"code","source":"# ── GENERATE .TS FILES FOR LOVABLE APP ─────────────────────────────────────\n\nmodel_config_ts = f\"\"\"export const MODEL_CONFIG = {{\n  modelName: \"KannadaCNN\",\n  version: \"1.0.0\",\n  inputShape: [1, 28, 28, 1],\n  inputSize: 28,\n  numClasses: 10,\n  classLabels: [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"],\n  bestValAccuracy: {val_acc * 100:.3f},\n  modelPath: \"/kannada_cnn.onnx\" // Make sure to place the model in the public folder\n}};\n\"\"\"\n\npreprocessing_ts = \"\"\"export function preprocessCanvas(imageData: ImageData): Float32Array {\n  const { data, width, height } = imageData;\n  const tensor = new Float32Array(width * height);\n\n  // Convert RGBA to Grayscale and normalize to [0, 1]\n  for (let i = 0; i < data.length; i += 4) {\n    const r = data[i];\n    const g = data[i + 1];\n    const b = data[i + 2];\n    const a = data[i + 3];\n\n    // Grayscale conversion based on luminance\n    const gray = (r * 0.299 + g * 0.587 + b * 0.114);\n\n    // Since images in Kannada-MNIST are typically dark strokes on light background\n    // If your canvas is white-on-black, no inversion is needed.\n    // Assuming user draws black on white canvas:\n    let pixelValue = 255 - gray; \n\n    // Normalize to [0, 1] as expected by the model\n    tensor[i / 4] = pixelValue / 255.0;\n  }\n\n  return tensor;\n}\n\"\"\"\n\nmodel_loader_ts = \"\"\"import * as ort from 'onnxruntime-web';\nimport { MODEL_CONFIG } from './model-config';\nimport { preprocessCanvas } from './preprocessing';\n\nexport class ModelLoader {\n  private session: ort.InferenceSession | null = null;\n\n  async loadModel() {\n    try {\n      this.session = await ort.InferenceSession.create(MODEL_CONFIG.modelPath, {\n        executionProviders: ['wasm']\n      });\n      console.log(\"ONNX Model loaded successfully.\");\n    } catch (e) {\n      console.error(\"Failed to load ONNX model\", e);\n    }\n  }\n\n  async predict(imageData: ImageData): Promise<number> {\n    if (!this.session) {\n      throw new Error(\"Model is not loaded yet.\");\n    }\n\n    const preprocessedData = preprocessCanvas(imageData);\n    const tensor = new ort.Tensor('float32', preprocessedData, MODEL_CONFIG.inputShape);\n\n    const feeds: Record<string, ort.Tensor> = {};\n    feeds[this.session.inputNames[0]] = tensor;\n\n    const results = await this.session.run(feeds);\n    const outputTensor = results[this.session.outputNames[0]];\n    const outputData = outputTensor.data as Float32Array;\n\n    // Argmax\n    let maxVal = -Infinity;\n    let maxIdx = -1;\n    for (let i = 0; i < outputData.length; i++) {\n      if (outputData[i] > maxVal) {\n        maxVal = outputData[i];\n        maxIdx = i;\n      }\n    }\n\n    return maxIdx;\n  }\n}\n\"\"\"\n\nwith open(\"model-config.ts\", \"w\") as f: f.write(model_config_ts)\nwith open(\"preprocessing.ts\", \"w\") as f: f.write(preprocessing_ts)\nwith open(\"model-loader.ts\", \"w\") as f: f.write(model_loader_ts)\n\nprint(\"TS files (model-config.ts, preprocessing.ts, model-loader.ts) generated successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T19:19:00.132489Z","iopub.execute_input":"2026-05-05T19:19:00.133431Z","iopub.status.idle":"2026-05-05T19:19:00.142578Z","shell.execute_reply.started":"2026-05-05T19:19:00.133395Z","shell.execute_reply":"2026-05-05T19:19:00.141586Z"}},"outputs":[],"execution_count":null}]}