{ "cells": [ { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Pipeline d'entraînement — Générateur de cartes Pokémon\n", "\n", "Dataset : images PNG avec métadonnées **pokemon_metadata** dans les chunks PNG (tEXt).\n", "Modèle : entrée = JSON (même format que pokemon_metadata). En amont, ta pipeline convertit le prompt utilisateur → JSON ; ici on conditionne directement sur ce JSON (sérialisé en texte)." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Using device: cuda\n", "Found 21783 file(s), validating...\n", "Valid images with pokemon_metadata: 21783\n", "Train: 19604, Val: 2179\n" ] } ], "source": [ "from pathlib import Path\n", "import json\n", "from PIL import Image, ImageFile\n", "import torch\n", "from torch.utils.data import Dataset, DataLoader, random_split\n", "from torchvision import transforms\n", "\n", "ImageFile.LOAD_TRUNCATED_IMAGES = True\n", "\n", "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"mps\" if torch.backends.mps.is_available() else \"cpu\")\n", "print(f\"Using device: {device}\")\n", "\n", "# Répertoire des images : dossiers dans cards/ (sous-dossiers autorisés)\n", "image_dir = Path(\"/home/do5-ajlp/juicepyter/cards\")\n", "image_extensions = {\".png\", \".jpg\", \".jpeg\"}\n", "\n", "def get_metadata_from_png(path):\n", " \"\"\"Lit le chunk pokemon_metadata depuis les métadonnées PNG (tEXt).\"\"\"\n", " with Image.open(path) as img:\n", " meta_str = img.info.get(\"pokemon_metadata\")\n", " if meta_str is None:\n", " return None\n", " try:\n", " return json.loads(meta_str)\n", " except json.JSONDecodeError:\n", " return None\n", "\n", "def metadata_to_conditioning(meta):\n", " \"\"\"Sérialise les metadata (dict, format pokemon_metadata) en chaîne JSON pour le conditionnement.\"\"\"\n", " return json.dumps(meta, sort_keys=True, ensure_ascii=False)\n", "\n", "all_paths = sorted(\n", " p for p in image_dir.rglob(\"*\")\n", " if p.is_file() and p.suffix.lower() in image_extensions\n", ")\n", "print(f\"Found {len(all_paths)} file(s), validating...\")\n", "\n", "image_paths = []\n", "for p in all_paths:\n", " try:\n", " meta = get_metadata_from_png(p)\n", " if meta is None:\n", " continue\n", " with Image.open(p) as img:\n", " img.verify()\n", " image_paths.append(p)\n", " except Exception:\n", " continue\n", "\n", "print(f\"Valid images with pokemon_metadata: {len(image_paths)}\")\n", "\n", "# Taille cible pour Stable Diffusion\n", "IMG_SIZE = 512\n", "\n", "transform = transforms.Compose([\n", " transforms.Resize((IMG_SIZE, IMG_SIZE)),\n", " transforms.CenterCrop(IMG_SIZE),\n", " transforms.ToTensor(),\n", " transforms.Normalize([0.5], [0.5]),\n", "])\n", "\n", "class PokemonCardDataset(Dataset):\n", " def __init__(self, image_paths, transform, tokenizer, max_length=77):\n", " self.image_paths = image_paths\n", " self.transform = transform\n", " self.tokenizer = tokenizer\n", " self.max_length = max_length\n", "\n", " def __len__(self):\n", " return len(self.image_paths)\n", "\n", " def __getitem__(self, idx):\n", " path = self.image_paths[idx]\n", " meta = get_metadata_from_png(path)\n", " conditioning = metadata_to_conditioning(meta)\n", " img = Image.open(path).convert(\"RGB\")\n", " pixel_values = self.transform(img)\n", " tokens = self.tokenizer(\n", " conditioning,\n", " truncation=True,\n", " padding=\"max_length\",\n", " max_length=self.max_length,\n", " return_tensors=\"pt\",\n", " )\n", " return {\n", " \"pixel_values\": pixel_values,\n", " \"input_ids\": tokens.input_ids.squeeze(0),\n", " \"attention_mask\": tokens.attention_mask.squeeze(0),\n", " }\n", "\n", "\n", "# Tokenizer CLIP (compatible SD 1.5)\n", "from transformers import CLIPTokenizer\n", "tokenizer = CLIPTokenizer.from_pretrained(\"openai/clip-vit-large-patch14\")\n", "\n", "dataset = PokemonCardDataset(image_paths, transform, tokenizer)\n", "train_size = int(0.9 * len(dataset))\n", "val_size = len(dataset) - train_size\n", "train_dataset, val_dataset = random_split(dataset, [train_size, val_size])\n", "\n", "BATCH_SIZE = 4\n", "train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=0)\n", "val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=0)\n", "\n", "print(f\"Train: {len(train_dataset)}, Val: {len(val_dataset)}\")" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/home/do5-ajlp/lib/python3.12/site-packages/huggingface_hub/utils/_validators.py:206: UserWarning: The `local_dir_use_symlinks` argument is deprecated and ignored in `hf_hub_download`. Downloading to a local directory does not use symlinks anymore.\n", " warnings.warn(\n", "Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "a348b58691b9495dac1a220589791e4c", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Loading weights: 0%| | 0/196 [00:00= EPOCHS:\n", " print(f\"Already trained {start_epoch} epochs (requested {EPOCHS}). Increase EPOCHS to continue.\")\n", "else:\n", " print(f\"Training epochs {start_epoch + 1} to {EPOCHS}...\")\n", "\n", "def encode_prompt(text_encoder, input_ids, attention_mask):\n", " with torch.no_grad():\n", " emb = text_encoder(input_ids, attention_mask=attention_mask)[0]\n", " return emb\n", "\n", "for epoch in range(start_epoch, EPOCHS):\n", " start = time.time()\n", "\n", " # Train\n", " unet.train()\n", " running_loss = 0.0\n", " smooth_loss = None # exponential moving average\n", "\n", " pbar = tqdm(train_loader, desc=f\"[{epoch+1}/{EPOCHS}] Train\", leave=True,\n", " bar_format=\"{l_bar}{bar:30}{r_bar}\", dynamic_ncols=True)\n", "\n", " for step, batch in enumerate(pbar):\n", " pixel_values = batch[\"pixel_values\"].to(device)\n", " input_ids = batch[\"input_ids\"].to(device)\n", " attention_mask = batch[\"attention_mask\"].to(device)\n", "\n", " with torch.no_grad():\n", " latents = vae.encode(pixel_values).latent_dist.sample() * 0.18215\n", "\n", " encoder_hidden_states = encode_prompt(text_encoder, input_ids, attention_mask)\n", " noise = torch.randn_like(latents)\n", " bsz = latents.shape[0]\n", " timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=device).long()\n", " noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps)\n", "\n", " pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample\n", " loss = torch.nn.functional.mse_loss(pred.float(), noise.float(), reduction=\"mean\")\n", "\n", " optimizer.zero_grad()\n", " loss.backward()\n", " torch.nn.utils.clip_grad_norm_(unet.parameters(), 1.0)\n", " optimizer.step()\n", "\n", " running_loss += loss.item() * bsz\n", "\n", " # Exponential moving average for smoother loss display\n", " smooth_loss = loss.item() if smooth_loss is None else 0.98 * smooth_loss + 0.02 * loss.item()\n", "\n", " pbar.set_postfix(ordered_dict={\n", " \"loss\": f\"{loss.item():.4f}\",\n", " \"ema\": f\"{smooth_loss:.4f}\",\n", " \"avg\": f\"{running_loss / ((step + 1) * bsz):.4f}\",\n", " })\n", "\n", " train_loss = running_loss / len(train_dataset)\n", " train_losses.append(train_loss)\n", "\n", " # Validate\n", " unet.eval()\n", " running_loss = 0.0\n", "\n", " with torch.no_grad():\n", " vbar = tqdm(val_loader, desc=f\"[{epoch+1}/{EPOCHS}] Val \", leave=True,\n", " bar_format=\"{l_bar}{bar:30}{r_bar}\", dynamic_ncols=True)\n", "\n", " for step, batch in enumerate(vbar):\n", " pixel_values = batch[\"pixel_values\"].to(device)\n", " input_ids = batch[\"input_ids\"].to(device)\n", " attention_mask = batch[\"attention_mask\"].to(device)\n", "\n", " latents = vae.encode(pixel_values).latent_dist.sample() * 0.18215\n", " encoder_hidden_states = encode_prompt(text_encoder, input_ids, attention_mask)\n", " noise = torch.randn_like(latents)\n", " bsz = latents.shape[0]\n", " timesteps = torch.randint(0, noise_scheduler.config.num_train_timesteps, (bsz,), device=device).long()\n", " noisy_latents = noise_scheduler.add_noise(latents, noise, timesteps)\n", "\n", " pred = unet(noisy_latents, timesteps, encoder_hidden_states).sample\n", " loss = torch.nn.functional.mse_loss(pred.float(), noise.float(), reduction=\"mean\")\n", " running_loss += loss.item() * bsz\n", "\n", " vbar.set_postfix(ordered_dict={\n", " \"loss\": f\"{loss.item():.4f}\",\n", " \"avg\": f\"{running_loss / ((step + 1) * bsz):.4f}\",\n", " })\n", "\n", " val_loss = running_loss / len(val_dataset)\n", " val_losses.append(val_loss)\n", "\n", " elapsed = time.time() - start\n", " mins, secs = divmod(int(elapsed), 60)\n", " print(f\"\\n{'='*60}\")\n", " print(f\" Epoch {epoch+1}/{EPOCHS} complete — {mins}m{secs:02d}s\")\n", " print(f\" Train loss : {train_loss:.4f}\")\n", " print(f\" Val loss : {val_loss:.4f}\")\n", " print(f\"{'='*60}\\n\")\n", "\n", " # Save checkpoint after each epoch\n", " Path(CHECKPOINT_DIR).mkdir(exist_ok=True)\n", " torch.save({\n", " \"epoch\": epoch + 1,\n", " \"unet_state_dict\": unet.state_dict(),\n", " \"optimizer_state_dict\": optimizer.state_dict(),\n", " \"train_losses\": train_losses,\n", " \"val_losses\": val_losses,\n", " }, CHECKPOINT_PATH)\n", " print(f\" Checkpoint saved (epoch {epoch + 1})\")" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "image/png": "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9JIkVeqpSe7u7hWWG1d550N1bFsZBw8eVF5env039x4eHnr33Xd1++2367777tPevXv1z3/+U40bN7bff381999/v3x8fLRq1SpJl+aXuLu76+GHH7bXmTJlil588UUNHz5c77zzjjZv3qwtW7aoUaNGNfoo2eHDh+v111/XpEmT9P7772vz5s32ifA1/Qjby2r6fFbG3XffrcOHD+vNN9/U7bffrjfeeENdu3bVG2+8Ya8zdepUffvtt5o3b568vLz03HPPqV27dtq3b9916ycA12HyNgDUgJSUFP300096//33HR6jeuTIERf26v9p3LixvLy8KnyyUGWeNnT5KVCXf5stSfXq1dOnn36qO++8UwMGDND58+f117/+tVKPWjWbzfrd736nlStXKicnR+vWrVPfvn0VGBhor/Puu+8qJiZGf/vb3+xl58+fv6YX0rVs2VJJSUk6d+6cw6jFoUOHHOqdPXtWSUlJmjt3rn0SuaQKbweqyhvQW7ZsWa4tSfZb5Fq2bFnpfTnD399fdevWvWJf3NzcFBQUZC9r2LChxo0bp3HjxuncuXO6++67NWfOHD366KP2Oq1bt9af/vQn/elPf9J3332nzp07629/+5v+9a9/XZdjAuA6jFgAQA24/BvmX/5GubS0VP/4xz9c1SUH7u7uioqK0oYNG3Ty5El7+ffff6+NGzf+6vYdO3ZUQECAlixZotzcXHt5o0aNtHz5cp0+fVo///yzBg0aVOk+jRo1ShcuXNDEiRN16tSpcu+ucHd3L/cb+sWLF5d7fG9l3Hvvvbp48aISEhLsZWVlZVq8eHG5NqXyIwMLFy4st8969epJUqWCzr333qtdu3YpNTXVXlZUVKR//vOfCg4OVvv27St7KE5xd3dX//799cEHHzg8EjYnJ0erVq3SnXfeKavVKkkOj9eVLs0JCQ0NVUlJiaRL7285f/68Q53WrVurfv369joAbm6MWABADejVq5d8fX0VExOjJ598UiaTSW+//fZ1vXXl18yZM0ebN2/WHXfcocmTJ6usrExLlizR7bffrvT09Ktu6+HhoSVLlmjEiBHq2LGjJk6cqJYtW+rgwYN688031bFjR/3nP//R4MGDtWPHDvvF6dX07t1bzZs31wcffCBvb289+OCDDp/ff//9evvtt+Xj46P27dsrNTVVW7dutT8+tyoGDRqkO+64QzNmzNDRo0fVvn17vf/+++XmTFitVvtcgQsXLqhZs2bavHlzhSNP4eHhkqSZM2fq4YcfVp06dTRo0CB74PilGTNmaPXq1YqOjtaTTz6phg0basWKFTpy5Ijee++9an9L95tvvlnhe0yeeuop/fWvf9WWLVt055136rHHHpOHh4eWLVumkpISvfzyy/a67du3V58+fRQeHq6GDRtqz549evfdd/XEE09Ikr799lvdc889Gj58uNq3by8PDw+tX79eOTk5Dre0Abh5ESwAoAY0atRIH3/8sf70pz/p2Wefla+vrx555BHdc889GjBggKu7J+nShfDGjRs1ffp0PffccwoKCtLzzz+vgwcPVuqpVb/73e+UkpKiF198UYsWLVJJSYnatGmjP//5z3rqqaf073//W/fdd58eeughffLJJ7/60jw3NzeNHDlSCxYs0KBBg1S/fn2HzxctWiR3d3f93//9n86fP6877rhDW7duvabv083NTR9++KGmTp2qf/3rXzKZTHrggQf0t7/9TV26dHGou2rVKk2ZMkVLly6VYRjq37+/Nm7c6PA0LUnq3r27XnjhBb322mvatGmTbDabjhw5UmGwCAgI0BdffKFnnnlGixcv1vnz5xUWFqaPPvpI9913X5WP59f8cmTml8aOHasOHTpo27ZtiouL07x582Sz2RQREaF//etfDhPvn3zySX344YfavHmzSkpK1LJlS/31r3+1vxgwKChII0eOVFJSkt5++215eHjotttu0zvvvKNhw4ZV+zEBuPGYjBvp12cAAJcbMmTIVR8rCgBARZhjAQC3sJ9//tlh/bvvvtOnn36qPn36uKZDAIBaixELALiFNWnSRGPHjlVISIiOHTumhIQElZSUaN++fWrTpo2ruwcAqEWYYwEAt7CBAwdq9erVys7OltlsVmRkpP7nf/6HUAEAqDJGLAAAAAA4jTkWAAAAAJxGsAAAAADgNOZY1CCbzaaTJ0+qfv36MplMru4OAAAAUCWGYaiwsFBNmzb91Zd3Eixq0MmTJxUUFOTqbgAAAABO+fHHH9W8efOr1nF5sFi6dKkWLFig7OxsderUSYsXL1aPHj0qrJuZmalZs2YpLS1Nx44d0yuvvKKpU6c61ElISFBCQoKOHj0qSerQoYNmzZql6Ohoe53Dhw9r+vTp2r59u0pKSjRw4EAtXrxYAQEB9jrffvutnn76ae3YsUOlpaUKCwvTCy+8oN/+9reVPrbLb4398ccfZbVaK70dAAAAcCMoKChQUFCQ/br2alwaLNauXavY2Fi99tprioiI0MKFCzVgwAAdOnRIjRs3Lle/uLhYISEheuihhzRt2rQK99m8eXPNnz9fbdq0kWEYWrFihQYPHqx9+/apQ4cOKioqUv/+/dWpUyclJydLkp577jkNGjRIX375pX2I5/7771ebNm2UnJwsb29vLVy4UPfff78OHz6swMDASh3f5dufrFYrwQIAAAC1VmVu63fp42YjIiLUvXt3LVmyRNKlOQlBQUGaMmWKZsyYcdVtg4ODNXXq1HIjFhVp2LChFixYoPHjx2vz5s2Kjo7W2bNn7Rf7+fn58vX11ebNmxUVFaXTp0/L399fn3/+ue666y5JUmFhoaxWq7Zs2aKoqKhKHV9BQYF8fHyUn59PsAAAAECtU5XrWZc9Faq0tFRpaWkOF+lubm6KiopSampqtbRRVlamNWvWqKioSJGRkZKkkpISmUwmmc1mez0vLy+5ublp+/btkqRGjRqpbdu2WrlypYqKinTx4kUtW7ZMjRs3Vnh4+BXbKykpUUFBgcMCAAAA3ApcFixOnz6tsrIyh3kNkhQQEKDs7Gyn9p2RkSGLxSKz2axJkyZp/fr1at++vSSpZ8+eqlevnp555hkVFxerqKhI06dPV1lZmbKysiRdGurZunWr9u3bp/r168vLy0t///vftWnTJvn6+l6x3Xnz5snHx8e+MHEbAAAAtwqXT96uCW3btlV6erry8/P17rvvKiYmRv/+97/Vvn17+fv7a926dZo8ebJeffVVubm5aeTIkeratat9foVhGHr88cfVuHFjbdu2Td7e3nrjjTc0aNAg7d69W02aNKmw3bi4OMXGxtrXL092AQAAQM2w2WwqLS11dTdqrTp16sjd3b1a9uWyYOHn5yd3d3fl5OQ4lOfk5FR6cvSVeHp6KjQ0VJIUHh6u3bt3a9GiRVq2bJkkqX///jp8+LBOnz4tDw8PNWjQQIGBgQoJCZEkJScn6+OPP3aYh/GPf/xDW7Zs0YoVK644/8NsNjvcYgUAAICaU1paqiNHjshms7m6K7Xa5WthZ9+75rJg4enpqfDwcCUlJWnIkCGSLiXOpKQkPfHEE9Xals1mU0lJSblyPz8/SZeCRG5urh544AFJl54+JancS0Dc3Nz4gwsAAHADMAxDWVlZcnd3V1BQ0K++vA3lGYah4uJi5ebmStIV78qpLJfeChUbG6uYmBh169ZNPXr00MKFC1VUVKRx48ZJksaMGaNmzZpp3rx5ki6l0gMHDth/PnHihNLT02WxWOwjFHFxcYqOjlaLFi1UWFioVatWKSUlRYmJifZ2ly9frnbt2snf31+pqal66qmnNG3aNLVt21aSFBkZKV9fX8XExGjWrFny9vbW66+/riNHjui+++67nl8RAAAAKnDx4kUVFxeradOmqlu3rqu7U2t5e3tLknJzc9W4cWOnbotyabAYMWKETp06pVmzZik7O1udO3fWpk2b7BO6jx8/7pA+T548qS5dutjX4+PjFR8fr969eyslJUXSpS9lzJgxysrKko+Pj8LCwpSYmKh+/frZtzt06JDi4uJ05swZBQcHa+bMmQ7vxfDz89OmTZs0c+ZM9e3bVxcuXFCHDh30wQcfqFOnTjX8rQAAAODXlJWVSbp0FwycczmYXbhwwalg4dL3WNzseI8FAABAzTh//ryOHDmiVq1aycvLy9XdqdWu9l3WivdYAAAAALh5ECwAAACAWiw4OFgLFy50dTcIFgAAAMD1YDKZrrrMmTPnmva7e/duTZgwoXo7ew1uyhfkAQAAADearKws+89r167VrFmzdOjQIXuZxWKx/2wYhsrKyuTh8euX6/7+/tXb0WvEiAUAAABwHQQGBtoXHx8fmUwm+/o333yj+vXra+PGjQoPD5fZbNb27dt1+PBhDR48WAEBAbJYLOrevbu2bt3qsN//vhXKZDLpjTfe0NChQ1W3bl21adNGH374YY0fH8ECAAAAtZ5hGCouveiSpTofsjpjxgzNnz9fBw8eVFhYmM6dO6d7771XSUlJ2rdvnwYOHKhBgwbp+PHjV93P3LlzNXz4cH311Ve69957NWrUKJ05c6ba+lkRboUCAABArffzhTK1n5X46xVrwIHnB6iuZ/VcVj///PMO719r2LChw3vUXnjhBa1fv14ffvihnnjiiSvuZ+zYsRo5cqQk6X/+53/06quvateuXRo4cGC19LMijFgAAAAAN4hu3bo5rJ87d07Tp09Xu3bt1KBBA1ksFh08ePBXRyzCwsLsP9erV09Wq1W5ubk10ufLGLEAAABAreddx10Hnh/gsrarS7169RzWp0+fri1btig+Pl6hoaHy9vbW7373O5WWll51P3Xq1HFYN5lMstls1dbPihAsAAAAUOuZTKZqux3pRrJjxw6NHTtWQ4cOlXRpBOPo0aOu7dQVcCsUAAAAcINq06aN3n//faWnp2v//v36/e9/X+MjD9eKYAEAAADcoP7+97/L19dXvXr10qBBgzRgwAB17drV1d2qkMmozudjwUFBQYF8fHyUn58vq9Xq6u4AAADcNM6fP68jR46oVatW8vLycnV3arWrfZdVuZ5lxAIAAACA0wgWAAAAAJxGsAAAAADgNIIFAAAAAKcRLAAAAAA4jWABAAAAwGkECwAAAABOI1gAAAAAcBrBAgAAAIDTCBYAAABALdGnTx9NnTrV1d2oEMECAAAAuA4GDRqkgQMHVvjZtm3bZDKZ9NVXX13nXlUfggUAAABwHYwfP15btmzRf/7zn3KfLV++XN26dVNYWJgLelY9CBYAAADAdXD//ffL399fb731lkP5uXPntG7dOg0ZMkQjR45Us2bNVLduXXXs2FGrV692TWevAcECAAAAtZ9hSKVFrlkMo1Jd9PDw0JgxY/TWW2/J+MU269atU1lZmR555BGFh4frk08+0ddff60JEyZo9OjR2rVrV019a9XKw9UdWLp0qRYsWKDs7Gx16tRJixcvVo8ePSqsm5mZqVmzZiktLU3Hjh3TK6+8Um7ySkJCghISEnT06FFJUocOHTRr1ixFR0fb6xw+fFjTp0/X9u3bVVJSooEDB2rx4sUKCAhw2Ncnn3yi559/Xl999ZW8vLzUu3dvbdiwoToPHwAAANXhQrH0P01d0/ZfTkqe9SpV9Q9/+IMWLFigf//73+rTp4+kS7dBDRs2TC1bttT06dPtdadMmaLExES98847V7w+vpG4dMRi7dq1io2N1ezZs7V371516tRJAwYMUG5uboX1i4uLFRISovnz5yswMLDCOs2bN9f8+fOVlpamPXv2qG/fvho8eLAyMzMlSUVFRerfv79MJpOSk5O1Y8cOlZaWatCgQbLZbPb9vPfeexo9erTGjRun/fv3a8eOHfr9739f/V8CAAAAbhm33XabevXqpTfffFOS9P3332vbtm0aP368ysrK9MILL6hjx45q2LChLBaLEhMTdfz4cRf3unJMhlHJsZsaEBERoe7du2vJkiWSJJvNpqCgIE2ZMkUz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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import matplotlib.pyplot as plt\n", "\n", "plt.figure(figsize=(8, 4))\n", "plt.plot(train_losses, label=\"Train\")\n", "plt.plot(val_losses, label=\"Val\")\n", "plt.xlabel(\"Epoch\")\n", "plt.ylabel(\"MSE Loss\")\n", "plt.title(\"Training & Validation Loss\")\n", "plt.legend()\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "74dd610894ef48b8853583908a4fa0ce", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Loading pipeline components...: 0%| | 0/6 [00:00 by passing `safety_checker=None`. Ensure that you abide to the conditions of the Stable Diffusion license and do not expose unfiltered results in services or applications open to the public. Both the diffusers team and Hugging Face strongly recommend to keep the safety filter enabled in all public facing circumstances, disabling it only for use-cases that involve analyzing network behavior or auditing its results. For more information, please have a look at https://github.com/huggingface/diffusers/pull/254 .\n", "Token indices sequence length is longer than the specified maximum sequence length for this model (351 > 77). Running this sequence through the model will result in indexing errors\n", "The following part of your input was truncated because CLIP can only handle sequences up to 77 tokens: ['your deck .\", \" energytype \": null , \" evolvefrom \": null , \" hp \": null , \" id \": \" sv 0 8 . 5 - 1 0 5 \", \" illustrator \": \" gidora \", \" image \": \" https :// assets . tcgdex . net / en / sv / sv 0 8 . 5 / 1 0 5 \", \" item \": null , \" legal \": {\" expanded \": true , \" standard \": true }, \" level \": null , \" localid \": \" 1 0 5 \", \" name \": \" crispin \", \" rarity \": \" uncommon \", \" regulationmark \": \" h \", \" resistances \": null , \" retreat \": null , \" set \": {\" cardcount \": {\" official \": 1 3 1 , \" total \": 1 8 0 }, \" id \": \" sv 0 8 . 5 \", \" logo \": \" https :// assets . tcgdex . net / en / sv / sv 0 8 . 5 / logo \", \" name \": \" prismatic evolutions \", \" symbol \": null }, \" stage \": null , \" suffix \": null , \" trainertype \": \" supporter \", \" types \": null , \" variants \": {\" firstedition \": false , \" holo \": true , \" normal \": true , \" reverse \": true , \" wpromo \": false }, \" weaknesses \": null }']\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "dbfa4d8442f44140b9e0b9cd17e2b64a", "version_major": 2, "version_minor": 0 }, "text/plain": [ " 0%| | 0/30 [00:00" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Visualiser des générations sur des prompts de validation\n", "from diffusers import StableDiffusionPipeline\n", "import random\n", "import copy\n", "\n", "unet.eval()\n", "# Merge LoRA weights into a COPY so the original unet stays trainable\n", "unet_copy = copy.deepcopy(unet)\n", "unet_merged = unet_copy.merge_and_unload()\n", "\n", "pipe = StableDiffusionPipeline.from_pretrained(\n", " model_id,\n", " vae=vae,\n", " unet=unet_merged,\n", " text_encoder=text_encoder,\n", " tokenizer=tokenizer,\n", " safety_checker=None,\n", ")\n", "pipe = pipe.to(device)\n", "del unet_copy # free memory\n", "\n", "indices = random.sample(range(len(val_dataset)), 4)\n", "\n", "def get_val_path(i):\n", " dataset_idx = int(val_dataset.indices[i]) # tensor → int\n", " return val_dataset.dataset.image_paths[dataset_idx]\n", "\n", "conditionings = [metadata_to_conditioning(get_metadata_from_png(get_val_path(indices[i]))) for i in range(4)]\n", "\n", "fig, axes = plt.subplots(2, 4, figsize=(16, 8))\n", "for i, cond in enumerate(conditionings):\n", " out = pipe(cond, num_inference_steps=30, guidance_scale=7.5).images[0]\n", " axes[0, i].imshow(out)\n", " axes[0, i].axis(\"off\")\n", " axes[0, i].set_title(cond[:50] + \"...\", fontsize=8)\n", "\n", " real = Image.open(get_val_path(indices[i])).convert(\"RGB\").resize((512, 512))\n", " axes[1, i].imshow(real)\n", " axes[1, i].axis(\"off\")\n", " axes[1, i].set_title(\"Ground truth\", fontsize=8)\n", "\n", "plt.suptitle(\"Generated vs Ground Truth\")\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def generate_card_from_metadata(meta, num_inference_steps=30, guidance_scale=7.5, save_path=None):\n", " \"\"\"Génère une carte à partir d'un dict au format pokemon_metadata (sortie de ta pipeline prompt → JSON).\"\"\"\n", " conditioning = metadata_to_conditioning(meta)\n", " image = pipe(conditioning, num_inference_steps=num_inference_steps, guidance_scale=guidance_scale).images[0]\n", " if save_path:\n", " image.save(save_path)\n", " return image\n", "\n", "# Exemple : meta = sortie de ta pipeline (prompt utilisateur → JSON)\n", "example_meta = {\n", " \"name\": \"Charizard\", \"types\": [\"Fire\"], \"hp\": 120, \"stage\": \"Stage2\", \"rarity\": \"Rare\",\n", " \"attacks\": [{\"name\": \"Fire Spin\", \"damage\": \"100\"}, {\"name\": \"Flamethrower\", \"damage\": \"50\"}],\n", "}\n", "out = generate_card_from_metadata(example_meta)\n", "out" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "The following part of your input was truncated because CLIP can only handle sequences up to 77 tokens: ['], \" category \": \" pokemon \", \" description \": \" it makes a nest to suit its long and skinny body . the nest is impossible for other pokémon to enter .\", \" evolvefrom \": \" sentret \", \" hp \": 1 1 0 , \" id \": \" swsh 3 - 1 3 6 \", \" illustrator \": \" tetsuya koizumi \", \" image \": \" https :// assets . tcgdex . net / en / swsh / swsh 3 / 1 3 6 \", \" localid \": \" 1 3 6 \", \" name \": \" furret \", \" rarity \": \" uncommon \", \" regulationmark \": \" d \", \" retreat \": 1 , \" set \": {\" cardcount \": {\" official \": 1 8 9 , \" total \": 2 0 1 }, \" id \": \" swsh 3 \", \" logo \": \" https :// assets . tcgdex . net / en / swsh / swsh 3 / logo \", \" name \": \" darkness ablaze \", \" symbol \": \" https :// assets . tcgdex . net / univ / swsh / swsh 3 / symbol \"}, \" stage \": \" stage 1 \", \" types \": [\" colorless \"], \" weaknesses \": [{\" type \": \" fighting \", \" value \": \"× 2 \"}]}']\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "b90f4851a87f4feb83ee481149dd3347", "version_major": 2, "version_minor": 0 }, "text/plain": [ " 0%| | 0/75 [00:00" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def generate_card_from_metadata(meta, num_inference_steps=30, guidance_scale=7.5, save_path=None):\n", " \"\"\"Génère une carte à partir d'un dict au format pokemon_metadata (sortie de ta pipeline prompt → JSON).\"\"\"\n", " conditioning = metadata_to_conditioning(meta)\n", " image = pipe(conditioning, num_inference_steps=num_inference_steps, guidance_scale=guidance_scale).images[0]\n", " if save_path:\n", " image.save(save_path)\n", " return image\n", "\n", "# Exemple : meta = sortie de ta pipeline (prompt utilisateur → JSON)\n", "example_meta = {\n", " \"category\": \"Pokemon\",\n", " \"id\": \"swsh3-136\",\n", " \"illustrator\": \"tetsuya koizumi\",\n", " \"image\": \"https://assets.tcgdex.net/en/swsh/swsh3/136\",\n", " \"localId\": \"136\",\n", " \"name\": \"Furret\",\n", " \"rarity\": \"Uncommon\",\n", " \"set\": {\n", " \"cardCount\": {\n", " \"official\": 189,\n", " \"total\": 201\n", " },\n", " \"id\": \"swsh3\",\n", " \"logo\": \"https://assets.tcgdex.net/en/swsh/swsh3/logo\",\n", " \"name\": \"Darkness Ablaze\",\n", " \"symbol\": \"https://assets.tcgdex.net/univ/swsh/swsh3/symbol\"\n", " },\n", " \"hp\": 110,\n", " \"types\": [\n", " \"Colorless\"\n", " ],\n", " \"evolveFrom\": \"Sentret\",\n", " \"description\": \"It makes a nest to suit its long and skinny body. The nest is impossible for other Pokémon to enter.\",\n", " \"stage\": \"Stage1\",\n", " \"attacks\": [\n", " {\n", " \"cost\": [\n", " \"Colorless\"\n", " ],\n", " \"name\": \"Feelin' Fine\",\n", " \"effect\": \"Draw 3 cards.\"\n", " },\n", " {\n", " \"cost\": [\n", " \"Colorless\"\n", " ],\n", " \"name\": \"Tail Smash\",\n", " \"effect\": \"Flip a coin. If tails, this attack does nothing.\",\n", " \"damage\": 90\n", " }\n", " ],\n", " \"weaknesses\": [\n", " {\n", " \"type\": \"Fighting\",\n", " \"value\": \"×2\"\n", " }\n", " ],\n", " \"retreat\": 1,\n", " \"regulationMark\": \"D\"\n", "}\n", "out = generate_card_from_metadata(example_meta)\n", "out" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "LoRA saved to /home/do5-ajlp/juicepyter/pokemon_card_lora (1 epochs total)\n" ] } ], "source": [ "# Sauvegarder le LoRA et l'historique d'entraînement\n", "LORA_PATH = \"/home/do5-ajlp/juicepyter/pokemon_card_lora\"\n", "unet.save_pretrained(LORA_PATH)\n", "torch.save({\n", " \"train_losses\": train_losses,\n", " \"val_losses\": val_losses,\n", " \"epochs\": len(train_losses),\n", "}, LORA_PATH + \"/training_history.pt\")\n", "print(f\"LoRA saved to {LORA_PATH} ({len(train_losses)} epochs total)\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 4 }