{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":52950,"databundleVersionId":5973250}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport tensorflow as tf\nimport gc\nimport os\nfrom sklearn.model_selection import train_test_split\n\n# ==========================================\n# 1. SETUP & PATHS\n# ==========================================\nBASE_DIR = \"/kaggle/input/competitions/asl-fingerspelling\"\ntrain_df = pd.read_csv(f\"{BASE_DIR}/train.csv\")\n\n# Sample 20k for speed and stability\ntrain_df = train_df.sample(n=min(20000, len(train_df)), random_state=42)\n\n# Clean labels: Keep only valid A-Z phrases\ntrain_df = train_df[train_df['phrase'].str.match(r'^[a-zA-Z]')].copy()\ntrain_df['label_encoded'] = (\n    train_df['phrase']\n    .str[0]\n    .str.lower()\n    .apply(lambda x: ord(x) - ord('a'))\n)\n\ntrain_df = train_df[\n    (train_df['label_encoded'] >= 0) & (train_df['label_encoded'] < 26)\n].reset_index(drop=True)\n\ntrain_data, val_data = train_test_split(train_df, test_size=0.2, random_state=42)\n\n# ==========================================\n# 2. BULLETPROOF DATA GENERATOR\n# ==========================================\nclass KaggleASLGenerator(tf.keras.utils.Sequence):\n    def __init__(self, df, batch_size=32, shuffle=True):\n        self.df = df.reset_index(drop=True)\n        self.batch_size = batch_size\n        self.shuffle = shuffle\n        self.indices = np.arange(len(self.df))\n\n    def __len__(self):\n        return int(np.ceil(len(self.df) / self.batch_size))\n\n    def on_epoch_end(self):\n        if self.shuffle:\n            np.random.shuffle(self.indices)\n\n    def __getitem__(self, index):\n        batch_indices = self.indices[index * self.batch_size:(index + 1) * self.batch_size]\n        batch_df = self.df.iloc[batch_indices]\n        X, y = [], []\n\n        for _, row in batch_df.iterrows():\n            try:\n                # Load parquet and grab hand landmarks\n                data = pd.read_parquet(f\"{BASE_DIR}/{row['path']}\")\n                # Extract first 128 frames, hand landmarks (468:543)\n                hand_data = data.iloc[:128, 468:543].values\n                \n                # Immediate NaN/Inf protection\n                hand_data = np.nan_to_num(hand_data, nan=0.0, posinf=0.0, neginf=0.0)\n\n                # Pad to 128 frames if too short\n                if hand_data.shape[0] < 128:\n                    pad = np.zeros((128 - hand_data.shape[0], hand_data.shape[1]))\n                    hand_data = np.vstack([hand_data, pad])\n                else:\n                    hand_data = hand_data[:128]\n\n                # Keep only 63 features (21 landmarks * 3 coords)\n                hand_data = hand_data[:, :63]\n\n                # Scale data to keep values small (Fixes NaN Loss)\n                max_val = np.max(np.abs(hand_data)) + 1e-6\n                hand_data = hand_data / max_val\n\n                X.append(hand_data)\n                y.append(row['label_encoded'])\n                del data\n            except:\n                X.append(np.zeros((128, 63)))\n                y.append(0)\n\n        gc.collect() # Force RAM cleanup\n        return np.array(X, dtype=np.float32), np.array(y, dtype=np.int32)\n\n# ==========================================\n# 3. MODEL ARCHITECTURE\n# ==========================================\nmodel = tf.keras.Sequential([\n    tf.keras.layers.Input(shape=(128, 63)),\n    tf.keras.layers.Masking(mask_value=0.0),\n    tf.keras.layers.LSTM(128, return_sequences=True),\n    tf.keras.layers.Dropout(0.3),\n    tf.keras.layers.LSTM(64),\n    tf.keras.layers.Dropout(0.3),\n    tf.keras.layers.Dense(64, activation='relu'),\n    tf.keras.layers.Dense(26, activation='softmax')\n])\n\n# Use clipnorm to prevent mathematical \"explosions\"\noptimizer = tf.keras.optimizers.Adam(learning_rate=1e-4, clipnorm=1.0)\nmodel.compile(optimizer=optimizer, loss='sparse_categorical_crossentropy', metrics=['accuracy'])\n\n# ==========================================\n# 4. START TRAINING\n# ==========================================\ntrain_gen = KaggleASLGenerator(train_data, batch_size=32)\nval_gen = KaggleASLGenerator(val_data, batch_size=32)\n\nprint(f\"Starting Training: {len(train_data)} samples...\")\nhistory = model.fit(train_gen, validation_data=val_gen, epochs=10)\n\n# ==========================================\n# 5. EXPORT TO TFLITE (FOR WEB APP)\n# ==========================================\nprint(\"Converting to TFLite...\")\nconverter = tf.lite.TFLiteConverter.from_keras_model(model)\n# Standard settings for LSTM support in TFLite\nconverter.target_spec.supported_ops = [\n    tf.lite.OpsSet.TFLITE_BUILTINS, \n    tf.lite.OpsSet.SELECT_TF_OPS\n]\nconverter._experimental_lower_tensor_list_ops = False\n\ntflite_model = converter.convert()\nwith open(\"/kaggle/working/speakable_v3.tflite\", \"wb\") as f:\n    f.write(tflite_model)\n\nprint(\"DONE! Download 'speakable_v3.tflite' from the Output section.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-14T04:01:33.894597Z","iopub.execute_input":"2026-04-14T04:01:33.895416Z"}},"outputs":[],"execution_count":null}]}