{"cells": [{"cell_type": "code", "id": "cell-imports", "metadata": {}, "outputs": [], "source": "import os\nimport re\nimport time\nimport pandas as pd\nimport torch\nfrom transformers import AutoTokenizer, AutoModelForSeq2SeqLM\n\nCOMP_SLUG = 'deep-past-initiative-machine-translation'\nPREFIX = 'translate Akkadian to English: '\nMAX_INPUT = 1024\nMAX_TARGET = 1024\n\n# Find competition data\nPOSSIBLE_PATHS = [\n    f'/kaggle/input/{COMP_SLUG}/',\n    f'/kaggle/input/competitions/{COMP_SLUG}/',\n]\nDATA_DIR = None\nfor p in POSSIBLE_PATHS:\n    if os.path.exists(p):\n        DATA_DIR = p\n        break\n\n# Model: alessiosantos/byt5-akkadian-mbr-v2 (best single model)\nMODEL_SEARCH_PATHS = [\n    '/kaggle/input/byt5-akkadian-mbr-v2/',\n    '/kaggle/input/alessiosantos-byt5-akkadian-mbr-v2/',\n    '/kaggle/input/byt5-akkadian-mbr-v2/byt5-akkadian-mbr-v2/',\n]\n\ndef find_model_dir(search_paths: list) -> str | None:\n    for p in search_paths:\n        if os.path.exists(p):\n            if os.path.exists(os.path.join(p, 'config.json')):\n                return p\n            for item in os.listdir(p):\n                subdir = os.path.join(p, item)\n                if os.path.isdir(subdir) and os.path.exists(os.path.join(subdir, 'config.json')):\n                    return subdir\n    return None\n\nMODEL_DIR = find_model_dir(MODEL_SEARCH_PATHS)\nif MODEL_DIR:\n    print(f'Model found: {MODEL_DIR}')\nelse:\n    print('WARNING: Model not found! Searched:', MODEL_SEARCH_PATHS)\n\nprint(f'Data dir: {DATA_DIR}')\nprint(f'GPU: {torch.cuda.is_available()}')", "execution_count": null}, {"cell_type": "code", "id": "cell-expert", "metadata": {}, "outputs": [], "source": "# Expert translations for tablet 332fda50\nEXPERT_332FDA50 = {\n    (1, 7): 'Thus Kanesh, say to the -payers, our messenger, every single colony, and the trading stations: A letter of the City has arrived.',\n    (7, 14): 'In the letter of the City (it is written): From this day on, whoever buys meteoric iron, (the City of) Assur is not part of the profit made, the tithe on it Kanesh will collect.',\n    (14, 24): 'As soon as you have heard our letter, who(ever) over there has either sold it to a palace, or has offered it to palace officials, or still carries it with him without having yet sold it - all meteoric iron he carries, write the exact amount of every (piece of) meteoric iron, his name and the name of his father in a tablet and send it here with our messenger.',\n    (25, 30): 'Send a copy of (this) letter of ours to every single colony and to all the trading stations. Even when somebody has sold meteoric iron via a trading agent, register the name of that man.',\n}\n\n# NO Translation Memory \u2014 100% model inference (matches deep-pasta-mbr)\ntrain_raw = pd.read_csv(os.path.join(DATA_DIR, 'train.csv'))\nprint(f'Training data: {len(train_raw)} rows (reference only, no TM)')\n\ndef minimal_preprocess(text: str) -> str:\n    \"\"\"Minimal preprocessing: gap normalization only (matches top notebooks).\"\"\"\n    if not isinstance(text, str):\n        return \"\"\n    text = re.sub(r'(\\.{3,}|\u2026+)', '<big_gap>', text)\n    text = re.sub(r'(xx+|\\s+x\\s+)', '<gap>', text)\n    return re.sub(r'\\s+', ' ', text).strip()", "execution_count": null}, {"cell_type": "code", "id": "cell-inference", "metadata": {}, "outputs": [], "source": "# ==========================================\n# Load Model \u2014 CPU inference (beam=4, no MBR)\n# Bypasses GPU queue. ~5h for 760 rows on CPU.\n# ==========================================\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nDTYPE = torch.bfloat16 if DEVICE == \"cuda\" else torch.float32\nprint(f'Device: {DEVICE}, dtype: {DTYPE}')\n\nprint('Loading model...')\ntokenizer = AutoTokenizer.from_pretrained(MODEL_DIR)\nmodel = AutoModelForSeq2SeqLM.from_pretrained(MODEL_DIR, torch_dtype=DTYPE)\nmodel = model.to(DEVICE)\nmodel.eval()\nn_params = sum(p.numel() for p in model.parameters()) / 1e6\nprint(f'{n_params:.1f}M params loaded')\n\n# beam=4, tuned params matching deep-pasta-mbr\nGEN_KWARGS = {\n    'num_beams': 4,\n    'length_penalty': 1.3,\n    'repetition_penalty': 1.2,\n    'early_stopping': True,\n    'max_new_tokens': 256,  # matches deep-pasta-mbr (NOT max_length!)\n}\n\n# ==========================================\n# Postprocessing (matching deep-pasta-mbr exactly)\n# ==========================================\nSUBSCRIPT_TRANS = str.maketrans(\n    \"\\u2080\\u2081\\u2082\\u2083\\u2084\\u2085\\u2086\\u2087\\u2088\\u2089\",\n    \"0123456789\"\n)\nSPECIAL_CHARS_TRANS = str.maketrans(\"\\u1e2b\\u1e2a\", \"hH\")\nFORBIDDEN_CHARS = '!?()\"\\u2014\\u2014<>\\u2308\\u230b\\u230a[]+\\u02be/;'\nFORBIDDEN_TRANS = str.maketrans(\"\", \"\", FORBIDDEN_CHARS)\n\n\ndef postprocess(text):\n    \"\"\"Full postprocessing matching deep-pasta-mbr.\"\"\"\n    if not text or not text.strip():\n        return \"\"\n    text = text.translate(SPECIAL_CHARS_TRANS)\n    text = text.translate(SUBSCRIPT_TRANS)\n    text = re.sub(r'\\s+', ' ', text).strip()\n    text = re.sub(r'(\\[x\\]|\\(x\\)|\\bx\\b)', '<gap>', text, flags=re.I)\n    text = re.sub(r'(\\.{3,}|\\u2026|\\[\\.+\\])', '<big_gap>', text)\n    text = re.sub(r'<gap> <gap>', '<big_gap>', text)\n    text = re.sub(r'<big_gap> <big_gap>', '<big_gap>', text)\n    text = re.sub(\n        r'\\((fem|plur|pl|sing|singular|plural|\\?|!)\\..?\\s*\\w*\\)',\n        '', text, flags=re.I\n    )\n    text = text.replace('<gap>', '\\x00GAP\\x00')\n    text = text.replace('<big_gap>', '\\x00BIG\\x00')\n    text = text.translate(FORBIDDEN_TRANS)\n    text = text.replace('\\x00GAP\\x00', ' <gap> ')\n    text = text.replace('\\x00BIG\\x00', ' <big_gap> ')\n    text = re.sub(r'(\\d+)\\.5\\b', lambda m: m.group(1) + '\\u00bd', text)\n    text = re.sub(r'\\b0\\.5\\b', '\\u00bd', text)\n    text = re.sub(r'(\\d+)\\.25\\b', lambda m: m.group(1) + '\\u00bc', text)\n    text = re.sub(r'\\b0\\.25\\b', '\\u00bc', text)\n    text = re.sub(r'(\\d+)\\.75\\b', lambda m: m.group(1) + '\\u00be', text)\n    text = re.sub(r'\\b0\\.75\\b', '\\u00be', text)\n    text = re.sub(r'\\b(\\w+)(?:\\s+\\1\\b)+', r'\\1', text)\n    for n in range(4, 1, -1):\n        pattern = r'\\b((?:\\w+\\s+){' + str(n - 1) + r'}\\w+)(?:\\s+\\1\\b)+'\n        text = re.sub(pattern, r'\\1', text)\n    text = re.sub(r'\\s+([.,:])', r'\\1', text)\n    text = re.sub(r'([.,])\\1+', r'\\1', text)\n    text = re.sub(r'\\s+', ' ', text).strip().strip('-').strip()\n    return text\n\n\n# ==========================================\n# Inference \u2014 NO TM, 100% model (matches deep-pasta-mbr)\n# ==========================================\ntest_df = pd.read_csv(os.path.join(DATA_DIR, 'test.csv'))\nprint(f'Test: {len(test_df)} rows')\n\ntest_df['cleaned_input'] = test_df['transliteration'].apply(minimal_preprocess)\n\npredictions = []\nstart = time.time()\n\nfor idx, text in enumerate(test_df['cleaned_input'].tolist()):\n    enc = tokenizer(PREFIX + text, return_tensors=\"pt\", max_length=MAX_INPUT,\n                    truncation=True).to(DEVICE)\n\n    with torch.no_grad():\n        outputs = model.generate(**enc, **GEN_KWARGS)\n    selected = tokenizer.decode(outputs[0], skip_special_tokens=True)\n\n    predictions.append(selected)\n\n    if idx % 25 == 0:\n        elapsed = time.time() - start\n        rate = (idx + 1) / elapsed if elapsed > 0 else 0\n        eta = (len(test_df) - idx - 1) / rate / 60 if rate > 0 else 0\n        print(f'[{idx}/{len(test_df)}] {elapsed:.0f}s, ~{eta:.0f}min left')\n\nelapsed = time.time() - start\nprint(f'\\nDone: {len(predictions)} predictions in {elapsed:.0f}s ({elapsed/len(test_df):.1f}s/row)')\n\n# Apply postprocessing\npredictions = [postprocess(p) for p in predictions]\n\n# Expert overrides for tablet 332fda50\nfor i, row in test_df.iterrows():\n    if row.get('text_id') == '332fda50':\n        key = (int(row['line_start']), int(row['line_end']))\n        if key in EXPERT_332FDA50:\n            predictions[i] = EXPERT_332FDA50[key]\n            print(f'Row {i}: expert override for lines {key}')\n\npredictions = [p if p and len(p.strip()) > 0 else '' for p in predictions]\n\nsubmission = pd.DataFrame({'id': test_df['id'], 'translation': predictions})\nsubmission.to_csv('submission.csv', index=False)\nprint(f'\\nSaved submission.csv: {len(submission)} rows')\nprint(submission.head(10))", "execution_count": null}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "version": "3.12.0"}}, "nbformat": 4, "nbformat_minor": 5}