{"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":"datasetVersion","sourceId":13890590,"datasetId":8849520,"databundleVersionId":14655819}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import subprocess\nsubprocess.run([\"pip\", \"install\",\n    \"transformers==4.41.0\",\n    \"sentencepiece\",\n    \"sacrebleu==2.3.1\",\n    \"nltk==3.9.1\",\n    \"tqdm==4.67.0\",\n    \"--quiet\"\n], check=True)\n\nimport torch\nimport transformers\nimport nltk\nnltk.download(\"punkt\", quiet=True)\nnltk.download(\"punkt_tab\", quiet=True)\n\nprint(f\"PyTorch       : {torch.__version__}\")\nprint(f\"Transformers  : {transformers.__version__}\")\nprint(f\"GPU           : {torch.cuda.get_device_name(0)}\")\nprint(f\"VRAM          : {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB\")\nprint(\"All libraries ready\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T14:15:12.62966Z","iopub.execute_input":"2026-03-18T14:15:12.630366Z","iopub.status.idle":"2026-03-18T14:15:33.038474Z","shell.execute_reply.started":"2026-03-18T14:15:12.630334Z","shell.execute_reply":"2026-03-18T14:15:33.037709Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport re\nimport json\nimport warnings\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm import tqdm\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom transformers import (\n    BertTokenizer,\n    EncoderDecoderModel,\n    get_linear_schedule_with_warmup,\n    logging as hf_logging,\n)\nfrom torch.optim import AdamW\nimport sacrebleu\nimport nltk\nwarnings.filterwarnings(\"ignore\")\nhf_logging.set_verbosity_error()\n\nSEED = 42\ntorch.manual_seed(SEED)\nnp.random.seed(SEED)\ntorch.backends.cudnn.deterministic = True\ntorch.backends.cudnn.benchmark     = False\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nDATASET_PATH = Path(\"/kaggle/input/datasets/psewmuthu/how2sign-holistic/how2sign_holistic_features\")\nOUTPUT_PATH  = Path(\"/kaggle/working/signverse\")\nMODEL_PATH   = OUTPUT_PATH / \"bert_asl_model\"\nLOG_PATH     = OUTPUT_PATH / \"logs\"\n\nfor p in [OUTPUT_PATH, MODEL_PATH, LOG_PATH]:\n    p.mkdir(parents=True, exist_ok=True)\n\n# ── Auto-detect checkpoint directory ─────────────────────────────\n# Today   : checkpoints are in /kaggle/working (current session)\n# Tomorrow: checkpoints are in /kaggle/input   (saved version)\n\nWORKING_CKPT = Path(\"/kaggle/working/signverse/checkpoints\")\nSAVED_CKPT   = None\n\n# Search all saved notebook outputs for checkpoints\nfor input_dir in Path(\"/kaggle/input\").rglob(\"signverse/checkpoints\"):\n    if input_dir.exists() and any(input_dir.glob(\"epoch_*\")):\n        SAVED_CKPT = input_dir\n        break\n\n# Decide which checkpoint dir to use\nif SAVED_CKPT is not None:\n    CHECKPOINT_DIR = SAVED_CKPT\n    print(f\"Saved checkpoint found : {CHECKPOINT_DIR}\")\nelse:\n    CHECKPOINT_DIR = WORKING_CKPT\n    CHECKPOINT_DIR.mkdir(parents=True, exist_ok=True)\n    print(f\"Using working checkpoint dir : {CHECKPOINT_DIR}\")\n\nCFG = {\n    \"bert_model\"   : \"bert-base-uncased\",\n    \"max_src_len\"  : 64,\n    \"max_tgt_len\"  : 64,\n    \"batch_size\"   : 32,\n    \"epochs\"       : 15,\n    \"lr\"           : 2e-5,\n    \"warmup_ratio\" : 0.1,\n    \"weight_decay\" : 0.01,\n    \"grad_clip\"    : 1.0,\n    \"num_beams\"    : 4,\n}\n\nprint(f\"Device         : {DEVICE}\")\nprint(f\"GPU            : {torch.cuda.get_device_name(0)}\")\nprint(f\"Output         : {OUTPUT_PATH}\")\nprint(f\"Checkpoint dir : {CHECKPOINT_DIR}\")\nprint(f\"Config         : {json.dumps(CFG, indent=2)}\")\nprint(\"Cell 2 complete\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T14:16:45.933729Z","iopub.execute_input":"2026-03-18T14:16:45.934497Z","iopub.status.idle":"2026-03-18T14:21:07.37875Z","shell.execute_reply.started":"2026-03-18T14:16:45.934466Z","shell.execute_reply":"2026-03-18T14:21:07.378098Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"Searching for dataset...\")\nprint(\"\\n/kaggle/input contents:\")\nfor item in sorted(os.listdir(\"/kaggle/input\")):\n    print(f\"  {item}\")\n\nprint(\"\\n/kaggle/input full tree:\")\nfor root, dirs, files in os.walk(\"/kaggle/input\"):\n    level = root.replace(\"/kaggle/input\", \"\").count(os.sep)\n    indent = \"  \" * level\n    print(f\"{indent}{os.path.basename(root)}/\")\n    subindent = \"  \" * (level + 1)\n    for file in files[:5]:\n        print(f\"{subindent}{file}\")\n    if len(files) > 5:\n        print(f\"{subindent}... and {len(files)-5} more files\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T14:21:55.182075Z","iopub.execute_input":"2026-03-18T14:21:55.182555Z","iopub.status.idle":"2026-03-18T14:22:21.38401Z","shell.execute_reply.started":"2026-03-18T14:21:55.182531Z","shell.execute_reply":"2026-03-18T14:22:21.383239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n# CELL 3 — Dataset structure analysis + raw CSV diagnostic\n# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\nDATASET_PATH  = Path(\"/kaggle/input/datasets/psewmuthu/how2sign-holistic/how2sign_holistic_features\")\nMETADATA_PATH = DATASET_PATH / \"metadata\"\n\nprint(\"=\" * 65)\nprint(\"  HOW2SIGN HOLISTIC — FILE STRUCTURE ANALYSIS\")\nprint(\"=\" * 65)\nprint(f\"\\nDataset path exists : {DATASET_PATH.exists()}\")\n\n# ── Count npy files per split ─────────────────────────────────────\nfor split in [\"train\", \"val\", \"test\"]:\n    for view in [\"frontal\", \"side\"]:\n        folder = DATASET_PATH / split / view\n        if folder.exists():\n            count = len(list(folder.glob(\"*.npy\")))\n            print(f\"  {split:5s} / {view:8s} : {count:6d} npy files\")\n\n# ── Sample npy analysis ───────────────────────────────────────────\nprint(\"\\n--- NPY SAMPLE ANALYSIS ---\")\nsample_path = DATASET_PATH / \"train\" / \"frontal\"\nnpy_files   = sorted(sample_path.glob(\"*.npy\"))\n\nif npy_files:\n    sample = np.load(npy_files[0], allow_pickle=True)\n    print(f\"File         : {npy_files[0].name}\")\n    print(f\"Shape        : {sample.shape}\")\n    print(f\"Dtype        : {sample.dtype}\")\n    print(f\"Min          : {sample.min():.4f}\")\n    print(f\"Max          : {sample.max():.4f}\")\n    print(f\"\\nShape means  :\")\n    print(f\"  {sample.shape[0]:4d} = number of frames\")\n    print(f\"  {sample.shape[1]:4d} = keypoints per frame\")\n    print(f\"  {sample.shape[2]:4d} = coordinates (x, y, z)\")\n    print(f\"\\nKeypoint breakdown (MediaPipe Holistic):\")\n    print(f\"  468 = face landmarks\")\n    print(f\"   33 = pose/body landmarks\")\n    print(f\"   21 = left hand landmarks\")\n    print(f\"   21 = right hand landmarks\")\n    print(f\"  ─────────────────────────\")\n    print(f\"  543 = total keypoints\")\n\n    print(f\"\\nSample clip names (first 5):\")\n    for f in npy_files[:5]:\n        print(f\"  {f.name}\")\n\n# ── Raw CSV diagnostic ────────────────────────────────────────────\nprint(\"\\n--- RAW CSV CONTENT ---\")\nfor fpath in sorted(METADATA_PATH.glob(\"*.csv\")):\n    print(f\"\\n{'=' * 65}\")\n    print(f\"FILE: {fpath.name}\")\n    print(f\"{'=' * 65}\")\n    with open(fpath, \"r\", encoding=\"utf-8\", errors=\"ignore\") as f:\n        for i, line in enumerate(f):\n            if i >= 8:\n                break\n            print(f\"  Line {i}: {repr(line)}\")\n\nprint(\"\\nCell 3 complete\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T14:22:49.348514Z","iopub.execute_input":"2026-03-18T14:22:49.348894Z","iopub.status.idle":"2026-03-18T14:22:50.420045Z","shell.execute_reply.started":"2026-03-18T14:22:49.348865Z","shell.execute_reply":"2026-03-18T14:22:50.419278Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import re\nimport json\n\nMETADATA_PATH = DATASET_PATH / \"metadata\"\n\ndef load_how2sign_csv(fpath):\n    df = pd.read_csv(\n        fpath,\n        sep=\"\\t\",\n        quoting=3,\n        encoding=\"utf-8\",\n        on_bad_lines=\"skip\",\n    )\n    df.columns = [c.strip() for c in df.columns]\n    return df\n\nprint(\"=\" * 65)\nprint(\"  LOADING HOW2SIGN METADATA\")\nprint(\"=\" * 65)\n\ntrain_df = load_how2sign_csv(METADATA_PATH / \"how2sign_realigned_train.csv\")\nval_df   = load_how2sign_csv(METADATA_PATH / \"how2sign_realigned_val.csv\")\ntest_df  = load_how2sign_csv(METADATA_PATH / \"how2sign_realigned_test.csv\")\n\nfor name, df in [(\"train\", train_df), (\"val\", val_df), (\"test\", test_df)]:\n    print(f\"{name:5s} | shape={df.shape} | columns={list(df.columns)}\")\n\n\nclass ASLGrammarTransformerV2:\n    ARTICLES     = {\"a\", \"an\", \"the\"}\n    AUXILIARIES  = {\n        \"is\",\"are\",\"was\",\"were\",\"am\",\"be\",\"been\",\"being\",\n        \"do\",\"does\",\"did\",\"has\",\"have\",\"had\",\n        \"will\",\"would\",\"could\",\"should\",\"shall\",\n        \"may\",\"might\",\"must\",\"can\",\"ought\",\n    }\n    PREPOSITIONS = {\n        \"to\",\"of\",\"in\",\"at\",\"on\",\"for\",\"with\",\n        \"by\",\"from\",\"into\",\"onto\",\"upon\",\"about\",\n        \"over\",\"under\",\"through\",\"between\",\"among\",\n        \"within\",\"without\",\"against\",\"toward\",\"towards\",\n        \"along\",\"across\",\"behind\",\"beside\",\"beyond\",\n        \"during\",\"except\",\"inside\",\"outside\",\"since\",\n        \"than\",\"until\",\"unto\",\"up\",\"via\",\"worth\",\n    }\n    WH_WORDS     = {\n        \"what\",\"where\",\"when\",\"who\",\"why\",\n        \"how\",\"which\",\"whom\",\"whose\",\n    }\n    TIME_WORDS   = {\n        \"yesterday\",\"today\",\"tomorrow\",\"now\",\"later\",\n        \"before\",\"after\",\"soon\",\"recently\",\"always\",\n        \"never\",\"sometimes\",\"already\",\"still\",\"yet\",\n        \"last\",\"next\",\"every\",\"daily\",\"weekly\",\"monthly\",\n        \"morning\",\"afternoon\",\"evening\",\"night\",\n        \"monday\",\"tuesday\",\"wednesday\",\"thursday\",\n        \"friday\",\"saturday\",\"sunday\",\"ago\",\"since\",\n    }\n    CONTRACTIONS = {\n        \"don't\":\"do not\",    \"doesn't\":\"does not\",\n        \"didn't\":\"did not\",  \"won't\":\"will not\",\n        \"can't\":\"cannot\",    \"isn't\":\"is not\",\n        \"aren't\":\"are not\",  \"wasn't\":\"was not\",\n        \"weren't\":\"were not\",\"i'm\":\"i am\",\n        \"i've\":\"i have\",     \"i'll\":\"i will\",\n        \"i'd\":\"i would\",     \"you're\":\"you are\",\n        \"you've\":\"you have\", \"you'll\":\"you will\",\n        \"he's\":\"he is\",      \"she's\":\"she is\",\n        \"it's\":\"it is\",      \"we're\":\"we are\",\n        \"they're\":\"they are\",\"there's\":\"there is\",\n        \"that's\":\"that is\",  \"let's\":\"let us\",\n        \"here's\":\"here is\",  \"how's\":\"how is\",\n        \"what's\":\"what is\",  \"who's\":\"who is\",\n        \"we've\":\"we have\",   \"they've\":\"they have\",\n        \"they'll\":\"they will\",\"we'll\":\"we will\",\n        \"we'd\":\"we would\",   \"you'd\":\"you would\",\n    }\n\n    def transform(self, sentence: str) -> str:\n        if not sentence or str(sentence).lower() == \"nan\":\n            return \"\"\n        s = str(sentence).strip().lower()\n        for k, v in self.CONTRACTIONS.items():\n            s = s.replace(k, v)\n        tokens = re.findall(r\"[a-z]+\", s)\n        if not tokens:\n            return \"\"\n        is_wh   = tokens[0] in self.WH_WORDS\n        negated = \"not\" in tokens\n        tokens  = [t for t in tokens if t != \"not\"]\n        tokens  = [t for t in tokens if t not in self.ARTICLES]\n        if is_wh:\n            rest   = [t for t in tokens[1:] if t not in self.AUXILIARIES]\n            tokens = [tokens[0]] + rest\n        else:\n            tokens = [t for t in tokens if t not in self.AUXILIARIES]\n        tokens     = [t for t in tokens if t not in self.PREPOSITIONS]\n        time_toks  = [t for t in tokens if t in self.TIME_WORDS]\n        other_toks = [t for t in tokens if t not in self.TIME_WORDS]\n        tokens     = time_toks + other_toks\n        if negated:\n            tokens.append(\"not\")\n        return \" \".join(tokens).upper()\n\n\ntransformer = ASLGrammarTransformerV2()\n\nprint(\"\\n\" + \"=\" * 65)\nprint(\"  ASL GRAMMAR TRANSFORMER V2 — VALIDATION\")\nprint(\"=\" * 65)\n\nchecks = [\n    (\"Are you hungry?\",                \"YOU HUNGRY\"),\n    (\"I do not understand.\",           \"I UNDERSTAND NOT\"),\n    (\"What is your name?\",             \"WHAT YOUR NAME\"),\n    (\"Yesterday I went to school.\",    \"YESTERDAY I WENT SCHOOL\"),\n    (\"Where is the bathroom?\",         \"WHERE BATHROOM\"),\n    (\"Today the weather is nice.\",     \"TODAY WEATHER NICE\"),\n    (\"He does not know the answer.\",   \"HE KNOW ANSWER NOT\"),\n    (\"She is going to the store.\",     \"SHE GOING STORE\"),\n    (\"Tomorrow we will go shopping.\",  \"TOMORROW WE GO SHOPPING\"),\n    (\"Please help me find the door.\",  \"PLEASE HELP ME FIND DOOR\"),\n    (\"I love learning sign language.\", \"I LOVE LEARNING SIGN LANGUAGE\"),\n]\n\npassed = 0\nfor eng, expected in checks:\n    result = transformer.transform(eng)\n    match  = \"PASS\" if result == expected else \"DIFF\"\n    if match == \"PASS\":\n        passed += 1\n    print(f\"  [{match}] {eng}\")\n    if match == \"DIFF\":\n        print(f\"         Got      : {result}\")\n        print(f\"         Expected : {expected}\")\n\nprint(f\"\\n  Score: {passed}/{len(checks)} passed\")\n\n\ndef rebuild_pairs(df, split):\n    pairs          = []\n    missing        = 0\n    frontal_folder = DATASET_PATH / split / \"frontal\"\n    side_folder    = DATASET_PATH / split / \"side\"\n    for _, row in df.iterrows():\n        sentence      = str(row.get(\"SENTENCE\", \"\")).strip()\n        sentence_name = str(row.get(\"SENTENCE_NAME\", \"\")).strip()\n        if not sentence or sentence.lower() == \"nan\":\n            continue\n        if not sentence_name or sentence_name.lower() == \"nan\":\n            continue\n        frontal_npy = frontal_folder / f\"{sentence_name}_holistic.npy\"\n        side_name   = sentence_name.replace(\"_front\", \"_side\")\n        side_npy    = side_folder   / f\"{side_name}_holistic.npy\"\n        if frontal_npy.exists():\n            npy_path = frontal_npy\n        elif side_npy.exists():\n            npy_path = side_npy\n        else:\n            missing += 1\n            continue\n        gloss = transformer.transform(sentence)\n        if not gloss:\n            continue\n        pairs.append({\n            \"sentence_name\" : sentence_name,\n            \"english\"       : sentence,\n            \"gloss\"         : gloss,\n            \"npy_path\"      : str(npy_path),\n            \"split\"         : split,\n        })\n    print(f\"  {split:5s} | matched={len(pairs):5d} | missing={missing}\")\n    return pairs\n\n\nprint(\"\\n\" + \"=\" * 65)\nprint(\"  BUILDING PAIRS\")\nprint(\"=\" * 65)\n\ntrain_pairs = rebuild_pairs(train_df, \"train\")\nval_pairs   = rebuild_pairs(val_df,   \"val\")\ntest_pairs  = rebuild_pairs(test_df,  \"test\")\n\nprint(f\"\\n  Total pairs : {len(train_pairs)+len(val_pairs)+len(test_pairs):,}\")\n\njson.dump(train_pairs, open(OUTPUT_PATH/\"train_pairs.json\",\"w\"), indent=2)\njson.dump(val_pairs,   open(OUTPUT_PATH/\"val_pairs.json\",  \"w\"), indent=2)\njson.dump(test_pairs,  open(OUTPUT_PATH/\"test_pairs.json\", \"w\"), indent=2)\n\nfrom transformers import BertTokenizer\nfrom torch.utils.data import Dataset, DataLoader\n\ntokenizer = BertTokenizer.from_pretrained(CFG[\"bert_model\"])\nprint(f\"\\n  Vocab size    : {tokenizer.vocab_size:,}\")\nprint(f\"  CLS token id  : {tokenizer.cls_token_id}\")\nprint(f\"  SEP token id  : {tokenizer.sep_token_id}\")\nprint(f\"  PAD token id  : {tokenizer.pad_token_id}\")\n\n\nclass SignVerseTextDataset(Dataset):\n    def __init__(self, pairs, tokenizer, max_src, max_tgt):\n        self.pairs     = [p for p in pairs\n                          if p.get(\"english\",\"\").strip()\n                          and p.get(\"gloss\",\"\").strip()]\n        self.tokenizer = tokenizer\n        self.max_src   = max_src\n        self.max_tgt   = max_tgt\n\n    def __len__(self):\n        return len(self.pairs)\n\n    def __getitem__(self, idx):\n        pair = self.pairs[idx]\n        src  = self.tokenizer(\n            pair[\"english\"],\n            max_length=self.max_src,\n            padding=\"max_length\",\n            truncation=True,\n            return_tensors=\"pt\",\n        )\n        tgt  = self.tokenizer(\n            pair[\"gloss\"],\n            max_length=self.max_tgt,\n            padding=\"max_length\",\n            truncation=True,\n            return_tensors=\"pt\",\n        )\n        labels = tgt[\"input_ids\"].squeeze().clone()\n        labels[labels == self.tokenizer.pad_token_id] = -100\n        return {\n            \"input_ids\"      : src[\"input_ids\"].squeeze(),\n            \"attention_mask\" : src[\"attention_mask\"].squeeze(),\n            \"labels\"         : labels,\n            \"english\"        : pair[\"english\"],\n            \"gloss\"          : pair[\"gloss\"],\n        }\n\n\ntrain_ds = SignVerseTextDataset(\n    train_pairs, tokenizer, CFG[\"max_src_len\"], CFG[\"max_tgt_len\"]\n)\nval_ds   = SignVerseTextDataset(\n    val_pairs, tokenizer, CFG[\"max_src_len\"], CFG[\"max_tgt_len\"]\n)\ntest_ds  = SignVerseTextDataset(\n    test_pairs, tokenizer, CFG[\"max_src_len\"], CFG[\"max_tgt_len\"]\n)\n\ntrain_loader = DataLoader(\n    train_ds, batch_size=CFG[\"batch_size\"],\n    shuffle=True, num_workers=2, pin_memory=True\n)\nval_loader   = DataLoader(\n    val_ds, batch_size=CFG[\"batch_size\"],\n    shuffle=False, num_workers=2, pin_memory=True\n)\ntest_loader  = DataLoader(\n    test_ds, batch_size=CFG[\"batch_size\"],\n    shuffle=False, num_workers=2, pin_memory=True\n)\n\nprint(\"\\n\" + \"=\" * 65)\nprint(\"  DATASET SUMMARY\")\nprint(\"=\" * 65)\nprint(f\"  Train : {len(train_ds):6,} samples | {len(train_loader):4,} batches\")\nprint(f\"  Val   : {len(val_ds):6,} samples | {len(val_loader):4,} batches\")\nprint(f\"  Test  : {len(test_ds):6,} samples | {len(test_loader):4,} batches\")\n\nb = next(iter(train_loader))\nprint(f\"\\n  Batch check:\")\nprint(f\"    input_ids shape : {b['input_ids'].shape}\")\nprint(f\"    labels shape    : {b['labels'].shape}\")\nprint(f\"    Sample English  : {b['english'][0][:60]}\")\nprint(f\"    Sample Gloss    : {b['gloss'][0][:60]}\")\n\nprint(\"\\nCell 4 complete\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T14:25:25.936715Z","iopub.execute_input":"2026-03-18T14:25:25.93752Z","iopub.status.idle":"2026-03-18T14:25:55.837549Z","shell.execute_reply.started":"2026-03-18T14:25:25.937491Z","shell.execute_reply":"2026-03-18T14:25:55.83687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport shutil\n\nCHECKPOINT_DIR = Path(\"/kaggle/working/signverse/checkpoints\")\nCHECKPOINT_DIR.mkdir(parents=True, exist_ok=True)\n\ndef save_checkpoint(model, tokenizer, epoch, val_loss, history):\n    ckpt_path = CHECKPOINT_DIR / f\"epoch_{epoch:02d}\"\n    ckpt_path.mkdir(parents=True, exist_ok=True)\n    model.save_pretrained(ckpt_path)\n    tokenizer.save_pretrained(ckpt_path)\n    meta = {\n        \"epoch\"    : epoch,\n        \"val_loss\" : val_loss,\n        \"history\"  : history,\n    }\n    json.dump(meta, open(ckpt_path / \"meta.json\", \"w\"), indent=2)\n    print(f\"    Checkpoint saved: epoch_{epoch:02d} | val_loss={val_loss:.4f}\")\n\n\ndef find_latest_checkpoint():\n    if not CHECKPOINT_DIR.exists():\n        return None\n    checkpoints = sorted(CHECKPOINT_DIR.glob(\"epoch_*\"))\n    if not checkpoints:\n        return None\n    latest    = checkpoints[-1]\n    meta_file = latest / \"meta.json\"\n    if not meta_file.exists():\n        return None\n    meta = json.load(open(meta_file))\n    print(f\"Found checkpoint : {latest.name}\")\n    print(f\"  Epoch          : {meta['epoch']}\")\n    print(f\"  Val loss       : {meta['val_loss']:.4f}\")\n    return meta[\"epoch\"], meta[\"val_loss\"], meta[\"history\"], latest\n\n\nprint(\"=\" * 65)\nprint(\"  CHECKING FOR EXISTING CHECKPOINTS\")\nprint(\"=\" * 65)\n\nresult = find_latest_checkpoint()\n\nif result is not None and len(result) == 4:\n    start_epoch, best_val_loss, history, ckpt_path = result\n    print(f\"\\nResuming from epoch {start_epoch + 1}\")\n    model = EncoderDecoderModel.from_pretrained(\n        str(ckpt_path)\n    ).to(DEVICE)\n    model.config.decoder_start_token_id = tokenizer.cls_token_id\n    model.config.pad_token_id           = tokenizer.pad_token_id\n    model.config.eos_token_id           = tokenizer.sep_token_id\nelse:\n    start_epoch   = 0\n    best_val_loss = float(\"inf\")\n    history       = {\"train_loss\": [], \"val_loss\": []}\n    print(\"\\nNo checkpoint found — starting from scratch\")\n    model = EncoderDecoderModel.from_encoder_decoder_pretrained(\n        CFG[\"bert_model\"],\n        CFG[\"bert_model\"],\n        tie_encoder_to_decoder=False,\n    ).to(DEVICE)\n    model.config.decoder_start_token_id = tokenizer.cls_token_id\n    model.config.pad_token_id           = tokenizer.pad_token_id\n    model.config.eos_token_id           = tokenizer.sep_token_id\n    model.config.vocab_size             = model.config.encoder.vocab_size\n    model.config.max_length             = CFG[\"max_tgt_len\"]\n    model.config.no_repeat_ngram_size   = 3\n    model.config.length_penalty         = 1.0\n    model.config.num_beams              = CFG[\"num_beams\"]\n    model.config.early_stopping         = True\n\ntotal     = sum(p.numel() for p in model.parameters())\ntrainable = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f\"\\nTotal parameters     : {total:,}\")\nprint(f\"Trainable parameters : {trainable:,}\")\nprint(f\"Model size estimate  : {total * 4 / 1e9:.2f} GB\")\n\noptimizer = AdamW(\n    model.parameters(),\n    lr=CFG[\"lr\"],\n    weight_decay=CFG[\"weight_decay\"],\n    eps=1e-8,\n)\n\nremaining_epochs = CFG[\"epochs\"] - start_epoch\ntotal_steps      = len(train_loader) * remaining_epochs\nwarmup_steps     = int(total_steps   * CFG[\"warmup_ratio\"])\n\nscheduler = get_linear_schedule_with_warmup(\n    optimizer,\n    num_warmup_steps=warmup_steps,\n    num_training_steps=total_steps,\n)\nscaler = torch.cuda.amp.GradScaler(\n    enabled=torch.cuda.is_available()\n)\n\nprint(f\"\\nOptimizer    : AdamW  lr={CFG['lr']}\")\nprint(f\"Start epoch  : {start_epoch + 1}\")\nprint(f\"End epoch    : {CFG['epochs']}\")\nprint(f\"Total steps  : {total_steps:,}\")\n\n\ndef train_one_epoch(model, loader, optimizer, scheduler, scaler, device):\n    model.train()\n    total_loss, steps = 0.0, 0\n    pbar = tqdm(loader, desc=\"  Train\", leave=False)\n    for batch in pbar:\n        input_ids      = batch[\"input_ids\"].to(device)\n        attention_mask = batch[\"attention_mask\"].to(device)\n        labels         = batch[\"labels\"].to(device)\n        optimizer.zero_grad()\n        with torch.cuda.amp.autocast(\n            enabled=torch.cuda.is_available()\n        ):\n            outputs = model(\n                input_ids=input_ids,\n                attention_mask=attention_mask,\n                labels=labels,\n            )\n            loss = outputs.loss\n        scaler.scale(loss).backward()\n        scaler.unscale_(optimizer)\n        torch.nn.utils.clip_grad_norm_(\n            model.parameters(), CFG[\"grad_clip\"]\n        )\n        scaler.step(optimizer)\n        scaler.update()\n        scheduler.step()\n        total_loss += loss.item()\n        steps      += 1\n        pbar.set_postfix(loss=f\"{loss.item():.4f}\")\n    return total_loss / steps\n\n\ndef validate(model, loader, device):\n    model.eval()\n    total_loss, steps = 0.0, 0\n    with torch.no_grad():\n        for batch in tqdm(loader, desc=\"  Val  \", leave=False):\n            input_ids      = batch[\"input_ids\"].to(device)\n            attention_mask = batch[\"attention_mask\"].to(device)\n            labels         = batch[\"labels\"].to(device)\n            with torch.cuda.amp.autocast(\n                enabled=torch.cuda.is_available()\n            ):\n                outputs = model(\n                    input_ids=input_ids,\n                    attention_mask=attention_mask,\n                    labels=labels,\n                )\n            total_loss += outputs.loss.item()\n            steps      += 1\n    return total_loss / steps\n\n\npatience = 0\nPATIENCE = 4\n\nprint(\"\\n\" + \"=\" * 65)\nprint(f\"  TRAINING | epochs={CFG['epochs']} | batch={CFG['batch_size']} | device={DEVICE}\")\nprint(\"=\" * 65)\nprint(f\"  {'Epoch':>5}  {'Train Loss':>11}  {'Val Loss':>10}  {'Status':>20}\")\nprint(f\"  {'─'*58}\")\n\nfor epoch in range(start_epoch + 1, CFG[\"epochs\"] + 1):\n\n    tr_loss = train_one_epoch(\n        model, train_loader,\n        optimizer, scheduler, scaler, DEVICE\n    )\n    vl_loss = validate(model, val_loader, DEVICE)\n\n    history[\"train_loss\"].append(tr_loss)\n    history[\"val_loss\"].append(vl_loss)\n\n    save_checkpoint(model, tokenizer, epoch, vl_loss, history)\n\n    if vl_loss < best_val_loss:\n        best_val_loss = vl_loss\n        patience      = 0\n        model.save_pretrained(MODEL_PATH)\n        tokenizer.save_pretrained(MODEL_PATH)\n        status = \"best saved\"\n    else:\n        patience += 1\n        status    = f\"patience {patience}/{PATIENCE}\"\n        if patience >= PATIENCE:\n            print(f\"\\n  Early stopping at epoch {epoch}\")\n            break\n\n    print(f\"  {epoch:>5}  {tr_loss:>11.4f}  {vl_loss:>10.4f}  {status:>20}\")\n\njson.dump(history, open(LOG_PATH / \"training_history.json\", \"w\"), indent=2)\n\nprint(f\"\\n  Best val loss : {best_val_loss:.4f}\")\nprint(f\"  Model saved   : {MODEL_PATH}\")\nprint(f\"  Checkpoints   : {CHECKPOINT_DIR}\")\nprint(\"Cell 5 complete\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nCHECKPOINT_DIR = Path(\"/kaggle/working/signverse/checkpoints\")\nMODEL_PATH     = Path(\"/kaggle/working/signverse/bert_asl_model\")\n\nprint(f\"Model exists      : {MODEL_PATH.exists()}\")\nprint(f\"Checkpoints exist : {CHECKPOINT_DIR.exists()}\")\n\ncheckpoints = sorted(CHECKPOINT_DIR.glob(\"epoch_*\"))\nprint(f\"Total checkpoints : {len(checkpoints)}\")\nfor c in checkpoints:\n    print(f\"  {c.name}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:30:39.782525Z","iopub.execute_input":"2026-03-18T16:30:39.783509Z","iopub.status.idle":"2026-03-18T16:30:39.78954Z","shell.execute_reply.started":"2026-03-18T16:30:39.783474Z","shell.execute_reply":"2026-03-18T16:30:39.788835Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from google.colab import auth\nfrom googleapiclient.discovery import build\nfrom googleapiclient.http import MediaFileUpload\nimport os\n\nauth.authenticate_user()\n\ndrive_service = build('drive', 'v3')\n\ndef upload_to_drive(file_path, folder_name=\"signverse_model\"):\n    file_path = str(file_path)\n    file_name = os.path.basename(file_path)\n    folder_query = f\"name='{folder_name}' and mimeType='application/vnd.google-apps.folder'\"\n    folders = drive_service.files().list(q=folder_query).execute()\n    if folders['files']:\n        folder_id = folders['files'][0]['id']\n    else:\n        folder = drive_service.files().create(\n            body={\n                'name': folder_name,\n                'mimeType': 'application/vnd.google-apps.folder'\n            }\n        ).execute()\n        folder_id = folder['id']\n    media = MediaFileUpload(file_path, resumable=True)\n    file_metadata = {\n        'name': file_name,\n        'parents': [folder_id]\n    }\n    file = drive_service.files().create(\n        body=file_metadata,\n        media_body=media,\n        fields='id'\n    ).execute()\n    print(f\"Uploaded: {file_name}\")\n    return file['id']\n\n\nMODEL_PATH = \"/kaggle/working/signverse/bert_asl_model\"\n\nprint(\"Uploading model files to Google Drive...\")\n\nfor f in os.listdir(MODEL_PATH):\n    full_path = os.path.join(MODEL_PATH, f)\n    if os.path.isfile(full_path):\n        size_mb = os.path.getsize(full_path) / 1e6\n        print(f\"Uploading {f} ({size_mb:.1f} MB)...\")\n        upload_to_drive(full_path)\n\nprint(\"All files uploaded to Google Drive\")\nprint(\"Check your Drive signverse_model folder\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-18T16:41:08.14922Z","iopub.execute_input":"2026-03-18T16:41:08.149788Z","iopub.status.idle":"2026-03-18T16:49:29.608959Z","shell.execute_reply.started":"2026-03-18T16:41:08.14975Z","shell.execute_reply":"2026-03-18T16:49:29.608292Z"}},"outputs":[],"execution_count":null}]}