{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":121150,"databundleVersionId":15061024,"sourceType":"competition"},{"sourceId":9818805,"sourceType":"datasetVersion","datasetId":6020151},{"sourceId":14374989,"sourceType":"datasetVersion","datasetId":9147887},{"sourceId":14376272,"sourceType":"datasetVersion","datasetId":9181082},{"sourceId":14410665,"sourceType":"datasetVersion","datasetId":9203761},{"sourceId":282751,"sourceType":"modelInstanceVersion","modelInstanceId":239470,"modelId":222398},{"sourceId":1135,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":981,"modelId":129},{"sourceId":729093,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":555251,"modelId":567811}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ==========================================\n# INSTALLATION (RUN THIS FIRST)\n# ==========================================\n!pip install -q transformers==4.36.0\n!pip install -q sentencepiece\n!pip install -q torch\n\nprint(\"✅ Packages installed\")\n\n# ==========================================\n# CAFA Akkadian Translation - FIXED (No Tensor Errors)\n# ⭐ FIXED: Long input truncation, regex patterns, error handling\n# ==========================================\n\nimport os\nimport re\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, Dataset\nfrom transformers import (\n    AutoTokenizer, AutoModelForSeq2SeqLM, AutoModelForCausalLM,\n    AutoModel, LogitsProcessor\n)\nfrom tqdm.auto import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\nprint(\"=\"*70)\nprint(\"🏺 AKKADIAN TRANSLATION - FIXED (TENSOR-SAFE)\")\nprint(\"=\"*70)\n\n# ==========================================\n# CONFIGURATION\n# ==========================================\nclass CFG:\n    # Data\n    DATA_PATH = \"/kaggle/input/deep-past-initiative-machine-translation/test.csv\"\n    OUTPUT_PATH = \"submission.csv\"\n    \n    # Models - Original 3 ByT5 checkpoints\n    BYT5_MODELS = [\n        \"/kaggle/input/byt5-base-big-data2\",\n        \"/kaggle/input/byt5-akkadian-model\",\n        \"/kaggle/input/train-gap-all-2/byt5-base-akkadian_gap_setence2\"\n    ]\n    \n    # Local Kaggle model paths\n    QWEN_PATH = \"/kaggle/input/qwen2-5-7b\"\n    \n    # Akkadian dictionary (expanded)\n    LEXICON = {\n        \"šarru\": [\"king\"], \"šarratu\": [\"queen\"],\n        \"māru\": [\"son\"], \"mārtu\": [\"daughter\"],\n        \"bītu\": [\"house\", \"temple\", \"household\"],\n        \"kaspum\": [\"silver\", \"money\"], \n        \"ālum\": [\"city\", \"town\"],\n        \"awīlum\": [\"man\", \"person\"],\n        \"ilum\": [\"god\"], \"ištaru\": [\"goddess\"],\n        \"šamû\": [\"heaven\", \"sky\"], \"erṣetu\": [\"earth\"],\n        \"wardum\": [\"slave\", \"servant\"], \"amtum\": [\"female slave\"],\n        \"tuppum\": [\"tablet\"], \"ṭuppum\": [\"tablet\"],\n        \"kānišum\": [\"Kanesh\"], \"aššur\": [\"Ashur\"],\n        \"šamšum\": [\"sun\"], \"šīnum\": [\"silver\"],\n    }\n    \n    # ⭐ SAFETY LIMITS\n    MAX_INPUT_LENGTH = 800  # Chars before truncation\n    MAX_OUTPUT_LENGTH = 512  # Tokens\n    \n    # Optimization flags\n    USE_ENSEMBLE = True\n    USE_CONSTRAINED_DECODING = True\n    USE_LLM_POSTEDIT = True\n    \n    # Hardware\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    BATCH_SIZE = 4 if torch.cuda.is_available() else 1\n    NUM_WORKERS = 2\n    \n    # Generation parameters\n    NUM_BEAMS = 12\n    NUM_RETURN_SEQUENCES = 5\n    MAX_NEW_TOKENS = 512\n    LENGTH_PENALTY = 1.10\n    \n    # Template markers for cleaning\n    TEMPLATE_MARKERS = [\n        \"You are an expert\",\n        \"RULES:\",\n        \"Given the following\",\n        \"Source:\",\n        \"Translate it\",\n        \"Do not add explanations\",\n        \"Be concise and clear\"\n    ]\n\nprint(f\"Device: {CFG.DEVICE}\")\nprint(f\"Optimizations enabled:\")\nprint(f\"  - Ensemble: {CFG.USE_ENSEMBLE}\")\nprint(f\"  - Constrained Decoding: {CFG.USE_CONSTRAINED_DECODING}\")\nprint(f\"  - LLM Post-Edit: {CFG.USE_LLM_POSTEDIT}\")\nprint(f\"  - ⭐ Input Safety: ENABLED (max {CFG.MAX_INPUT_LENGTH} chars)\")\nprint(\"=\"*70)\n\n# ==========================================\n# PREPROCESSING (⭐ FIXED)\n# ==========================================\ndef preprocess_transliteration(text):\n    \"\"\"Preprocess with safety checks for extreme inputs\"\"\"\n    if pd.isna(text): \n        return \"\"\n    \n    processed = str(text)\n    \n    # ⭐ FIX 1: TRUNCATE EXTREME INPUTS\n    if len(processed) > CFG.MAX_INPUT_LENGTH:\n        print(f\"  ⚠️ Truncating long input: {len(processed)} → {CFG.MAX_INPUT_LENGTH} chars\")\n        processed = processed[:CFG.MAX_INPUT_LENGTH] + \" <big_gap>\"\n    \n    # ⭐ FIX 2: MORE SPECIFIC GAP PATTERNS (avoid \"bigggg\" in names)\n    # Only match ACTUAL gap notation, not repeated letters in names\n    processed = re.sub(r'(\\[\\.{3,}\\]|…{2,}|\\.{5,})', '<big_gap>', processed)\n    processed = re.sub(r'(xx+|\\s+x\\s+|\\[x+\\])', '<gap>', processed)\n    \n    # Clean up consecutive gaps\n    processed = re.sub(r'<gap>(\\s*<gap>)+', '<gap>', processed)\n    processed = re.sub(r'<big_gap>(\\s*<big_gap>)+', '<big_gap>', processed)\n    \n    return processed.strip()\n\ndef extract_akkadian_words(text):\n    \"\"\"Extract potential Akkadian words for dictionary lookup\"\"\"\n    clean = re.sub(r'<\\w+>', '', text)\n    words = re.findall(r'\\b[a-zšṣṭḫāēīū]+\\b', clean, re.IGNORECASE)\n    return words\n\n# ==========================================\n# POSTPROCESSING (Enhanced)\n# ==========================================\ndef postprocess_translation(text):\n    if not isinstance(text, str) or not text.strip(): \n        return \"\"\n    \n    processed = text.replace('ḫ', 'h').replace('Ḫ', 'H')\n    sub_map = str.maketrans(\"₀₁₂₃₄₅₆₇₈₉\", \"0123456789\")\n    processed = processed.translate(sub_map)\n\n    processed = re.sub(r'(\\[x\\]|\\(x\\)|\\bx\\b)', '<gap>', processed, flags=re.I)\n    processed = re.sub(r'(\\.{3,}|…|\\[\\.+\\])', '<big_gap>', processed)\n    \n    processed = re.sub(r'<gap>\\s*<gap>', ' <big_gap> ', processed)\n    processed = re.sub(r'<big_gap>\\s*<big_gap>', ' <big_gap> ', processed)\n\n    processed = re.sub(r'\\((fem|plur|pl|sing|singular|plural|\\?|!)\\.?\\s*\\w*\\)', '', processed, flags=re.I)\n\n    processed = processed.replace('<gap>', '\\x00GAP\\x00').replace('<big_gap>', '\\x00BIG\\x00')\n    \n    bad_chars = '!?()\"—–<>⌈⌋⌊[]+ʾ/;'\n    processed = processed.translate(str.maketrans('', '', bad_chars))\n\n    processed = processed.replace('\\x00GAP\\x00', ' <gap> ').replace('\\x00BIG\\x00', ' <big_gap> ')\n\n    frac_map = {\n        r'\\.5\\b': ' ½', r'\\.25\\b': ' ¼', r'\\.75\\b': ' ¾',\n        r'\\.33+\\d*\\b': ' ⅓', r'\\.66+\\d*\\b': ' ⅔'\n    }\n    for pat, rep in frac_map.items():\n        processed = re.sub(r'(\\d+)' + pat, r'\\1' + rep, processed)\n        processed = re.sub(r'\\b0' + pat, rep.strip(), processed)\n\n    # Remove repeated words/phrases\n    processed = re.sub(r'\\b(\\w+)(?:\\s+\\1\\b)+', r'\\1', processed)\n    for n in range(4, 1, -1):\n        pat = r'\\b((?:\\w+\\s+){' + str(n-1) + r'}\\w+)(?:\\s+\\1\\b)+'\n        processed = re.sub(pat, r'\\1', processed)\n\n    return re.sub(r'\\s+', ' ', processed).strip().strip('-')\n\n# ⭐ Clean template leakage\ndef remove_template_contamination(text, source_text):\n    \"\"\"Remove system prompt and source code if they leak into output\"\"\"\n    original_text = text\n    \n    # Remove template markers\n    for marker in CFG.TEMPLATE_MARKERS:\n        if marker in text:\n            parts = text.split(marker)\n            text = parts[0].strip()\n            print(f\"      ⚠️ Removed template marker: '{marker[:30]}...'\")\n    \n    # Remove source text if it leaked\n    akkadian_pattern = r'\\b[a-zšṣṭḫāēīū]{2,}-[a-zšṣṭḫāēīū]{2,}'\n    if re.search(akkadian_pattern, text):\n        sentences = text.split('.')\n        clean_sentences = []\n        for sent in sentences:\n            if re.search(akkadian_pattern, sent):\n                break\n            clean_sentences.append(sent)\n        \n        if clean_sentences:\n            text = '. '.join(clean_sentences)\n            if not text.endswith('.'):\n                text += '.'\n            print(f\"      ⚠️ Removed source text leakage\")\n    \n    # Safety check: if we removed too much, return original\n    if len(text) < 10 and len(original_text) > 50:\n        print(f\"      ⚠️ Cleaning removed too much, using ByT5 output\")\n        return None\n    \n    return text\n\n# ==========================================\n# DATASET\n# ==========================================\nclass AkkadianDataset(Dataset):\n    def __init__(self, dataframe):\n        self.ids = dataframe['id'].tolist()\n        self.transliterations = dataframe['transliteration'].tolist()\n        self.texts = [\"translate Akkadian to English: \" + str(t) for t in self.transliterations]\n    \n    def __len__(self):\n        return len(self.ids)\n    \n    def __getitem__(self, idx):\n        return self.ids[idx], self.texts[idx], self.transliterations[idx]\n\n# ==========================================\n# CONSTRAINED DECODING\n# ==========================================\nclass LexiconConstraintProcessor(LogitsProcessor):\n    \"\"\"Boost probabilities of dictionary translations\"\"\"\n    def __init__(self, tokenizer, lexicon, source_words):\n        self.tokenizer = tokenizer\n        self.lexicon = lexicon\n        self.source_words = source_words\n        self.boost_tokens = self._get_boost_tokens()\n    \n    def _get_boost_tokens(self):\n        boost = set()\n        for word in self.source_words:\n            if word.lower() in self.lexicon:\n                for translation in self.lexicon[word.lower()]:\n                    token_ids = self.tokenizer.encode(translation, add_special_tokens=False)\n                    boost.update(token_ids)\n        return boost\n    \n    def __call__(self, input_ids, scores):\n        if len(self.boost_tokens) > 0:\n            for token_id in self.boost_tokens:\n                if token_id < scores.shape[1]:\n                    scores[:, token_id] += 2.0\n        return scores\n\n# ==========================================\n# MODEL LOADING\n# ==========================================\ndef load_models():\n    \"\"\"Load all models from local Kaggle paths\"\"\"\n    print(\"\\n[1/6] Loading models...\")\n    \n    models = []\n    tokenizer = None\n    \n    for i, path in enumerate(CFG.BYT5_MODELS):\n        print(f\"  Loading ByT5 model {i+1}/3: {path.split('/')[-1]}...\")\n        model = AutoModelForSeq2SeqLM.from_pretrained(path)\n        model = model.to(CFG.DEVICE).eval()\n        models.append(model)\n        \n        if tokenizer is None:\n            tokenizer = AutoTokenizer.from_pretrained(path)\n    \n    print(f\"  ✅ ByT5 models loaded\")\n    \n    llm_model = None\n    llm_tokenizer = None\n    if CFG.USE_LLM_POSTEDIT:\n        try:\n            print(f\"  Loading Qwen from {CFG.QWEN_PATH}...\")\n            llm_tokenizer = AutoTokenizer.from_pretrained(CFG.QWEN_PATH, trust_remote_code=True)\n            llm_model = AutoModelForCausalLM.from_pretrained(\n                CFG.QWEN_PATH,\n                torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,\n                device_map=\"auto\" if torch.cuda.is_available() else None,\n                trust_remote_code=True\n            )\n            llm_model.eval()\n            print(f\"  ✅ Qwen loaded\")\n        except Exception as e:\n            print(f\"  ⚠️  Qwen loading failed: {e}\")\n            CFG.USE_LLM_POSTEDIT = False\n    \n    print(\"  ✅ All models loaded successfully\")\n    return models, tokenizer, llm_model, llm_tokenizer\n\n# ==========================================\n# ENSEMBLE GENERATION (⭐ WITH ERROR HANDLING)\n# ==========================================\n@torch.inference_mode()\ndef generate_ensemble_candidates(models, tokenizer, inputs, source_words):\n    \"\"\"Generate n-best candidates from all models\"\"\"\n    candidates = []\n    \n    for model_idx, model in enumerate(models):\n        try:\n            logits_processors = []\n            if CFG.USE_CONSTRAINED_DECODING:\n                constraint_proc = LexiconConstraintProcessor(tokenizer, CFG.LEXICON, source_words)\n                logits_processors.append(constraint_proc)\n            \n            outputs = model.generate(\n                input_ids=inputs.input_ids.to(CFG.DEVICE),\n                attention_mask=inputs.attention_mask.to(CFG.DEVICE),\n                num_beams=CFG.NUM_BEAMS,\n                num_return_sequences=CFG.NUM_RETURN_SEQUENCES,\n                max_new_tokens=CFG.MAX_NEW_TOKENS,\n                length_penalty=CFG.LENGTH_PENALTY,\n                output_scores=True,\n                return_dict_in_generate=True,\n                logits_processor=logits_processors if logits_processors else None\n            )\n            \n            for seq, score in zip(outputs.sequences, outputs.sequences_scores):\n                decoded = tokenizer.decode(seq, skip_special_tokens=True)\n                candidates.append({\n                    'text': decoded,\n                    'model_score': score.item(),\n                    'model_id': model_idx\n                })\n        except Exception as e:\n            print(f\"      ⚠️ Model {model_idx} failed: {e}\")\n            continue\n    \n    # Return best by score, or fallback\n    if candidates:\n        return max(candidates, key=lambda x: x['model_score'])\n    else:\n        return {'text': '<gap> translation failed <gap>', 'model_score': 0.0, 'model_id': -1}\n\n# ==========================================\n# LLM POST-EDITING\n# ==========================================\n@torch.inference_mode()\ndef llm_post_edit(translation, source, llm_model, llm_tokenizer):\n    \"\"\"Refine translation with Qwen - NO TEMPLATE CONTAMINATION\"\"\"\n    if not CFG.USE_LLM_POSTEDIT or llm_model is None:\n        return translation\n    \n    try:\n        # Ultra-short prompt to prevent leakage\n        system_msg = \"You are an Akkadian translator. Fix grammar errors in the English translation. Keep <gap> tokens. Output only the corrected translation, nothing else.\"\n        \n        user_msg = f\"Translation: {translation}\\n\\nCorrected:\"\n        \n        messages = [\n            {\"role\": \"system\", \"content\": system_msg},\n            {\"role\": \"user\", \"content\": user_msg}\n        ]\n        \n        text = llm_tokenizer.apply_chat_template(\n            messages,\n            tokenize=False,\n            add_generation_prompt=True\n        )\n        \n        inputs = llm_tokenizer([text], return_tensors=\"pt\", max_length=768, truncation=True)\n        inputs = {k: v.to(llm_model.device) for k, v in inputs.items()}\n        \n        # Conservative generation\n        outputs = llm_model.generate(\n            **inputs,\n            max_new_tokens=200,\n            temperature=0.3,\n            do_sample=True,\n            top_p=0.9,\n            repetition_penalty=1.2,\n            pad_token_id=llm_tokenizer.pad_token_id or llm_tokenizer.eos_token_id,\n            eos_token_id=llm_tokenizer.eos_token_id\n        )\n        \n        # Only decode new tokens\n        input_length = inputs['input_ids'].shape[1]\n        generated_ids = outputs[0][input_length:]\n        response = llm_tokenizer.decode(generated_ids, skip_special_tokens=True)\n        corrected = response.strip()\n        \n        # Aggressive template removal\n        corrected = remove_template_contamination(corrected, source)\n        \n        if corrected is None:\n            return translation\n        \n        # Clean common artifacts\n        if \"Corrected:\" in corrected:\n            corrected = corrected.split(\"Corrected:\")[-1].strip()\n        if \"Translation:\" in corrected:\n            corrected = corrected.split(\"Translation:\")[-1].strip()\n        \n        # Remove incomplete sentences at the end\n        if corrected and not corrected[-1] in '.!?':\n            last_period = max(corrected.rfind('.'), corrected.rfind('!'), corrected.rfind('?'))\n            if last_period > len(corrected) * 0.5:\n                corrected = corrected[:last_period + 1]\n        \n        # Safety checks\n        if len(corrected) < 5:\n            print(f\"      ⚠️ LLM output too short, using original\")\n            return translation\n        \n        if len(corrected) > len(translation) * 2.5:\n            print(f\"      ⚠️ LLM output too long, using original\")\n            return translation\n        \n        # Check if gaps were preserved\n        source_has_gaps = '<gap>' in translation or '<big_gap>' in translation\n        output_has_gaps = '<gap>' in corrected or '<big_gap>' in corrected\n        \n        if source_has_gaps and not output_has_gaps:\n            print(f\"      ⚠️ LLM removed gaps, using original\")\n            return translation\n        \n        return corrected\n    \n    except Exception as e:\n        print(f\"    ⚠️  LLM post-edit failed: {e}\")\n        return translation\n\n# ==========================================\n# MAIN PIPELINE (⭐ WITH FULL ERROR HANDLING)\n# ==========================================\ndef main():\n    import time\n    start_time = time.time()\n    \n    print(\"\\n[2/6] Loading data...\")\n    df = pd.read_csv(CFG.DATA_PATH)\n    df['transliteration'] = df['transliteration'].apply(preprocess_transliteration)\n    print(f\"  Loaded {len(df)} samples\")\n    \n    models, tokenizer, llm_model, llm_tokenizer = load_models()\n    \n    dataset = AkkadianDataset(df)\n    dataloader = DataLoader(\n        dataset,\n        batch_size=1,\n        shuffle=False,\n        num_workers=0\n    )\n    \n    print(\"\\n[3/6] Running translation pipeline...\")\n    results = []\n    failed_count = 0\n    \n    for sample_id, text, source in tqdm(dataloader, desc=\"  Translating\"):\n        # ✅ FIX: Convert tensors to Python types\n        sample_id = int(sample_id[0])  # tensor → int\n        text = text[0]  # List → string\n        source = source[0]  # List → string\n        \n        # ⭐ FIX 3: FULL TRY-CATCH PER SAMPLE\n        try:\n            source_words = extract_akkadian_words(source)\n            inputs = tokenizer(text, return_tensors=\"pt\", max_length=CFG.MAX_OUTPUT_LENGTH, truncation=True)\n            \n            # Safety check: ensure inputs are valid\n            if inputs.input_ids.shape[1] == 0:\n                raise ValueError(\"Empty input after tokenization\")\n            \n            # Strategy 1: Ensemble\n            if CFG.USE_ENSEMBLE:\n                best_candidate = generate_ensemble_candidates(models, tokenizer, inputs, source_words)\n                translation = best_candidate['text']\n            else:\n                outputs = models[0].generate(\n                    input_ids=inputs.input_ids.to(CFG.DEVICE),\n                    attention_mask=inputs.attention_mask.to(CFG.DEVICE),\n                    num_beams=CFG.NUM_BEAMS,\n                    max_new_tokens=CFG.MAX_NEW_TOKENS,\n                    length_penalty=CFG.LENGTH_PENALTY\n                )\n                translation = tokenizer.decode(outputs[0], skip_special_tokens=True)\n            \n            translation = postprocess_translation(translation)\n            \n            # Strategy 2: LLM refinement\n            if CFG.USE_LLM_POSTEDIT and len(translation) > 10:\n                refined = llm_post_edit(translation, source, llm_model, llm_tokenizer)\n                refined = postprocess_translation(refined)\n                \n                # Use refined only if it's reasonable\n                if 10 < len(refined) < len(translation) * 2:\n                    translation = refined\n            \n            # Final safety: ensure non-empty\n            if not translation or len(translation) < 3:\n                translation = \"<gap> translation unavailable <gap>\"\n            \n            # ✅ Append as tuple of (int, str)\n            results.append((sample_id, translation))\n        \n        except Exception as e:\n            failed_count += 1\n            print(f\"\\n    ❌ Failed on {sample_id}: {e}\")\n            \n            # ⭐ SMART FALLBACK\n            fallback_parts = []\n            \n            if source_words:\n                known_translations = []\n                for word in source_words[:5]:\n                    if word.lower() in CFG.LEXICON:\n                        known_translations.extend(CFG.LEXICON[word.lower()])\n                \n                if known_translations:\n                    fallback_parts.append(' '.join(known_translations[:3]))\n            \n            numbers = re.findall(r'\\d+', source)\n            if numbers:\n                fallback_parts.append(f\"involving {numbers[0]}\")\n            \n            if fallback_parts:\n                fallback = ' '.join(fallback_parts) + \" <big_gap>\"\n            else:\n                fallback = \"From <gap> to <gap> <big_gap>\"\n            \n            # ✅ Append as tuple of (int, str)\n            results.append((sample_id, fallback))\n            continue\n    \n    print(f\"\\n  Translation complete: {len(results)} total, {failed_count} failures\")\n    \n    print(\"\\n[4/6] Creating submission...\")\n    submission = pd.DataFrame(results, columns=['id', 'translation'])\n    \n    print(\"\\n[5/6] Quality checks...\")\n    print(f\"  Total translations: {len(submission)}\")\n    print(f\"  Avg length: {submission['translation'].str.len().mean():.1f} chars\")\n    print(f\"  Empty translations: {(submission['translation'].str.len() == 0).sum()}\")\n    print(f\"  Has <gap> tokens: {submission['translation'].str.contains('<gap>').sum()}\")\n    \n    print(\"\\n[6/6] Saving submission...\")\n    submission.to_csv(CFG.OUTPUT_PATH, index=False)\n    \n    total_time = (time.time() - start_time) / 60\n    \n    print(\"\\n✅ Complete!\")\n    print(\"=\"*70)\n    print(f\"📁 Output: {CFG.OUTPUT_PATH}\")\n    print(f\"⏱️  Total Time: {total_time:.1f} minutes\")\n    print(f\"❌ Failed: {failed_count}/{len(submission)}\")\n    print(f\"\\n📊 Sample translations:\")\n    print(submission.head(10).to_string(index=False))\n    print(\"=\"*70)\n    \n    return submission\n\nif __name__ == '__main__':\n    result = main()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}