{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Convert a Trove list into a CollectionBuilder exhibition\n", "\n", "This notebook converts [Trove lists](https://trove.nla.gov.au/help/become-voluntrove/lists) into a series of files that can be uploaded to a [CollectionBuilder-GH](https://github.com/CollectionBuilder/collectionbuilder-gh) repository to create an instant exhibition. See the [CollectionBuilder site](https://collectionbuilder.github.io/) for more information on how CollectionBuilder works and what it can do.\n", "\n", "**Demo**: [this exhibition](https://wragge.github.io/trove-wragge-list-demo/) was generated from this [Trove list](https://trove.nla.gov.au/list/83777).\n", "\n", "#### 1. What you need\n", "\n", "* a [Trove API key](https://trove.nla.gov.au/about/create-something/using-api#getting-an-api-key) (copy & paste your key where indicated below)\n", "* a [GitHub account](https://github.com/signup)\n", "* a [Trove List](https://trove.nla.gov.au/help/become-voluntrove/lists) containing items you want to include in your exhibition\n", "\n", "#### 2. Setup a GitHub repository for your exhibition\n", "\n", "1. Login to your GitHub account.\n", "2. Go to my [customised CollectionBuilder-GH template repository](https://github.com/wragge/collectionbuilder-gh-trove).\n", "3. Click on the big green **Use this template** button.\n", "4. Give your repository a name by typing in the **Repository name** box – the name of the repository will form part of the url for your new exhibition, so you probably want to give it a name that relates to the exhibition.\n", "5. Click on the big green **Create a repository from template** button. You'll be automatically redirected to your new repository.\n", "\n", "#### 3. Enable GitHub Pages for your repository\n", "\n", "GitHub builds your exhibition from the files in the repository using GitHub Pages. You need to enable this after you create your repository:\n", "\n", "1. Click on the **Settings** button in your new repository.\n", "2. Click on the **Pages** button in the side menubar.\n", "3. Under **Branch** select 'main' from the dropdown list and click on **Save**.\n", "\n", "GitHub will now build your exhibition. Once it's ready you'll see a link on the 'Pages' page. The url will have the form `https://[your GH user name].github.io/[your repository name]`. At the moment the exhibition will contain dummy data – the next step is to generate your own exhibition data! \n", "\n", "#### 4. Generate your exhibition files from your Trove list\n", "\n", "1. Find your Trove list's numeric id. The list id is the number in the url of your Trove list. So [the list](https://trove.nla.gov.au/list/83774) with this url `https://trove.nla.gov.au/list/83774` has an id of `83774`.\n", "2. Copy and paste your *list id* and *Trove API key* where indicated below in this notebook,\n", "3. From the Jupyter **Run** menu select **Run all cells**.\n", "4. When everything has finished running, a link to a zip file will be displayed at the bottom of the notebook. Download it to your own computer and open the zip file. Done!\n", "\n", "#### 5. Add more metadata (optional)\n", "\n", "The metadata describing the items in your exhibition is contained in the `_data/[list id]-items.csv` file. If the items in your exhibition relate to specific places, you may want to add some extra metadata so that CollectionBuilder can display them on a map.\n", "\n", "Information about places is contained in three columns: `location`, `latitude`, and `longitude`. In the `location` field you can include a list of place names, separated by semicolons, eg: 'Melbourne; Sydney; Hobart'. These placenames will be used to build a word cloud when you click on the **Location** tab in your exhibition. \n", "\n", "To add an item to CollectionBuilder's map view, you need to supply values for `latitude` and `longitude`.\n", "\n", "You might also want to edit the `subject`, and `description` fields.\n", "\n", "1. Open your metadata file with either a text editor or a spreadsheet program (but beware that some programs, like Excel, might mangle your dates).\n", "2. Edit the desired values.\n", "3. Make sure the edited file is saved in CSV (plain text) format, replacing the original metadata file.\n", "\n", "Note that GitHub has it's own built-in file editor. So if you don't have a way of editing the CSV file on your own computer, just skip down to the 'Upload your files...' section below and add them to your GitHub repository. To edit the file just view it in GitHub and click on the pencil icon. Once you've finished editing, make sure you click the **Commit** button to save your changes.\n", "\n", "#### 6. Replace tiny images (optional)\n", "\n", "Trove work records often only include links to tiny thumbnailed versions of images. These don't look great in an exhibition, so you might want to replace them. Different collections use different image viewers, so there's no easy, automated way to do this. You'll have to manually download them and replace the thumnailed versions.\n", "\n", "1. From the Trove work record, click on the **View** button and open the link to the original item.\n", "2. Use whatever download mechanism is provided to save a copy of the image on your computer.\n", "3. Rename the downloaded image to match the name of the tiny thumbnailed version in your exhibition's `objects` directory.\n", "4. Replace the thumbnail image in the `objects` directory with the new downloaded version.\n", "\n", "#### 7. Upload your files to the exhibition repository\n", "\n", "You're now ready to add your exhibition files to the exhibition repository!\n", "\n", "1. Go to the GitHub repository you created above.\n", "2. Click on the **Add file** button and select **Upload files**.\n", "3. Select the `_config.yml` file in the exhibition files you downloaded from this notebook.\n", "4. Click on the green **Commit changes** button to save the file in your repository.\n", "5. Open the `_data` directory in your GitHub repository.\n", "6. Click on the **Add file** button and select **Upload files**.\n", "7. Select the `_data/[list id]-items.csv` file in your exhibition files.\n", "8. Click on the green **Commit changes** button to save the file in your repository.\n", "9. Open the `objects` directory in your GitHub repository.\n", "10. Select all the files in the `objects` directory of your exhibition files.\n", "11. Click on the green **Commit changes** button to save the files in your repository.\n", "\n", "Once you've uploaded the files, GitHub will rebuild the exhibition using your data. It might take a little while to generate, but once it's ready you see it at `https://[your GH user name].github.io/[your repository name]`.\n", "\n", "If your not happy with the metadata and how it displays, you can either edit the exhibition files on your own computer and re-upload them to GitHub. Or you can use GitHub's built-in file editor to make changes. To edit a file just view it in GitHub and click on the pencil icon. Once you've finished editing, make sure you click the **Commit** button to save your changes.\n", "\n", "Every time you make a change to your repository, GitHub will automatically rebuild your exhibition.\n", "\n", "#### 8. Further customisation\n", "\n", "You can further customise the look and feel of your exhibition by editing the `_data/theme.yml` file. For example, you can:\n", "\n", "* Set a different `featured-image` to display in the header of your exhibition.\n", "* Change the `latitude` and `longitude` values to set the centre on the map view.\n", "\n", "See the [CollectionBuilder documentation](https://collectionbuilder.github.io/cb-docs/docs/theme/) for more options.\n", "\n", "\n", "#### Annotating Trove list items\n", "\n", "You can add your own annotations to Trove list items and these will automatically be included in your exhibition. To add a descriptive note:\n", "\n", "1. Make sure you're logged in to your Trove account.\n", "2. Go to your list (you can find a list of your lists in your Trove user profile).\n", "3. Go to the item in your list you want to annotate and click on the **Add list item note button**.\n", "4. Add your note.\n", "\n", "Your note will be added to the `description` field of the item when you generate your exhibition files. In addition, any tags added to items in your list will be added to the `subject` field.\n", "\n", "Note that if you make changes to your list, you'll need to regenerate the exhibition files using this notebook and upload them to your GitHub repository before the changes are visible in your exhibition." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import shutil\n", "from pathlib import Path\n", "\n", "import pandas as pd\n", "import requests\n", "import yaml\n", "from IPython.display import HTML\n", "from PIL import Image\n", "from PIL.ImageOps import fit\n", "from requests.adapters import HTTPAdapter\n", "from requests.packages.urllib3.util.retry import Retry\n", "from tqdm.auto import tqdm\n", "from trove_newspaper_images.articles import download_images\n", "\n", "s = requests.Session()\n", "retries = Retry(total=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504])\n", "s.mount(\"http://\", HTTPAdapter(max_retries=retries))\n", "s.mount(\"https://\", HTTPAdapter(max_retries=retries))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%capture\n", "# Load variables from the .env file if it exists\n", "# Use %%capture to suppress messages\n", "%load_ext dotenv\n", "%dotenv" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Add your API key and list ID values\n", "\n", "This is the only section that you'll need to edit. Paste your API key and list id in the cells below as indicated. Once you've finished, select **Run all cells** from the **Run** menu to generate your exhibition files." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Insert your Trove API key between the quotes\n", "API_KEY = \"YOUR API KEY\"\n", "\n", "# Use api key value from environment variables if it is available\n", "if os.getenv(\"TROVE_API_KEY\"):\n", " API_KEY = os.getenv(\"TROVE_API_KEY\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Paste your list id between the quotes\n", "list_id = \"83777\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Define some functions" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def listify(value):\n", " \"\"\"\n", " Sometimes values can be lists and sometimes not.\n", " Turn them all into lists to make life easier.\n", " \"\"\"\n", " if isinstance(value, (str, int)):\n", " try:\n", " value = str(value)\n", " except ValueError:\n", " pass\n", " value = [value]\n", " return value\n", "\n", "\n", "def get_url(identifiers, linktype):\n", " \"\"\"\n", " Loop through the identifiers to find the request url.\n", " \"\"\"\n", " url = \"\"\n", " for identifier in identifiers:\n", " if identifier[\"linktype\"] == linktype:\n", " url = identifier[\"value\"]\n", " break\n", " return url\n", "\n", "\n", "def save_as_csv(list_dir, data, data_type):\n", " df = pd.DataFrame(data)\n", " df[\"pages\"] = df[\"pages\"].astype(\"Int64\")\n", " df.to_csv(Path(list_dir, \"_data\", f\"{list_id}-{data_type}.csv\"), index=False)\n", "\n", "\n", "def make_filename(article):\n", " \"\"\"\n", " Create a filename for a text file or PDF.\n", " For easy sorting/aggregation the filename has the format:\n", " PUBLICATIONDATE-NEWSPAPERID-ARTICLEID\n", " \"\"\"\n", " date = article[\"date\"]\n", " date = date.replace(\"-\", \"\")\n", " newspaper_id = article[\"newspaper_id\"]\n", " article_id = article[\"id\"]\n", " return \"{}-{}-{}\".format(date, newspaper_id, article_id)\n", "\n", "\n", "def get_list(list_id):\n", " list_url = f\"https://api.trove.nla.gov.au/v2/list/{list_id}?encoding=json&reclevel=full&include=listItems&key={API_KEY}\"\n", " response = s.get(list_url)\n", " return response.json()\n", "\n", "\n", "def get_article(id):\n", " article_api_url = f\"https://api.trove.nla.gov.au/v2/newspaper/{id}/?encoding=json&reclevel=full&key={API_KEY}&include=tags\"\n", " response = s.get(article_api_url)\n", " return response.json()\n", "\n", "\n", "def get_work(id):\n", " article_api_url = f\"https://api.trove.nla.gov.au/v2/work/{id}/?encoding=json&reclevel=full&key={API_KEY}&include=tags,links\"\n", " response = s.get(article_api_url)\n", " return response.json()\n", "\n", "\n", "def make_dirs(list_id):\n", " list_dir = Path(\"cb-exhibitions\", list_id)\n", " list_dir.mkdir(parents=True, exist_ok=True)\n", " Path(list_dir, \"objects\").mkdir(exist_ok=True)\n", " Path(list_dir, \"temp\").mkdir(exist_ok=True)\n", " Path(list_dir, \"_data\").mkdir(exist_ok=True)\n", " return list_dir\n", "\n", "\n", "def get_subjects(work):\n", " subjects = []\n", " if \"subject\" in work:\n", " subjects = listify(work[\"subject\"])\n", " else:\n", " subjects = []\n", " if \"tag\" in work:\n", " for tag in work[\"tag\"]:\n", " subjects.append(tag[\"value\"])\n", " return subjects\n", "\n", "\n", "def get_work_image_url(record):\n", " image_url = get_url(record.get(\"identifier\", \"\"), \"viewcopy\")\n", " if not image_url:\n", " image_url = get_url(record.get(\"identifier\", \"\"), \"thumbnail\")\n", " return image_url\n", "\n", "\n", "def save_work_image(list_dir, record):\n", " image_url = get_work_image_url(record)\n", " if image_url:\n", " response = s.get(image_url)\n", " if response.status_code == 200:\n", " filename = Path(list_dir, \"objects\", f\"work-{record.get('id', '')}.jpg\")\n", " filename.write_bytes(response.content)\n", " return filename\n", "\n", "\n", "def get_article_tags(record):\n", " subjects = []\n", " article = get_article(record[\"id\"])[\"article\"]\n", " if \"tag\" in article:\n", " for tag in article[\"tag\"]:\n", " subjects.append(tag[\"value\"])\n", " return subjects\n", "\n", "\n", "def get_parent(record):\n", " parent = \"\"\n", " parents = listify(record.get(\"isPartOf\", []))\n", " if parents:\n", " if isinstance(parents[0], dict) and \"value\" in parents[0]:\n", " parent = parents[0][\"value\"]\n", " else:\n", " parent = parents[0]\n", " return parent\n", "\n", "\n", "def update_config(list_data, list_dir):\n", " with Path(\"cb-config\", \"_config.yml\").open(\"r\") as config_in:\n", " config = yaml.safe_load(config_in)\n", " config[\"title\"] = list_data[\"list\"][0][\"title\"]\n", " config[\"author\"] = list_data[\"list\"][0][\"creator\"].replace(\"public:\", \"\")\n", " config[\"metadata\"] = f'{list_data[\"list\"][0][\"id\"]}-items'\n", " with Path(list_dir, \"_config.yml\").open(\"w\") as config_out:\n", " config_out.write(yaml.dump(config))\n", "\n", "\n", "def harvest_list(list_id):\n", " list_dir = make_dirs(list_id)\n", " data = get_list(list_id)\n", " update_config(data, list_dir)\n", " items = []\n", " for item in tqdm(data[\"list\"][0][\"listItem\"]):\n", " for zone, record in item.items():\n", " if zone == \"work\":\n", " # Some fields aren't included in the list data, so get the full work record\n", " work_data = get_work(record[\"id\"])[\"work\"]\n", " work = {\n", " \"objectid\": f\"work-{record.get('id', '')}\",\n", " \"title\": record.get(\"title\", \"\"),\n", " \"type\": \";\".join(listify(record.get(\"type\", \"\"))),\n", " \"date\": listify(record.get(\"issued\", []))[0],\n", " \"creator\": \"; \".join(listify(record.get(\"contributor\", \"\"))),\n", " \"is_part_of\": get_parent(record),\n", " \"trove_url\": record.get(\"troveUrl\", \"\"),\n", " \"source_url\": get_url(record.get(\"identifier\", \"\"), \"fulltext\"),\n", " \"description\": item.get(\"note\", \"\"),\n", " \"subject\": \"; \".join(get_subjects(work_data)),\n", " \"location\": \"\",\n", " \"latitude\": \"\",\n", " \"longitude\": \"\",\n", " }\n", " image_filename = save_work_image(list_dir, work_data)\n", " if image_filename:\n", " work[\"filename\"] = image_filename.name\n", " work[\"format\"] = \"image/jpeg\"\n", " items.append(work)\n", " elif zone == \"article\":\n", " newspaper_id = record.get(\"title\", {}).get(\"id\")\n", " newspaper_title = record.get(\"title\", {}).get(\"value\")\n", " newspaper_link = f'{newspaper_title}'\n", " # citation =\n", " article = {\n", " \"objectid\": f\"article-{record.get('id', '')}\",\n", " \"title\": record.get(\"heading\", \"\"),\n", " \"date\": record.get(\"date\", \"\"),\n", " \"is_part_of\": newspaper_link,\n", " \"pages\": record.get(\"pageSequence\", \"\"),\n", " \"trove_url\": f'http://nla.gov.au/nla.news-article{record.get(\"id\")}',\n", " \"type\": \"Newspaper article\",\n", " \"format\": \"image/jpeg\",\n", " \"description\": item.get(\"note\", \"\"),\n", " \"subject\": \"; \".join(get_article_tags(record)),\n", " \"location\": \"\",\n", " \"latitude\": \"\",\n", " \"longitude\": \"\",\n", " }\n", " images = download_images(record[\"id\"], Path(list_dir, \"temp\"))\n", " img = Image.open(Path(list_dir, \"temp\", images[0]))\n", " cropped = fit(\n", " img, (800, 800), method=Image.Resampling.LANCZOS, centering=(0.5, 0)\n", " )\n", " cropped.save(Path(list_dir, \"objects\", images[0]), \"JPEG\")\n", " article[\"filename\"] = images[0]\n", " items.append(article)\n", " shutil.rmtree(Path(list_dir, \"temp\"))\n", " if items:\n", " save_as_csv(list_dir, items, \"items\")\n", " return items" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Let's do it!\n", "\n", "Run the cell below to start the exhibition building process." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "items = harvest_list(list_id)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Download the results\n", "\n", "Run the cell below to zip up all the harvested files and create a download link." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "list_dir = Path(\"cb-exhibitions\", list_id)\n", "shutil.make_archive(list_dir, \"zip\", list_dir)\n", "HTML(f'Download your files')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "----\n", "\n", "Created by [Tim Sherratt](https://timsherratt.org/) for the [GLAM Workbench](https://glam-workbench.github.io/)." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.12" }, "vscode": { "interpreter": { "hash": "f54aba2de7a75230217f549a064c6555500d2132634fbcab9606dbfda34a2a1b" } }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": { "185b7c2bba684a4eb09716422b9f633b": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HBoxModel", "state": { "children": [ "IPY_MODEL_8d365d4586e34cfba26cae54db746f21", "IPY_MODEL_26cbab71e3ce45c599ee7620b3814982", "IPY_MODEL_9e462bf9c2f64dc1a2fe8154a5d84024" ], "layout": "IPY_MODEL_bdb06018fdeb492a97793ea62c552283" } }, "1f0f166b98bd478789c0b7a9cd272bba": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLStyleModel", "state": { "description_width": "", "font_size": null, "text_color": null } }, "20312a1542694ef1b2699870eab7b52c": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": {} }, "26cbab71e3ce45c599ee7620b3814982": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "FloatProgressModel", "state": { "bar_style": "success", "layout": "IPY_MODEL_38b80a3469314317bb0ae5dd4e1ea2c2", "max": 16, "style": "IPY_MODEL_b12c0776a17e48f0ab34d74dcce1b35f", "value": 16 } }, "2f1f76884f4f4cf0b0bacd3daec2bca4": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "FloatProgressModel", "state": { "bar_style": "success", "layout": "IPY_MODEL_678ccfb9714b499994b1f954127af20c", "max": 16, "style": "IPY_MODEL_79491c1e830d4cddb4ea9568fbf787e0", "value": 16 } }, "38b80a3469314317bb0ae5dd4e1ea2c2": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": {} }, "42858e8f3962429c95555e5281a9c175": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLStyleModel", "state": { "description_width": "", "font_size": null, "text_color": null } }, "5c82299b5d8d472ab0d335819b3ea5e3": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLModel", "state": { "layout": "IPY_MODEL_20312a1542694ef1b2699870eab7b52c", "style": "IPY_MODEL_42858e8f3962429c95555e5281a9c175", "value": " 16/16 [00:26<00:00, 1.49s/it]" } }, "678ccfb9714b499994b1f954127af20c": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": {} }, "79491c1e830d4cddb4ea9568fbf787e0": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "ProgressStyleModel", "state": { "description_width": "" } }, "8d365d4586e34cfba26cae54db746f21": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLModel", "state": { "layout": "IPY_MODEL_af9dfb4033ea44f7a8a4ee1c26b9ceb8", "style": "IPY_MODEL_1f0f166b98bd478789c0b7a9cd272bba", "value": "100%" } }, "9e462bf9c2f64dc1a2fe8154a5d84024": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLModel", "state": { "layout": "IPY_MODEL_c03340940711419db1c3d7ddcd6d81f3", "style": "IPY_MODEL_da810c16a904443386cdb9f01f13b4d9", "value": " 16/16 [00:36<00:00, 1.23s/it]" } }, "9eb58fc1f05349c8a5098fb5e18bfcf6": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLModel", "state": { "layout": "IPY_MODEL_dff055bb32d14370b64dac6fac2dad46", "style": "IPY_MODEL_fd9b3122437743a2ba228aca329e00b1", "value": "100%" } }, "af9dfb4033ea44f7a8a4ee1c26b9ceb8": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": {} }, "b12c0776a17e48f0ab34d74dcce1b35f": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "ProgressStyleModel", "state": { "description_width": "" } }, "b7c1b4a9baad41b2a60944fbe334682c": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HBoxModel", "state": { "children": [ "IPY_MODEL_9eb58fc1f05349c8a5098fb5e18bfcf6", "IPY_MODEL_2f1f76884f4f4cf0b0bacd3daec2bca4", "IPY_MODEL_5c82299b5d8d472ab0d335819b3ea5e3" ], "layout": "IPY_MODEL_fd9c10f5f80049e29f9965e76cb14150" } }, "bdb06018fdeb492a97793ea62c552283": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": {} }, "c03340940711419db1c3d7ddcd6d81f3": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": {} }, "da810c16a904443386cdb9f01f13b4d9": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLStyleModel", "state": { "description_width": "", "font_size": null, "text_color": null } }, "dff055bb32d14370b64dac6fac2dad46": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": {} }, "fd9b3122437743a2ba228aca329e00b1": { "model_module": "@jupyter-widgets/controls", "model_module_version": "2.0.0", "model_name": "HTMLStyleModel", "state": { "description_width": "", "font_size": null, "text_color": null } }, "fd9c10f5f80049e29f9965e76cb14150": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", "state": {} } }, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 4 }