Laura Wagner commited on
Commit
6d08c5c
·
2 Parent(s): 6f079a5b87a88b

Merge remote-tracking branch 'origin/main' into main

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +3 -0
  2. README.md +28 -19
  3. gitlab-ci.yml +11 -0
  4. {scripts → jupyter_notebooks}/0_Scraping_image_metadata.ipynb +42 -8
  5. jupyter_notebooks/0_Scraping_model_metadata.ipynb +643 -0
  6. scripts/Section_2_Image_grid.ipynb → jupyter_notebooks/Section_1_Figure_1_image_grid.ipynb +2 -2
  7. scripts/Section_6-1_Histograms.ipynb → jupyter_notebooks/Section_2-2-2_Figure_3_histogram_monthly_images_nsfw_levels.ipynb +2 -2
  8. scripts/Section_6-2_Demographics.ipynb → jupyter_notebooks/Section_2-2-2_Figure_4_Demographic_patterns_in_gen_images.ipynb +16 -0
  9. scripts/Section_6-3_Models.ipynb → jupyter_notebooks/Section_2-3-2_top_10_most_popular_checkpoints.ipynb +0 -0
  10. scripts/Section_6-4_Top_models.ipynb → jupyter_notebooks/Section_2-3-3_Figure_7_top_30_adapters.ipynb +0 -0
  11. jupyter_notebooks/Section_2-3-4__Figure_8_Deepfake_victims.ipynb +668 -0
  12. jupyter_notebooks/Section_2-3-4__Figure_8a_sunburst_gender.ipynb +127 -0
  13. jupyter_notebooks/Section_2-3-4__Figure_8b_sunburst_profession.ipynb +123 -0
  14. jupyter_notebooks/Section_2-3_Figure_5_co-occurence_promotional_tags.ipynb +314 -0
  15. jupyter_notebooks/Section_2-4_Figure_9_Training_tags_Sankey.ipynb +203 -0
  16. jupyter_notebooks/Section_2-4_Figure_9_ectract_LoRA_metadata_v2.ipynb +400 -0
  17. jupyter_notebooks/Section_2-4_Figure_9_extract_LoRA_metadata.ipynb +557 -0
  18. {scripts → jupyter_notebooks}/Section_3-3-4_deepfakes.ipynb +0 -0
  19. jupyter_notebooks/SuppM_Figure_13_Danbooru_categories.ipynb +141 -0
  20. jupyter_notebooks/SuppM_Figure_S12_asset_types.ipynb +129 -0
  21. jupyter_notebooks/SuppM_Figure_S13_Danbooru_Taxonomy.ipynb +1848 -0
  22. jupyter_notebooks/SuppM_Figure_S14_co-occurence_training_data.ipynb +152 -0
  23. misc/api_keys_example.txt +0 -2
  24. misc/danbooru/tag_info_test/wombat_tag_info.json +27 -0
  25. misc/deepseek_query_index.txt +1 -0
  26. misc/deletemememe.txt +0 -0
  27. misc/lists/countries.csv +250 -0
  28. misc/lists/danbooru.json +0 -0
  29. misc/lists/mapped_professions.csv +1074 -0
  30. misc/lists/professions.csv +36 -0
  31. plots/2024-06_image_contributions.svg +0 -0
  32. plots/Figure_12.svg +1936 -0
  33. plots/Sec-6-5_lora_tags_categories.svg +0 -2350
  34. plots/Sec_6-5_taggers.svg +0 -777
  35. plots/Section_6-7_model_contributions_sankey.svg +0 -1
  36. plots/models_nsfw_monthly_histogram_with_quarters.svg +0 -2108
  37. public/Figure_13.html +166 -0
  38. public/Figure_8a_sunburst.html +223 -0
  39. public/Figure_8b_sunburst.html +254 -0
  40. public/Figure_9.html +226 -0
  41. public/Figure_9_old.html +213 -0
  42. public/csv/sankey_data.csv +22 -0
  43. public/figure_15.html +325 -0
  44. public/figure_5.html +442 -0
  45. public/img/booru_tags.svg +0 -0
  46. public/img/japan.svg +1776 -0
  47. plots/grid10x120.png → public/img/sankey.png +2 -2
  48. public/img/tag_clusters.png +3 -0
  49. public/img/training_tags.png +3 -0
  50. public/index.html +62 -0
.gitignore CHANGED
@@ -3,5 +3,8 @@ ext
3
  ARCHIVE
4
  misc/api_keys.txt
5
  misc/training_tags_categories
 
 
6
  scripts/ARCHIVE
7
  scripts/CEMETARY
 
 
3
  ARCHIVE
4
  misc/api_keys.txt
5
  misc/training_tags_categories
6
+ misc/credentials/*
7
+ misc/credentials
8
  scripts/ARCHIVE
9
  scripts/CEMETARY
10
+ cemetary
README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Code for the Paper titled "Perpetual Misogyny: How Gendered Tropes Shape Text-To-Image AI Personalization"
2
 
3
 
4
 
@@ -7,28 +7,37 @@
7
 
8
  ```
9
  CIVITAI_VISUALIZATIONS/
10
- ├── .virtual_documents/
11
- ├── data/ # Final datasets
12
- │ ├── subset1/
13
  │ ├── subset2/
14
  │ └── ...
15
- ├── misc/ # Miscellaneous scripts and utilities
16
- ├── plots/ # Generated plots and visualizations
17
- ├── scripts/ # Jupyter notebooks for analysis
18
- │ ├── 0_Scraping_image_metadata.ipynb # Scraping scripts interacting with the CivitAI API, dataset consolidation
19
- │ ├── Section_2_image_grid.ipynb # Code to generate an image grid as shown in Section 2
20
- │ ├── Section_6-1_Histograms.ipynb # Histograms showing image and model contributions over the span of two years
21
- │ ├── Section_6-1_Models_ratios.ipynb # Models' NSFW ratio estimation
22
- │ ├── Section_6-2_Demographics.ipynb # Visualization of estimations of the demographic distribution of subjects depicted in CivitAI imagery
23
- │ ├── Section_6-3_Models.ipynb # Models download for systematic inference + ranking and promotional tags visualizations
24
- │ ├── Section_6-4_Top_Models.ipynb # Ranking plot of popular adapters by download count
25
- │ └── Section_6-5_LoRA_metadata.ipynb # LoRA download and textual training tags extraction, categorizing and ranking
26
- │ ├── Section_6-6_Users_Images.ipynb # Cumulative plot of users' image contributions
27
- │ ├── Section_6-7_Users_Models.ipynb # Sankey diagram of users' model contributions
 
 
 
 
 
 
 
 
 
28
  ├── .gitignore
29
  ├── .gitmodules
30
- ├── README.md
31
- └── requirements.txt
32
  ```
33
 
34
  # Project Setup
 
1
+ # Code for the Paper titled ["Perpetual Misogyny: How Gendered Tropes Shape Text-To-Image AI Personalization"](http://arxiv.org/pdf/2505.04600)
2
 
3
 
4
 
 
7
 
8
  ```
9
  CIVITAI_VISUALIZATIONS/
10
+ ├── .virtual_documents/ # Temporary files from Jupyter
11
+ ├── data/ # Final curated datasets
12
+ │ ├── subset1/ # Specific data splits or versions
13
  │ ├── subset2/
14
  │ └── ...
15
+ ├── misc/
16
+ │ └── credentials/ # API keys and sensitive config (excluded from versioning)
17
+ ├── plots/ # Output plots and figures used in the paper
18
+ ├── ├── jupyter_notebooks/ # Main analysis notebooks
19
+ │ ├── 0_Scraping_image_metadata.ipynb # Scrapes CivitAI metadata via API
20
+ │ ├── Section_1_Figure_1_image_grid.ipynb # Grid of sample images
21
+ │ ├── Section_3-2-1_Figure_3_histogram.ipynb # Histogram of upload trends
22
+ │ ├── Section_3-2-1_Figure_4_Mivolo.ipynb # Model activity plot (Mivolo-focused)
23
+ │ ├── Section_3-3-1_Figure_5_tags.ipynb # Tag frequency and usage visualizations
24
+ │ ├── Section_3-3-3_download_popular_models.ipynb # Download models for analysis
25
+ │ ├── Section_3-3-3_Figure_6.ipynb # Promotional tag usage patterns
26
+ │ ├── Section_3-3-4_Figure_8a.ipynb # Ranking of popular models
27
+ │ ├── Section_3-3-4_Figure_8b.ipynb # Continuation of model rankings
28
+ │ ├── Section_3-3-4_Figure_9_Sankey.ipynb # Sankey diagram: user-model contributions
29
+ │ ├── Section_3-3-4_LLM_annotation.ipynb # Annotations using large language models
30
+ │ ├── Section_3-4_extract_LoRA_metadata.ipynb # LoRA metadata extraction
31
+ │ ├── SuppM_Figure_12_Danbooru_Taxonomy.ipynb # Danbooru tag taxonomy: visualization
32
+ │ ├── SuppM_Figure_13_Danbooru_taxonomy.ipynb # Tag grouping and structure
33
+ │ └── SuppM_Figure_13.ipynb # Supplementary figure generation
34
+ ├── misc/ # Utility scripts and API credentials (excluded)
35
+ ├── plots/ # Output plots and visualizations
36
+ ├── public/ # Optional public-facing files
37
  ├── .gitignore
38
  ├── .gitmodules
39
+ ├── README.md # This file
40
+ └── requirements.txt # Python dependencies
41
  ```
42
 
43
  # Project Setup
gitlab-ci.yml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # .gitlab-ci.yml
3
+ pages:
4
+ stage: deploy
5
+ script:
6
+ - echo "Nothing to build, serving public/"
7
+ artifacts:
8
+ paths:
9
+ - public
10
+ only:
11
+ - main
{scripts → jupyter_notebooks}/0_Scraping_image_metadata.ipynb RENAMED
@@ -79,7 +79,7 @@
79
  },
80
  {
81
  "cell_type": "code",
82
- "execution_count": 3,
83
  "id": "f8decb63-43f5-4731-823d-94632eee7618",
84
  "metadata": {
85
  "execution": {
@@ -108,7 +108,7 @@
108
  },
109
  {
110
  "cell_type": "code",
111
- "execution_count": 4,
112
  "id": "4b2426c3-96a0-468e-b6dc-78dea9c3e92b",
113
  "metadata": {
114
  "execution": {
@@ -140,7 +140,7 @@
140
  "outputs": [],
141
  "source": [
142
  "# Define the input timestamp in ISO 8601 format\n",
143
- "input_timestamp = \"2025-02-01T00:00:00.000Z\" # point in time from when you want to obtain metadata (you can copy the timestamp from the last *.json batch obtained to get the data of longer timespans)\n",
144
  "\n",
145
  "# Function to convert an ISO 8601 date string to a Unix timestamp in milliseconds with a 2-hour offset \n",
146
  "def iso_to_timestamp(iso_str):\n",
@@ -304,7 +304,7 @@
304
  },
305
  {
306
  "cell_type": "code",
307
- "execution_count": 6,
308
  "id": "7c89cb68-983b-47df-a028-e02d7ca0829d",
309
  "metadata": {
310
  "execution": {
@@ -317,7 +317,7 @@
317
  },
318
  "outputs": [],
319
  "source": [
320
- "#get_image_metadata()"
321
  ]
322
  },
323
  {
@@ -1284,12 +1284,46 @@
1284
  "id": "49e35088-8b2e-4189-83e1-3098d55dcad2",
1285
  "metadata": {},
1286
  "outputs": [],
1287
- "source": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1288
  }
1289
  ],
1290
  "metadata": {
1291
  "kernelspec": {
1292
- "display_name": "Python 3 (ipykernel)",
1293
  "language": "python",
1294
  "name": "python3"
1295
  },
@@ -1303,7 +1337,7 @@
1303
  "name": "python",
1304
  "nbconvert_exporter": "python",
1305
  "pygments_lexer": "ipython3",
1306
- "version": "3.11.9"
1307
  }
1308
  },
1309
  "nbformat": 4,
 
79
  },
80
  {
81
  "cell_type": "code",
82
+ "execution_count": 1,
83
  "id": "f8decb63-43f5-4731-823d-94632eee7618",
84
  "metadata": {
85
  "execution": {
 
108
  },
109
  {
110
  "cell_type": "code",
111
+ "execution_count": 2,
112
  "id": "4b2426c3-96a0-468e-b6dc-78dea9c3e92b",
113
  "metadata": {
114
  "execution": {
 
140
  "outputs": [],
141
  "source": [
142
  "# Define the input timestamp in ISO 8601 format\n",
143
+ "input_timestamp = \"2025-03-24T12:59:03.335Z\" # point in time from when you want to obtain metadata (you can copy the timestamp from the last *.json batch obtained to get the data of longer timespans)\n",
144
  "\n",
145
  "# Function to convert an ISO 8601 date string to a Unix timestamp in milliseconds with a 2-hour offset \n",
146
  "def iso_to_timestamp(iso_str):\n",
 
304
  },
305
  {
306
  "cell_type": "code",
307
+ "execution_count": null,
308
  "id": "7c89cb68-983b-47df-a028-e02d7ca0829d",
309
  "metadata": {
310
  "execution": {
 
317
  },
318
  "outputs": [],
319
  "source": [
320
+ "get_image_metadata()"
321
  ]
322
  },
323
  {
 
1284
  "id": "49e35088-8b2e-4189-83e1-3098d55dcad2",
1285
  "metadata": {},
1286
  "outputs": [],
1287
+ "source": [
1288
+ "import pandas as pd\n",
1289
+ "import os\n",
1290
+ "\n",
1291
+ "# Load the dataset\n",
1292
+ "df = pd.read_csv('data/all_models_with_tags.csv')\n",
1293
+ "\n",
1294
+ "# Filter for rows where poi is True\n",
1295
+ "filtered_df = df[df['poi'] == True]\n",
1296
+ "os.makedirs('data/model_subsets', exist_ok=True)\n",
1297
+ "\n",
1298
+ "# Save the filtered DataFrame to a new CSV file\n",
1299
+ "filtered_df.to_csv('data/model_subsets/all_models_poi.csv', index=False)\n"
1300
+ ]
1301
+ },
1302
+ {
1303
+ "cell_type": "code",
1304
+ "execution_count": null,
1305
+ "id": "06c15f2c",
1306
+ "metadata": {},
1307
+ "outputs": [],
1308
+ "source": [
1309
+ "import pandas as pd\n",
1310
+ "import os\n",
1311
+ "\n",
1312
+ "# Load the dataset\n",
1313
+ "df = pd.read_csv('data/all_models_with_tags.csv')\n",
1314
+ "\n",
1315
+ "# Filter for rows where poi is True\n",
1316
+ "filtered_df = df[df['poi'] == False]\n",
1317
+ "os.makedirs('data/model_subsets', exist_ok=True)\n",
1318
+ "\n",
1319
+ "# Save the filtered DataFrame to a new CSV file\n",
1320
+ "filtered_df.to_csv('data/model_subsets/all_models_poi_false.csv', index=False)\n"
1321
+ ]
1322
  }
1323
  ],
1324
  "metadata": {
1325
  "kernelspec": {
1326
+ "display_name": "latm",
1327
  "language": "python",
1328
  "name": "python3"
1329
  },
 
1337
  "name": "python",
1338
  "nbconvert_exporter": "python",
1339
  "pygments_lexer": "ipython3",
1340
+ "version": "3.10.15"
1341
  }
1342
  },
1343
  "nbformat": 4,
jupyter_notebooks/0_Scraping_model_metadata.ipynb ADDED
@@ -0,0 +1,643 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "1111ea95-d385-49b9-a4d9-ef886ace5c7a",
6
+ "metadata": {
7
+ "execution": {
8
+ "iopub.execute_input": "2025-02-06T11:24:25.566747Z",
9
+ "iopub.status.busy": "2025-02-06T11:24:25.566066Z",
10
+ "iopub.status.idle": "2025-02-06T11:24:25.571748Z",
11
+ "shell.execute_reply": "2025-02-06T11:24:25.571305Z",
12
+ "shell.execute_reply.started": "2025-02-06T11:24:25.566705Z"
13
+ }
14
+ },
15
+ "source": [
16
+ "# 0 Scraping Metadata and Dataset consolidation\n"
17
+ ]
18
+ },
19
+ {
20
+ "cell_type": "markdown",
21
+ "id": "6632505a-e7ca-4463-9ffc-e36fad42235f",
22
+ "metadata": {},
23
+ "source": [
24
+ "## IMAGES\n",
25
+ "---"
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "markdown",
30
+ "id": "e3388bac-bb71-40bc-a693-9ac7a2d5f32c",
31
+ "metadata": {
32
+ "execution": {
33
+ "iopub.execute_input": "2025-02-06T10:08:22.229784Z",
34
+ "iopub.status.busy": "2025-02-06T10:08:22.229287Z",
35
+ "iopub.status.idle": "2025-02-06T10:08:22.232210Z",
36
+ "shell.execute_reply": "2025-02-06T10:08:22.231793Z",
37
+ "shell.execute_reply.started": "2025-02-06T10:08:22.229766Z"
38
+ }
39
+ },
40
+ "source": [
41
+ "### Step 1: Image metadata scraping, sorting and CSV consolidation"
42
+ ]
43
+ },
44
+ {
45
+ "cell_type": "code",
46
+ "execution_count": 1,
47
+ "id": "f8decb63-43f5-4731-823d-94632eee7618",
48
+ "metadata": {
49
+ "execution": {
50
+ "iopub.execute_input": "2025-02-08T19:37:51.115763Z",
51
+ "iopub.status.busy": "2025-02-08T19:37:51.114573Z",
52
+ "iopub.status.idle": "2025-02-08T19:37:51.170027Z",
53
+ "shell.execute_reply": "2025-02-08T19:37:51.169401Z",
54
+ "shell.execute_reply.started": "2025-02-08T19:37:51.115738Z"
55
+ }
56
+ },
57
+ "outputs": [],
58
+ "source": [
59
+ "import os\n",
60
+ "import json\n",
61
+ "import csv\n",
62
+ "import requests\n",
63
+ "from datetime import datetime\n",
64
+ "import time\n",
65
+ "from pathlib import Path\n",
66
+ "import hashlib\n",
67
+ "import pandas as pd\n",
68
+ "import sys\n",
69
+ "from datetime import datetime, timedelta\n",
70
+ "import shutil"
71
+ ]
72
+ },
73
+ {
74
+ "cell_type": "code",
75
+ "execution_count": 2,
76
+ "id": "4b2426c3-96a0-468e-b6dc-78dea9c3e92b",
77
+ "metadata": {
78
+ "execution": {
79
+ "iopub.execute_input": "2025-02-08T19:37:51.809027Z",
80
+ "iopub.status.busy": "2025-02-08T19:37:51.808835Z",
81
+ "iopub.status.idle": "2025-02-08T19:37:51.812922Z",
82
+ "shell.execute_reply": "2025-02-08T19:37:51.812429Z",
83
+ "shell.execute_reply.started": "2025-02-08T19:37:51.809009Z"
84
+ }
85
+ },
86
+ "outputs": [],
87
+ "source": [
88
+ "current_dir = Path.cwd()"
89
+ ]
90
+ },
91
+ {
92
+ "cell_type": "markdown",
93
+ "id": "11647bb7-5ce9-414a-8486-5bdce8d9cfea",
94
+ "metadata": {
95
+ "execution": {
96
+ "iopub.execute_input": "2025-02-06T12:49:43.126762Z",
97
+ "iopub.status.busy": "2025-02-06T12:49:43.125797Z",
98
+ "iopub.status.idle": "2025-02-06T12:49:43.129759Z",
99
+ "shell.execute_reply": "2025-02-06T12:49:43.129176Z",
100
+ "shell.execute_reply.started": "2025-02-06T12:49:43.126736Z"
101
+ }
102
+ },
103
+ "source": [
104
+ "## MODELS"
105
+ ]
106
+ },
107
+ {
108
+ "cell_type": "markdown",
109
+ "id": "5c9cb5e3-7cea-4574-9319-f3cd89354b1f",
110
+ "metadata": {
111
+ "execution": {
112
+ "iopub.execute_input": "2025-02-06T13:23:38.017078Z",
113
+ "iopub.status.busy": "2025-02-06T13:23:38.016639Z",
114
+ "iopub.status.idle": "2025-02-06T13:23:38.019993Z",
115
+ "shell.execute_reply": "2025-02-06T13:23:38.019549Z",
116
+ "shell.execute_reply.started": "2025-02-06T13:23:38.017053Z"
117
+ }
118
+ },
119
+ "source": [
120
+ "### Step 1: Scrape model metadata"
121
+ ]
122
+ },
123
+ {
124
+ "cell_type": "markdown",
125
+ "id": "83e12b9f-5ae1-407d-9754-5979d837f787",
126
+ "metadata": {
127
+ "execution": {
128
+ "iopub.execute_input": "2025-02-06T14:06:04.857874Z",
129
+ "iopub.status.busy": "2025-02-06T14:06:04.857500Z",
130
+ "iopub.status.idle": "2025-02-06T14:06:04.860438Z",
131
+ "shell.execute_reply": "2025-02-06T14:06:04.860030Z",
132
+ "shell.execute_reply.started": "2025-02-06T14:06:04.857856Z"
133
+ }
134
+ },
135
+ "source": [
136
+ "#### the resulting files will appear in data/raw/model_metadata as *.json"
137
+ ]
138
+ },
139
+ {
140
+ "cell_type": "code",
141
+ "execution_count": 12,
142
+ "id": "5db9c00e",
143
+ "metadata": {},
144
+ "outputs": [],
145
+ "source": [
146
+ "key_karussell = current_dir.parent / 'misc/credentials/civitai_api_keys.txt'\n",
147
+ "directory_path = current_dir.parent / 'data/raw/model_metadata/'"
148
+ ]
149
+ },
150
+ {
151
+ "cell_type": "code",
152
+ "execution_count": 13,
153
+ "id": "41ee14e0-fb78-4f91-aba1-13faf05af7d8",
154
+ "metadata": {
155
+ "execution": {
156
+ "iopub.execute_input": "2025-02-08T19:37:56.687832Z",
157
+ "iopub.status.busy": "2025-02-08T19:37:56.687251Z",
158
+ "iopub.status.idle": "2025-02-08T19:37:56.696572Z",
159
+ "shell.execute_reply": "2025-02-08T19:37:56.696059Z",
160
+ "shell.execute_reply.started": "2025-02-08T19:37:56.687809Z"
161
+ }
162
+ },
163
+ "outputs": [],
164
+ "source": [
165
+ "import datetime\n",
166
+ "\n",
167
+ "def load_api_keys():\n",
168
+ " \"\"\"Load API keys from a text file, one per line.\"\"\"\n",
169
+ " if not os.path.exists(key_karussell):\n",
170
+ " raise FileNotFoundError(f\"API key file '{API_KEYS_FILE}' not found!\")\n",
171
+ " \n",
172
+ " with open(key_karussell, 'r') as file:\n",
173
+ " keys = [line.strip() for line in file if line.strip()]\n",
174
+ " \n",
175
+ " if not keys:\n",
176
+ " raise ValueError(\"No API keys found in the file!\")\n",
177
+ " \n",
178
+ " return keys\n",
179
+ "\n",
180
+ "def get_model_metadata():\n",
181
+ " base_url = \"https://civitai.com/api/v1/models\"\n",
182
+ " params = {\"sort\": \"Newest\", \"nsfw\": True}\n",
183
+ "\n",
184
+ " # Load API keys\n",
185
+ " api_keys = load_api_keys()\n",
186
+ " key_index = 0 # Start with the first key\n",
187
+ "\n",
188
+ " page_counter = 0\n",
189
+ " max_pages = 300000000 # Adjust as needed\n",
190
+ " os.makedirs(directory_path, exist_ok=True)\n",
191
+ "\n",
192
+ " while True:\n",
193
+ " if page_counter >= max_pages:\n",
194
+ " print(f\"Reached the limit of {max_pages} pages.\")\n",
195
+ " break\n",
196
+ "\n",
197
+ " headers = {\n",
198
+ " \"Accept\": \"application/json\",\n",
199
+ " \"Authorization\": f\"Bearer {api_keys[key_index]}\"\n",
200
+ " }\n",
201
+ "\n",
202
+ " response = requests.get(base_url, headers=headers, params=params)\n",
203
+ "\n",
204
+ " if response.status_code == 200:\n",
205
+ " data = response.json()\n",
206
+ " page_counter += 1\n",
207
+ "\n",
208
+ " # Add timestamp\n",
209
+ " formatted_timestamp = datetime.datetime.now().strftime(\"data obtained on the %d.%m.%Y at %H:%M CEST\")\n",
210
+ "\n",
211
+ " data['timestamp'] = formatted_timestamp\n",
212
+ "\n",
213
+ " # Save data to file\n",
214
+ " file_path = os.path.join(directory_path, f'newest_models_{page_counter}.json')\n",
215
+ " with open(file_path, 'w', encoding='utf-8') as file:\n",
216
+ " json.dump(data, file, indent=4)\n",
217
+ "\n",
218
+ " # Check for nextCursor\n",
219
+ " next_cursor = data.get('metadata', {}).get('nextCursor')\n",
220
+ " if not next_cursor:\n",
221
+ " print(\"No more data available.\")\n",
222
+ " break\n",
223
+ " else:\n",
224
+ " params['cursor'] = next_cursor\n",
225
+ " \n",
226
+ " elif response.status_code in (401, 403): # Unauthorized or Forbidden\n",
227
+ " print(f\"API Key {key_index + 1} failed with status {response.status_code}. Trying next key...\")\n",
228
+ " key_index += 1\n",
229
+ "\n",
230
+ " if key_index >= len(api_keys):\n",
231
+ " print(\"All API keys failed. Exiting.\")\n",
232
+ " break # Stop if all keys fail\n",
233
+ " \n",
234
+ " else:\n",
235
+ " print(f\"Failed to fetch data: HTTP {response.status_code}\")\n",
236
+ " break # Stop on other errors\n"
237
+ ]
238
+ },
239
+ {
240
+ "cell_type": "markdown",
241
+ "id": "8f43ce23-5986-4b67-be2d-453831af9a6e",
242
+ "metadata": {},
243
+ "source": [
244
+ "uncomment this to get model metadata"
245
+ ]
246
+ },
247
+ {
248
+ "cell_type": "code",
249
+ "execution_count": 14,
250
+ "id": "3f95b4ba-5742-4268-b2e8-de9145faf495",
251
+ "metadata": {
252
+ "execution": {
253
+ "iopub.execute_input": "2025-02-08T19:37:58.117386Z",
254
+ "iopub.status.busy": "2025-02-08T19:37:58.117158Z",
255
+ "iopub.status.idle": "2025-02-08T19:37:58.121162Z",
256
+ "shell.execute_reply": "2025-02-08T19:37:58.120580Z",
257
+ "shell.execute_reply.started": "2025-02-08T19:37:58.117369Z"
258
+ }
259
+ },
260
+ "outputs": [
261
+ {
262
+ "name": "stdout",
263
+ "output_type": "stream",
264
+ "text": [
265
+ "Failed to fetch data: HTTP 500\n"
266
+ ]
267
+ }
268
+ ],
269
+ "source": [
270
+ "get_model_metadata()"
271
+ ]
272
+ },
273
+ {
274
+ "cell_type": "markdown",
275
+ "id": "1ed5f44e-612e-4b6d-8974-904fb3e058d6",
276
+ "metadata": {
277
+ "execution": {
278
+ "iopub.execute_input": "2025-02-06T12:52:40.162173Z",
279
+ "iopub.status.busy": "2025-02-06T12:52:40.159989Z",
280
+ "iopub.status.idle": "2025-02-06T12:52:40.170634Z",
281
+ "shell.execute_reply": "2025-02-06T12:52:40.169945Z",
282
+ "shell.execute_reply.started": "2025-02-06T12:52:40.162124Z"
283
+ }
284
+ },
285
+ "source": [
286
+ "### Step 2 Consolidate Model-dataset CSV"
287
+ ]
288
+ },
289
+ {
290
+ "cell_type": "code",
291
+ "execution_count": 8,
292
+ "id": "464d82f5-c24e-4b53-9b50-682fa4bf3430",
293
+ "metadata": {
294
+ "execution": {
295
+ "iopub.execute_input": "2025-02-08T19:37:59.052245Z",
296
+ "iopub.status.busy": "2025-02-08T19:37:59.051645Z",
297
+ "iopub.status.idle": "2025-02-08T19:37:59.056017Z",
298
+ "shell.execute_reply": "2025-02-08T19:37:59.055598Z",
299
+ "shell.execute_reply.started": "2025-02-08T19:37:59.052224Z"
300
+ }
301
+ },
302
+ "outputs": [],
303
+ "source": [
304
+ "## path thingy\n",
305
+ "try: #scripts\n",
306
+ " current_dir = Path(__file__).resolve().parent\n",
307
+ "except NameError:\n",
308
+ " # jupyter\n",
309
+ " current_dir = Path.cwd()"
310
+ ]
311
+ },
312
+ {
313
+ "cell_type": "code",
314
+ "execution_count": null,
315
+ "id": "14f3db4d-ef93-4629-8a27-971adb49248b",
316
+ "metadata": {
317
+ "execution": {
318
+ "iopub.execute_input": "2025-02-08T19:37:59.443457Z",
319
+ "iopub.status.busy": "2025-02-08T19:37:59.443236Z",
320
+ "iopub.status.idle": "2025-02-08T19:37:59.890103Z",
321
+ "shell.execute_reply": "2025-02-08T19:37:59.889571Z",
322
+ "shell.execute_reply.started": "2025-02-08T19:37:59.443439Z"
323
+ }
324
+ },
325
+ "outputs": [
326
+ {
327
+ "name": "stdout",
328
+ "output_type": "stream",
329
+ "text": [
330
+ "Processing file: /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/raw/model_metadata/newest_models_2.json\n",
331
+ "Processing file: /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/raw/model_metadata/newest_models_1.json\n",
332
+ "Processing file: /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/raw/model_metadata/newest_models_4.json\n",
333
+ "Processing file: /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/raw/model_metadata/newest_models_6.json\n",
334
+ "Processing file: /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/raw/model_metadata/newest_models_7.json\n",
335
+ "Processing file: /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/raw/model_metadata/newest_models_5.json\n",
336
+ "Processing file: /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/raw/model_metadata/newest_models_3.json\n"
337
+ ]
338
+ }
339
+ ],
340
+ "source": [
341
+ "import os\n",
342
+ "import json\n",
343
+ "import pandas as pd\n",
344
+ "import hashlib\n",
345
+ "from pathlib import Path\n",
346
+ "from datetime import datetime, timezone\n",
347
+ "\n",
348
+ "\n",
349
+ "\n",
350
+ "\n",
351
+ "def hash_username(username):\n",
352
+ " return hashlib.sha256(username.encode('utf-8')).hexdigest()[:16]\n",
353
+ "\n",
354
+ "def parse_date(date_str):\n",
355
+ " try:\n",
356
+ " return datetime.fromisoformat(date_str.replace('Z', '+00:00'))\n",
357
+ " except Exception:\n",
358
+ " return datetime.min.replace(tzinfo=timezone.utc) # Make it timezone-aware\n",
359
+ "\n",
360
+ "def get_latest_model_version(model_versions):\n",
361
+ " return max(model_versions, key=lambda mv: parse_date(mv.get('publishedAt', '')))\n",
362
+ "\n",
363
+ "def process_directory_recursively(root_dir):\n",
364
+ " root_path = Path(root_dir)\n",
365
+ " seen = {} # id -> (publishedAt, record)\n",
366
+ " data_records = []\n",
367
+ "\n",
368
+ " for json_file in root_path.rglob('*.json'):\n",
369
+ " if not json_file.is_file():\n",
370
+ " continue\n",
371
+ "\n",
372
+ " #print(f\"Processing file: {json_file}\")\n",
373
+ " try:\n",
374
+ " with open(json_file, 'r', encoding='utf-8') as f:\n",
375
+ " data = json.load(f)\n",
376
+ " except Exception as e:\n",
377
+ " print(f\"Failed to load {json_file}: {e}\")\n",
378
+ " continue\n",
379
+ "\n",
380
+ " items = data.get('items') or data.get('data') or []\n",
381
+ " for item in items:\n",
382
+ " if not isinstance(item, dict):\n",
383
+ " continue\n",
384
+ "\n",
385
+ " model_id = item.get('id')\n",
386
+ " model_versions = item.get('modelVersions', [])\n",
387
+ " if not model_versions:\n",
388
+ " continue\n",
389
+ "\n",
390
+ " latest_version = get_latest_model_version(model_versions)\n",
391
+ " published_at = latest_version.get('publishedAt', '')\n",
392
+ " current_dt = parse_date(published_at)\n",
393
+ "\n",
394
+ " if model_id in seen and current_dt <= seen[model_id][0]:\n",
395
+ " continue\n",
396
+ " seen[model_id] = (current_dt, item)\n",
397
+ "\n",
398
+ " for model_id, (_, item) in seen.items():\n",
399
+ " model_versions = item.get('modelVersions', [])\n",
400
+ " latest_version = get_latest_model_version(model_versions)\n",
401
+ " version_ids = [mv.get('id', '') for mv in model_versions[:20]]\n",
402
+ "\n",
403
+ " files = latest_version.get('files', [])\n",
404
+ " auto_hashes = files[0].get('hashes', {}) if files else {}\n",
405
+ " images = latest_version.get('images', [])\n",
406
+ " first_image_url = images[0]['url'] if images else ''\n",
407
+ " latest_image_url = images[-1]['url'] if images else ''\n",
408
+ "\n",
409
+ " username = item.get('creator', {}).get('username', '')\n",
410
+ " record = {\n",
411
+ " 'id': item.get('id', ''),\n",
412
+ " 'name': item.get('name', ''),\n",
413
+ " 'type': item.get('type', ''),\n",
414
+ " 'baseModel': latest_version.get('baseModel', ''),\n",
415
+ " 'downloadCount': item.get('stats', {}).get('downloadCount', 0),\n",
416
+ " 'nsfwLevel': item.get('nsfwLevel', 0),\n",
417
+ " 'modelVersions': len(model_versions),\n",
418
+ " 'publishedAt': latest_version.get('publishedAt', ''),\n",
419
+ " 'usernameHash': hash_username(username) if username else '',\n",
420
+ " 'downloadUrl': latest_version.get('downloadUrl', ''),\n",
421
+ " 'firstImageUrl': first_image_url,\n",
422
+ " 'latestImageUrl': latest_image_url,\n",
423
+ " 'poi': item.get('poi', False),\n",
424
+ " 'AutoV1': auto_hashes.get('AutoV1', ''),\n",
425
+ " 'AutoV2': auto_hashes.get('AutoV2', ''),\n",
426
+ " 'AutoV3': auto_hashes.get('AutoV3', ''),\n",
427
+ " 'SHA256': auto_hashes.get('SHA256', ''),\n",
428
+ " 'CRC32': auto_hashes.get('CRC32', ''),\n",
429
+ " 'BLAKE3': auto_hashes.get('BLAKE3', ''),\n",
430
+ " 'previewImage': latest_image_url\n",
431
+ " }\n",
432
+ "\n",
433
+ " for i in range(20):\n",
434
+ " record[f'version_id_{i+1}'] = version_ids[i] if i < len(version_ids) else ''\n",
435
+ "\n",
436
+ " tags = item.get('tags', [])\n",
437
+ " for i in range(7):\n",
438
+ " record[f'tag_{i+1}'] = tags[i] if i < len(tags) else ''\n",
439
+ "\n",
440
+ " data_records.append(record)\n",
441
+ "\n",
442
+ " return pd.DataFrame(data_records)\n",
443
+ "\n",
444
+ "# Usage Example\n",
445
+ "root_directory = current_dir.parent / 'data/raw/model_metadata/'\n",
446
+ "df = process_directory_recursively(root_directory)\n",
447
+ "df_sorted = df.sort_values(by='downloadCount', ascending=False)\n",
448
+ "\n",
449
+ "# Optionally save\n",
450
+ "# df_sorted.to_csv('combined_metadata.csv', index=False)\n"
451
+ ]
452
+ },
453
+ {
454
+ "cell_type": "markdown",
455
+ "id": "2d6f822f-754f-4feb-9ed6-6f868421c454",
456
+ "metadata": {},
457
+ "source": [
458
+ "### Save model-data to CSV"
459
+ ]
460
+ },
461
+ {
462
+ "cell_type": "code",
463
+ "execution_count": null,
464
+ "id": "f861cf7b-5eb3-46ad-ab2d-9e2fdf2169b4",
465
+ "metadata": {
466
+ "execution": {
467
+ "iopub.execute_input": "2025-02-08T19:38:02.047421Z",
468
+ "iopub.status.busy": "2025-02-08T19:38:02.046761Z",
469
+ "iopub.status.idle": "2025-02-08T19:38:02.193381Z",
470
+ "shell.execute_reply": "2025-02-08T19:38:02.192886Z",
471
+ "shell.execute_reply.started": "2025-02-08T19:38:02.047397Z"
472
+ }
473
+ },
474
+ "outputs": [
475
+ {
476
+ "name": "stdout",
477
+ "output_type": "stream",
478
+ "text": [
479
+ "Data has been saved to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/CSV/Civiverse-Models.csv\n"
480
+ ]
481
+ }
482
+ ],
483
+ "source": [
484
+ "output_csv = current_dir.parent / 'data/CSV/Civiverse-Models_2025.csv'\n",
485
+ "output_csv.parent.mkdir(parents=True, exist_ok=True)\n",
486
+ "df_sorted.to_csv(output_csv, index=False)\n",
487
+ "print(f\"Data has been saved to {output_csv}\")"
488
+ ]
489
+ },
490
+ {
491
+ "cell_type": "markdown",
492
+ "id": "9b948933-2a2f-42b0-9083-2d81586ae3f2",
493
+ "metadata": {
494
+ "execution": {
495
+ "iopub.execute_input": "2025-02-06T13:42:32.447860Z",
496
+ "iopub.status.busy": "2025-02-06T13:42:32.446832Z",
497
+ "iopub.status.idle": "2025-02-06T13:42:32.453827Z",
498
+ "shell.execute_reply": "2025-02-06T13:42:32.453271Z",
499
+ "shell.execute_reply.started": "2025-02-06T13:42:32.447821Z"
500
+ }
501
+ },
502
+ "source": [
503
+ "### Step 3 Create Subsets: Checkpoint only, POI True, POI False"
504
+ ]
505
+ },
506
+ {
507
+ "cell_type": "code",
508
+ "execution_count": 11,
509
+ "id": "ae894e26-4984-40e6-80a6-54fb6b61c873",
510
+ "metadata": {
511
+ "execution": {
512
+ "iopub.execute_input": "2025-02-08T19:38:03.089160Z",
513
+ "iopub.status.busy": "2025-02-08T19:38:03.088490Z",
514
+ "iopub.status.idle": "2025-02-08T19:38:03.093067Z",
515
+ "shell.execute_reply": "2025-02-08T19:38:03.092634Z",
516
+ "shell.execute_reply.started": "2025-02-08T19:38:03.089138Z"
517
+ }
518
+ },
519
+ "outputs": [],
520
+ "source": [
521
+ "file_path = current_dir.parent / 'data/CSV/Civiverse-Models.csv' # Update this with your actual file path\n",
522
+ "(current_dir.parent / 'data/CSV/model_subsets').mkdir(parents=True, exist_ok=True)\n"
523
+ ]
524
+ },
525
+ {
526
+ "cell_type": "code",
527
+ "execution_count": 13,
528
+ "id": "88438f88-c723-423e-a4e5-e07f59096b72",
529
+ "metadata": {
530
+ "execution": {
531
+ "iopub.execute_input": "2025-02-08T19:41:39.279495Z",
532
+ "iopub.status.busy": "2025-02-08T19:41:39.279039Z",
533
+ "iopub.status.idle": "2025-02-08T19:41:39.674445Z",
534
+ "shell.execute_reply": "2025-02-08T19:41:39.673981Z",
535
+ "shell.execute_reply.started": "2025-02-08T19:41:39.279476Z"
536
+ }
537
+ },
538
+ "outputs": [
539
+ {
540
+ "name": "stdout",
541
+ "output_type": "stream",
542
+ "text": [
543
+ "Files saved successfully!\n"
544
+ ]
545
+ }
546
+ ],
547
+ "source": [
548
+ "import pandas as pd\n",
549
+ "\n",
550
+ "# Load the dataset\n",
551
+ "\n",
552
+ "data = pd.read_csv(file_path)\n",
553
+ "\n",
554
+ "# Version 1: Only 'poi' true models\n",
555
+ "poi_true_models = data[data['poi'] == True]\n",
556
+ "\n",
557
+ "# Version 2: Only types lora, dora, locon, textual inversion\n",
558
+ "specific_types = ['LORA', 'DORA', 'LOCON', 'textualInversion']\n",
559
+ "adapters = data[data['type'].isin(specific_types)]\n",
560
+ "\n",
561
+ "# Version 3: Only type checkpoint\n",
562
+ "checkpoint_models = data[data['type'] == 'Checkpoint']\n",
563
+ "\n",
564
+ "# Version 4: All models apart from 'poi' true\n",
565
+ "non_poi_models = data[data['poi'] != True]\n",
566
+ "\n",
567
+ "# Version 5: All models apart from 'poi' true and with nsfwLevel below 13\n",
568
+ "non_poi_low_nsfw_models = data[(data['poi'] != True) & (data['nsfwLevel'] < 13)]\n",
569
+ "\n",
570
+ "# Save the versions as separate CSV files\n",
571
+ "poi_true_models.to_csv(current_dir.parent / 'data/CSV/model_subsets/Civiverse_adapters_poi_true.csv', index=False)\n",
572
+ "adapters.to_csv(current_dir.parent / 'data/CSV/adapters.csv', index=False)\n",
573
+ "checkpoint_models.to_csv(current_dir.parent / 'data/CSV/model_subsets/Civiverse_checkpoint_only.csv', index=False)\n",
574
+ "non_poi_models.to_csv(current_dir.parent / 'data/CSV/model_subsets/Civiverse_adapters_poi_false.csv', index=False)\n",
575
+ "\n",
576
+ "print(\"Files saved successfully!\")\n"
577
+ ]
578
+ },
579
+ {
580
+ "cell_type": "code",
581
+ "execution_count": null,
582
+ "id": "49e35088-8b2e-4189-83e1-3098d55dcad2",
583
+ "metadata": {},
584
+ "outputs": [],
585
+ "source": [
586
+ "import pandas as pd\n",
587
+ "import os\n",
588
+ "\n",
589
+ "# Load the dataset\n",
590
+ "df = pd.read_csv('data/all_models_with_tags.csv')\n",
591
+ "\n",
592
+ "# Filter for rows where poi is True\n",
593
+ "filtered_df = df[df['poi'] == True]\n",
594
+ "os.makedirs('data/model_subsets', exist_ok=True)\n",
595
+ "\n",
596
+ "# Save the filtered DataFrame to a new CSV file\n",
597
+ "filtered_df.to_csv('data/model_subsets/all_models_poi.csv', index=False)\n"
598
+ ]
599
+ },
600
+ {
601
+ "cell_type": "code",
602
+ "execution_count": null,
603
+ "id": "06c15f2c",
604
+ "metadata": {},
605
+ "outputs": [],
606
+ "source": [
607
+ "import pandas as pd\n",
608
+ "import os\n",
609
+ "\n",
610
+ "# Load the dataset\n",
611
+ "df = pd.read_csv('data/all_models_with_tags.csv')\n",
612
+ "\n",
613
+ "# Filter for rows where poi is True\n",
614
+ "filtered_df = df[df['poi'] == False]\n",
615
+ "os.makedirs('data/model_subsets', exist_ok=True)\n",
616
+ "\n",
617
+ "# Save the filtered DataFrame to a new CSV file\n",
618
+ "filtered_df.to_csv('data/model_subsets/all_models_poi_false.csv', index=False)\n"
619
+ ]
620
+ }
621
+ ],
622
+ "metadata": {
623
+ "kernelspec": {
624
+ "display_name": "latm",
625
+ "language": "python",
626
+ "name": "python3"
627
+ },
628
+ "language_info": {
629
+ "codemirror_mode": {
630
+ "name": "ipython",
631
+ "version": 3
632
+ },
633
+ "file_extension": ".py",
634
+ "mimetype": "text/x-python",
635
+ "name": "python",
636
+ "nbconvert_exporter": "python",
637
+ "pygments_lexer": "ipython3",
638
+ "version": "3.10.15"
639
+ }
640
+ },
641
+ "nbformat": 4,
642
+ "nbformat_minor": 5
643
+ }
scripts/Section_2_Image_grid.ipynb → jupyter_notebooks/Section_1_Figure_1_image_grid.ipynb RENAMED
@@ -367,7 +367,7 @@
367
  ],
368
  "metadata": {
369
  "kernelspec": {
370
- "display_name": "Python 3 (ipykernel)",
371
  "language": "python",
372
  "name": "python3"
373
  },
@@ -381,7 +381,7 @@
381
  "name": "python",
382
  "nbconvert_exporter": "python",
383
  "pygments_lexer": "ipython3",
384
- "version": "3.11.9"
385
  }
386
  },
387
  "nbformat": 4,
 
367
  ],
368
  "metadata": {
369
  "kernelspec": {
370
+ "display_name": "latm",
371
  "language": "python",
372
  "name": "python3"
373
  },
 
381
  "name": "python",
382
  "nbconvert_exporter": "python",
383
  "pygments_lexer": "ipython3",
384
+ "version": "3.10.15"
385
  }
386
  },
387
  "nbformat": 4,
scripts/Section_6-1_Histograms.ipynb → jupyter_notebooks/Section_2-2-2_Figure_3_histogram_monthly_images_nsfw_levels.ipynb RENAMED
@@ -454,7 +454,7 @@
454
  ],
455
  "metadata": {
456
  "kernelspec": {
457
- "display_name": "Python 3 (ipykernel)",
458
  "language": "python",
459
  "name": "python3"
460
  },
@@ -468,7 +468,7 @@
468
  "name": "python",
469
  "nbconvert_exporter": "python",
470
  "pygments_lexer": "ipython3",
471
- "version": "3.11.9"
472
  }
473
  },
474
  "nbformat": 4,
 
454
  ],
455
  "metadata": {
456
  "kernelspec": {
457
+ "display_name": "latm",
458
  "language": "python",
459
  "name": "python3"
460
  },
 
468
  "name": "python",
469
  "nbconvert_exporter": "python",
470
  "pygments_lexer": "ipython3",
471
+ "version": "3.10.15"
472
  }
473
  },
474
  "nbformat": 4,
scripts/Section_6-2_Demographics.ipynb → jupyter_notebooks/Section_2-2-2_Figure_4_Demographic_patterns_in_gen_images.ipynb RENAMED
@@ -189,6 +189,14 @@
189
  "mivolo_out.mkdir(parents=True, exist_ok=True) # Create the output directory if it doesn't exist"
190
  ]
191
  },
 
 
 
 
 
 
 
 
192
  {
193
  "cell_type": "code",
194
  "execution_count": null,
@@ -1470,6 +1478,14 @@
1470
  " print(f\"File not found: {input_file_path}\")"
1471
  ]
1472
  },
 
 
 
 
 
 
 
 
1473
  {
1474
  "cell_type": "code",
1475
  "execution_count": null,
 
189
  "mivolo_out.mkdir(parents=True, exist_ok=True) # Create the output directory if it doesn't exist"
190
  ]
191
  },
192
+ {
193
+ "cell_type": "markdown",
194
+ "id": "ffb7dd23",
195
+ "metadata": {},
196
+ "source": [
197
+ "## MiVOLO gender and age inference"
198
+ ]
199
+ },
200
  {
201
  "cell_type": "code",
202
  "execution_count": null,
 
1478
  " print(f\"File not found: {input_file_path}\")"
1479
  ]
1480
  },
1481
+ {
1482
+ "cell_type": "markdown",
1483
+ "id": "26aeeef7",
1484
+ "metadata": {},
1485
+ "source": [
1486
+ "## Visualization code"
1487
+ ]
1488
+ },
1489
  {
1490
  "cell_type": "code",
1491
  "execution_count": null,
scripts/Section_6-3_Models.ipynb → jupyter_notebooks/Section_2-3-2_top_10_most_popular_checkpoints.ipynb RENAMED
File without changes
scripts/Section_6-4_Top_models.ipynb → jupyter_notebooks/Section_2-3-3_Figure_7_top_30_adapters.ipynb RENAMED
File without changes
jupyter_notebooks/Section_2-3-4__Figure_8_Deepfake_victims.ipynb ADDED
@@ -0,0 +1,668 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "06763fde",
6
+ "metadata": {},
7
+ "source": [
8
+ "# LLM annotation of Deepfake adapters"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "markdown",
13
+ "id": "b773045a",
14
+ "metadata": {},
15
+ "source": [
16
+ "## Step 01 Data cleaning NER using spaCy"
17
+ ]
18
+ },
19
+ {
20
+ "cell_type": "markdown",
21
+ "id": "234a55e5",
22
+ "metadata": {},
23
+ "source": [
24
+ "#### Here we clean leetspeak and architectre specifiers from the names as a preprossesing step for the named entity recognition NER below"
25
+ ]
26
+ },
27
+ {
28
+ "cell_type": "code",
29
+ "execution_count": 1,
30
+ "id": "f177df11",
31
+ "metadata": {},
32
+ "outputs": [],
33
+ "source": [
34
+ "import pandas as pd\n",
35
+ "import spacy\n",
36
+ "import re\n",
37
+ "import torch\n",
38
+ "from pathlib import Path\n",
39
+ "import unicodedata\n"
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "code",
44
+ "execution_count": 2,
45
+ "id": "a2383f32",
46
+ "metadata": {},
47
+ "outputs": [],
48
+ "source": [
49
+ "current_dir = Path.cwd()\n",
50
+ "poi_models_dir = current_dir.parent / \"data/CSV/model_subsets/POI_models.csv\" ### POI models dataset\n",
51
+ "output = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_01.csv\" ### Output file"
52
+ ]
53
+ },
54
+ {
55
+ "cell_type": "code",
56
+ "execution_count": 3,
57
+ "id": "66fc691f",
58
+ "metadata": {},
59
+ "outputs": [
60
+ {
61
+ "name": "stderr",
62
+ "output_type": "stream",
63
+ "text": [
64
+ "/home/lauhp/anaconda3/envs/latm/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
65
+ " from .autonotebook import tqdm as notebook_tqdm\n"
66
+ ]
67
+ }
68
+ ],
69
+ "source": [
70
+ "nlp = spacy.load(\"en_core_web_sm\") # or another model of your choice"
71
+ ]
72
+ },
73
+ {
74
+ "cell_type": "code",
75
+ "execution_count": 4,
76
+ "id": "8348c4a4",
77
+ "metadata": {},
78
+ "outputs": [
79
+ {
80
+ "name": "stdout",
81
+ "output_type": "stream",
82
+ "text": [
83
+ "Done! Saved to NER_poi_step_01.csv\n"
84
+ ]
85
+ }
86
+ ],
87
+ "source": [
88
+ "def preprocess_name(name):\n",
89
+ " name = str(name)\n",
90
+ "\n",
91
+ " # Normalize unicode characters (e.g., fancy fonts)\n",
92
+ " name = unicodedata.normalize(\"NFKD\", name)\n",
93
+ "\n",
94
+ " # Lowercase everything\n",
95
+ " name = name.lower()\n",
96
+ "\n",
97
+ " # Remove special keywords and patterns\n",
98
+ " junk_words = [\n",
99
+ " 'jav', 'jp', 'lora', 'locon', 'lycoris', 'requested', 'japanese', 'model',\n",
100
+ " 'flux', 'flux1.d', 'pony', 'realistic'\n",
101
+ " ]\n",
102
+ " for word in junk_words:\n",
103
+ " name = re.sub(rf'\\b{re.escape(word)}\\b', '', name, flags=re.IGNORECASE)\n",
104
+ "\n",
105
+ " # Remove versions like v1, v2.0, etc.\n",
106
+ " name = re.sub(r'v\\.?\\d+(\\.\\d+)?', '', name)\n",
107
+ "\n",
108
+ " # Remove 'not' followed by a word\n",
109
+ " name = re.sub(r'\\bnot\\s+\\w+', '', name)\n",
110
+ "\n",
111
+ " # Replace underscores and pipes with spaces\n",
112
+ " name = re.sub(r'[_|]', ' ', name)\n",
113
+ "\n",
114
+ " # Remove parentheses and content within\n",
115
+ " name = re.sub(r'\\(.*?\\)', '', name)\n",
116
+ "\n",
117
+ " # Remove excess whitespace\n",
118
+ " name = re.sub(r'\\s+', ' ', name).strip()\n",
119
+ "\n",
120
+ " return name\n",
121
+ "\n",
122
+ "# -------------------------\n",
123
+ "# Fallback Extractor\n",
124
+ "# -------------------------\n",
125
+ "def fallback_extract(text):\n",
126
+ " words = text.split()\n",
127
+ " capitalized = [w for w in words if w and w[0].isalpha()]\n",
128
+ " if len(capitalized) >= 2:\n",
129
+ " return \" \".join(capitalized[:2])\n",
130
+ " elif capitalized:\n",
131
+ " return capitalized[0]\n",
132
+ " return None\n",
133
+ "\n",
134
+ "# -------------------------\n",
135
+ "# Full Extraction Logic\n",
136
+ "# -------------------------\n",
137
+ "def extract_real_name(raw_name):\n",
138
+ " cleaned = preprocess_name(raw_name)\n",
139
+ " doc = nlp(cleaned)\n",
140
+ " persons = [ent.text for ent in doc.ents if ent.label_ == \"PERSON\"]\n",
141
+ " if persons:\n",
142
+ " return persons[0]\n",
143
+ " return fallback_extract(cleaned)\n",
144
+ "\n",
145
+ "# -------------------------\n",
146
+ "# Load Data and Apply\n",
147
+ "# -------------------------\n",
148
+ "df = pd.read_csv(poi_models_dir) # Or your own path\n",
149
+ "\n",
150
+ "# Apply the full extractor\n",
151
+ "texts = df['name'].astype(str).tolist()\n",
152
+ "docs = list(nlp.pipe([preprocess_name(t) for t in texts], batch_size=32))\n",
153
+ "\n",
154
+ "def extract_from_doc(doc, raw_text):\n",
155
+ " persons = [ent.text for ent in doc.ents if ent.label_ == \"PERSON\"]\n",
156
+ " if persons:\n",
157
+ " return persons[0]\n",
158
+ " return fallback_extract(preprocess_name(raw_text))\n",
159
+ "\n",
160
+ "df['real_name'] = [extract_from_doc(doc, raw_text) for doc, raw_text in zip(docs, texts)]\n",
161
+ "\n",
162
+ "# Save the result\n",
163
+ "df.to_csv(output, index=False)\n",
164
+ "print(\"Done! Saved to NER_poi_step_01.csv\")\n"
165
+ ]
166
+ },
167
+ {
168
+ "cell_type": "markdown",
169
+ "id": "a40b17fe",
170
+ "metadata": {},
171
+ "source": [
172
+ "## Step 2: compare country and profession with lists"
173
+ ]
174
+ },
175
+ {
176
+ "cell_type": "code",
177
+ "execution_count": 5,
178
+ "id": "414954ed",
179
+ "metadata": {},
180
+ "outputs": [
181
+ {
182
+ "name": "stdout",
183
+ "output_type": "stream",
184
+ "text": [
185
+ " real_name likely_country likely_nationality likely_profession\n",
186
+ "0 ronnie alonte model\n",
187
+ "1 zh elena kamperi twitch streamer\n",
188
+ "2 sofia vergara model\n",
189
+ "3 安然 anran celebrity\n",
190
+ "4 zoe kravitz celebrity\n"
191
+ ]
192
+ }
193
+ ],
194
+ "source": [
195
+ "import pandas as pd\n",
196
+ "from pathlib import Path\n",
197
+ "\n",
198
+ "# Set up paths\n",
199
+ "current_dir = Path.cwd()\n",
200
+ "countries = current_dir.parent / \"misc/lists/countries.csv\"\n",
201
+ "professions = current_dir.parent / \"misc/lists/professions.csv\"\n",
202
+ "inputNER = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_01.csv\"\n",
203
+ "outfile = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_02.csv\"\n",
204
+ "\n",
205
+ "# Load datasets\n",
206
+ "poi_df = pd.read_csv(inputNER)\n",
207
+ "countries_df = pd.read_csv(countries)\n",
208
+ "professions_df = pd.read_csv(professions)\n",
209
+ "\n",
210
+ "# Step 1: Combine tags into one lowercase list\n",
211
+ "def combine_tags(row):\n",
212
+ " return [str(row[f\"tag_{i}\"]).strip().lower() for i in range(1, 8) if pd.notna(row.get(f\"tag_{i}\"))]\n",
213
+ "\n",
214
+ "poi_df[\"tags\"] = poi_df.apply(combine_tags, axis=1)\n",
215
+ "\n",
216
+ "# Step 2: Build tag → (country, nationality) mapping\n",
217
+ "tag_to_country_nationality = {}\n",
218
+ "\n",
219
+ "for _, row in countries_df.iterrows():\n",
220
+ " country = str(row[\"en_short_name\"]).strip()\n",
221
+ " nationality = str(row[\"nationality\"]).strip()\n",
222
+ "\n",
223
+ " country_lc = country.lower()\n",
224
+ " nationality_lc = nationality.lower()\n",
225
+ "\n",
226
+ " # Add variations to the mapping\n",
227
+ " tag_to_country_nationality[country_lc] = (country, \"\")\n",
228
+ " tag_to_country_nationality[nationality_lc] = (\"\", nationality)\n",
229
+ " tag_to_country_nationality[country_lc.replace(\" \", \"\")] = (country, \"\")\n",
230
+ " tag_to_country_nationality[nationality_lc.replace(\" \", \"\")] = (\"\", nationality)\n",
231
+ "\n",
232
+ " for part in country_lc.split():\n",
233
+ " tag_to_country_nationality[part] = (country, \"\")\n",
234
+ " for part in nationality_lc.split():\n",
235
+ " tag_to_country_nationality[part] = (\"\", nationality)\n",
236
+ "\n",
237
+ "# Step 3: Infer likely_country and likely_nationality\n",
238
+ "# Step 3: Infer likely_country and likely_nationality\n",
239
+ "def infer_country_and_nationality(tags):\n",
240
+ " for tag in tags:\n",
241
+ " cleaned = tag.replace(\" \", \"\").lower()\n",
242
+ " if cleaned in tag_to_country_nationality:\n",
243
+ " country, nationality = tag_to_country_nationality[cleaned]\n",
244
+ " # Special case: skip if country is \"Isle of Man\"\n",
245
+ " if country == \"Isle of Man\":\n",
246
+ " country = \"\"\n",
247
+ " return pd.Series([country, nationality])\n",
248
+ " return pd.Series([\"\", \"\"])\n",
249
+ "\n",
250
+ "\n",
251
+ "poi_df[[\"likely_country\", \"likely_nationality\"]] = poi_df[\"tags\"].apply(infer_country_and_nationality)\n",
252
+ "\n",
253
+ "# Step 4: Build tag → profession mapping\n",
254
+ "profession_alias_map = {}\n",
255
+ "\n",
256
+ "for _, row in professions_df.iterrows():\n",
257
+ " canonical = str(row['profession']).strip().lower()\n",
258
+ " profession_alias_map[canonical] = canonical\n",
259
+ " for alias_col in ['alias_1', 'alias_2', 'alias_3']:\n",
260
+ " alias = row.get(alias_col)\n",
261
+ " if pd.notna(alias):\n",
262
+ " profession_alias_map[str(alias).strip().lower()] = canonical\n",
263
+ "\n",
264
+ "# Step 5: Infer likely profession from tags\n",
265
+ "def infer_profession_from_tags(tags):\n",
266
+ " matched = []\n",
267
+ " for tag in tags:\n",
268
+ " cleaned = tag.strip().lower()\n",
269
+ " if cleaned in profession_alias_map:\n",
270
+ " matched.append(profession_alias_map[cleaned])\n",
271
+ "\n",
272
+ " if not matched:\n",
273
+ " return \"\"\n",
274
+ " if \"celebrity\" in matched and len(set(matched)) > 1:\n",
275
+ " # Drop 'celebrity' if other professions are present\n",
276
+ " matched = [m for m in matched if m != \"celebrity\"]\n",
277
+ "\n",
278
+ " return matched[0] # Return the first specific match\n",
279
+ "\n",
280
+ "\n",
281
+ "poi_df[\"likely_profession\"] = poi_df[\"tags\"].apply(infer_profession_from_tags)\n",
282
+ "\n",
283
+ "# Step 6: Save enriched dataset\n",
284
+ "poi_df.to_csv(outfile, index=False)\n",
285
+ "\n",
286
+ "# Optional: Preview\n",
287
+ "print(poi_df[[\"real_name\", \"likely_country\", \"likely_nationality\", \"likely_profession\"]].head())\n"
288
+ ]
289
+ },
290
+ {
291
+ "cell_type": "code",
292
+ "execution_count": 6,
293
+ "id": "054f230b",
294
+ "metadata": {},
295
+ "outputs": [],
296
+ "source": [
297
+ "#!pip install transformers torch\n",
298
+ "#!python -m spacy download en_core_web_trf\n",
299
+ "#pip install openai"
300
+ ]
301
+ },
302
+ {
303
+ "cell_type": "markdown",
304
+ "id": "59185461",
305
+ "metadata": {},
306
+ "source": [
307
+ "## Step 3: Query Deepseek-v3 with NAME and HINTS"
308
+ ]
309
+ },
310
+ {
311
+ "cell_type": "code",
312
+ "execution_count": 7,
313
+ "id": "504b970f",
314
+ "metadata": {},
315
+ "outputs": [],
316
+ "source": [
317
+ "#!pip install openpyxl"
318
+ ]
319
+ },
320
+ {
321
+ "cell_type": "code",
322
+ "execution_count": 10,
323
+ "id": "7c209115",
324
+ "metadata": {},
325
+ "outputs": [
326
+ {
327
+ "name": "stdout",
328
+ "output_type": "stream",
329
+ "text": [
330
+ "Row 1/4...\n",
331
+ "Saved up to row 1\n",
332
+ "Row 2/4...\n",
333
+ "Saved up to row 2\n",
334
+ "Row 3/4...\n",
335
+ "Saved up to row 3\n",
336
+ "Row 4/4...\n",
337
+ "Saved up to row 4\n",
338
+ "All done! Files: /home/lauhp/000_PHD/000_010_PUBLICATION/2025_SAGE/CODE/pm-paper_uzh_gitlab/pm-paper/data/CSV/Deepseek_annotated_POI.csv /home/lauhp/000_PHD/000_010_PUBLICATION/2025_SAGE/CODE/pm-paper_uzh_gitlab/pm-paper/data/CSV/Deepseek_annotated_POI.xlsx\n"
339
+ ]
340
+ }
341
+ ],
342
+ "source": [
343
+ "import pandas as pd\n",
344
+ "import openai\n",
345
+ "import time\n",
346
+ "import os\n",
347
+ "from pathlib import Path\n",
348
+ "from openai import OpenAI # Add this import\n",
349
+ "\n",
350
+ "# === PATHS & CONFIG ===\n",
351
+ "current_dir = Path.cwd()\n",
352
+ "inputCSV = current_dir.parent / \"data/CSV/model_subsets/NER_poi_step_02.csv\"\n",
353
+ "api_key_file = current_dir.parent / \"misc/credentials/deepseek_api_key.txt\" #store your API key under misc/credentials/deepseek_api_key.txt\n",
354
+ "\n",
355
+ "# Output both CSV and Excel for compatibility\n",
356
+ "OUTPUT_CSV = current_dir.parent / \"data/CSV/Deepseek_annotated_POI.csv\"\n",
357
+ "OUTPUT_XLSX = current_dir.parent / \"data/CSV/Deepseek_annotated_POI.xlsx\"\n",
358
+ "INDEX_FILE = current_dir.parent / \"misc/deepseek_query_index.txt\"\n",
359
+ "SAVE_INTERVAL = 1 # Save every N rows\n",
360
+ "START_ROW = 1 # Row index to start from (0-based)\n",
361
+ "END_ROW = 5 # Row index to end (exclusive)\n",
362
+ "\n",
363
+ "# === LOAD API KEY & CLIENT ===\n",
364
+ "with open(api_key_file) as f:\n",
365
+ " api_key = f.read().strip()\n",
366
+ "\n",
367
+ "client = OpenAI(\n",
368
+ " api_key=api_key,\n",
369
+ " base_url=\"https://api.deepseek.com/v1\"\n",
370
+ ")\n",
371
+ "\n",
372
+ "# === LOAD DATA ===\n",
373
+ "df = pd.read_csv(inputCSV)\n",
374
+ "df = df.iloc[START_ROW:END_ROW].reset_index(drop=True)\n",
375
+ "\n",
376
+ "# === PREPARE PROMPTS ===\n",
377
+ "def create_prompt(row):\n",
378
+ " name = row['real_name'] if pd.notna(row['real_name']) else row['name']\n",
379
+ " hints = []\n",
380
+ " for col in ('likely_profession','likely_nationality','likely_country'):\n",
381
+ " if pd.notna(row.get(col, None)):\n",
382
+ " hints.append(row[col])\n",
383
+ " if not hints:\n",
384
+ " tags = [row[f'tag_{i}'] for i in range(1,8) if pd.notna(row.get(f'tag_{i}', None))]\n",
385
+ " hints.extend(tags[:3])\n",
386
+ " hint_text = \", \".join(hints[:5])\n",
387
+ " return (\n",
388
+ " f\"Given '{name}' ({hint_text}), provide:\\n\"\n",
389
+ " \"1. Full legal name (Western order if non-latin script)\\n\"\n",
390
+ " \"2. Any stage names/aliases (comma separated)\\n\"\n",
391
+ " \"3. Gender\\n\"\n",
392
+ " \"4. Top 3 most specific, factual professions (use industry-standard terms, no euphemisms)\\n\"\n",
393
+ " \"5. Primary country associated\\n\"\n",
394
+ " \"Use 'Unknown' when uncertain or you encounter a fictional character or place. \"\n",
395
+ " \"For entertainment fields, specify sub-genres when known (kpop, adult industry, etc.).\"\n",
396
+ " )\n",
397
+ "\n",
398
+ "prompts = df.apply(create_prompt, axis=1).tolist()\n",
399
+ "\n",
400
+ "# === CHECK FOR EXISTING OUTPUT ===\n",
401
+ "if os.path.exists(INDEX_FILE):\n",
402
+ " with open(INDEX_FILE, 'r') as f:\n",
403
+ " current_index = int(f.read().strip())\n",
404
+ "else:\n",
405
+ " current_index = 0\n",
406
+ "\n",
407
+ "results = []\n",
408
+ "if os.path.exists(OUTPUT_XLSX):\n",
409
+ " existing = pd.read_excel(OUTPUT_XLSX)\n",
410
+ " if {'full_name','aliases','gender','profession_llm','country'}.issubset(existing.columns):\n",
411
+ " results = existing[['full_name','aliases','gender','profession_llm','country']].values.tolist()\n",
412
+ "\n",
413
+ "# === QUERY & PARSE ===\n",
414
+ "def query_deepseek(prompt):\n",
415
+ " try:\n",
416
+ " resp = client.chat.completions.create(\n",
417
+ " model=\"deepseek-chat\",\n",
418
+ " messages=[\n",
419
+ " {\"role\":\"system\",\"content\":\"You extract key data on a person; respond with exactly 5 numbered lines.\"},\n",
420
+ " {\"role\":\"user\",\"content\":prompt}\n",
421
+ " ],\n",
422
+ " temperature=0.05, top_p=0.8\n",
423
+ " )\n",
424
+ " return resp.choices[0].message.content.strip()\n",
425
+ " except Exception as e:\n",
426
+ " print(\"API error:\", e)\n",
427
+ " return \"\"\n",
428
+ "\n",
429
+ "def parse_response(resp):\n",
430
+ " lines = [l.strip() for l in resp.split('\\n') if l.strip()]\n",
431
+ " out = [\"Unknown\"]*5\n",
432
+ " for l in lines:\n",
433
+ " if l.startswith('1.'): out[0] = l[2:].strip()\n",
434
+ " elif l.startswith('2.'): out[1] = l[2:].strip()\n",
435
+ " elif l.startswith('3.'): out[2] = l[2:].strip()\n",
436
+ " elif l.startswith('4.'): out[3] = l[2:].strip()\n",
437
+ " elif l.startswith('5.'): out[4] = l[2:].strip()\n",
438
+ " return out\n",
439
+ "\n",
440
+ "# === PROCESS & SAVE ===\n",
441
+ "for i in range(current_index, len(df)):\n",
442
+ " print(f\"Row {i+1}/{len(df)}...\")\n",
443
+ " data = parse_response(query_deepseek(prompts[i]))\n",
444
+ " if i < len(results): results[i] = data\n",
445
+ " else: results.append(data)\n",
446
+ " current_index = i+1\n",
447
+ "\n",
448
+ " if current_index % SAVE_INTERVAL == 0 or current_index == len(df):\n",
449
+ " out_df = df.iloc[:current_index].copy()\n",
450
+ " out_df[['full_name','aliases','gender','profession_llm','country']] = pd.DataFrame(results[:current_index])\n",
451
+ " # CSV\n",
452
+ " out_df.to_csv(OUTPUT_CSV, index=False)\n",
453
+ " # Excel\n",
454
+ " out_df.to_excel(OUTPUT_XLSX, index=False)\n",
455
+ " with open(INDEX_FILE, 'w') as f:\n",
456
+ " f.write(str(current_index))\n",
457
+ " print(\"Saved up to row\", current_index)\n",
458
+ " time.sleep(1)\n",
459
+ "\n",
460
+ "print(\"All done! Files:\", OUTPUT_CSV, OUTPUT_XLSX)\n"
461
+ ]
462
+ },
463
+ {
464
+ "cell_type": "markdown",
465
+ "id": "7910e574",
466
+ "metadata": {},
467
+ "source": []
468
+ },
469
+ {
470
+ "cell_type": "markdown",
471
+ "id": "9d377005",
472
+ "metadata": {},
473
+ "source": [
474
+ "# Aggregate by individual names"
475
+ ]
476
+ },
477
+ {
478
+ "cell_type": "markdown",
479
+ "id": "d2e75354",
480
+ "metadata": {},
481
+ "source": [
482
+ " ##### e.g. Emma Watson [model1, model2, model3] etc."
483
+ ]
484
+ },
485
+ {
486
+ "cell_type": "code",
487
+ "execution_count": 11,
488
+ "id": "747c3a2f",
489
+ "metadata": {},
490
+ "outputs": [],
491
+ "source": [
492
+ "import pandas as pd\n",
493
+ "import re\n",
494
+ "from pathlib import Path\n",
495
+ "current_dir = Path.cwd()\n",
496
+ "\n",
497
+ "profession_map = current_dir.parent / \"misc/lists/mapped_professions.csv\"\n",
498
+ "\n",
499
+ "poi_df = current_dir.parent / \"data/CSV/Deepseek_annotated_POI.csv\"\n",
500
+ "\n",
501
+ "output = current_dir.parent / \"data/CSV/Deepseek_annotated_POI_aggregated.csv\"\n",
502
+ "\n",
503
+ "countries_csv = current_dir.parent / \"misc/lists/countries.csv\"\n",
504
+ "countries_df = pd.read_csv(countries_csv)\n",
505
+ "\n",
506
+ "# Extract valid country names (strip whitespace)\n",
507
+ "valid_countries = set(countries_df['en_short_name'].str.strip())\n",
508
+ "\n",
509
+ "\n",
510
+ "# Load the dataset\n",
511
+ "df = pd.read_csv(poi_df) # Update path if needed\n",
512
+ "\n",
513
+ "# Step 1: Group by 'full_name' and aggregate required information\n",
514
+ "grouped_df = df.groupby('full_name').agg(\n",
515
+ " No_of_models=('id', 'count'),\n",
516
+ " modelIDs=('id', lambda x: list(x)),\n",
517
+ " combinedDownloadCount=('downloadCount', 'sum')\n",
518
+ ").reset_index()\n",
519
+ "\n",
520
+ "# Step 2: Keep representative info for each person\n",
521
+ "# Keep representative info (including aliases)\n",
522
+ "additional_columns = df.groupby('full_name').agg(\n",
523
+ " country=('country', 'first'),\n",
524
+ " profession_llm=('profession_llm', 'first'),\n",
525
+ " gender=('gender', 'first'),\n",
526
+ " aliases=('aliases', 'first') # ✅ Add this line\n",
527
+ ").reset_index()\n",
528
+ "\n",
529
+ "\n",
530
+ "\n",
531
+ "def standardize_country(country):\n",
532
+ " if not isinstance(country, str):\n",
533
+ " return \"Unknown\"\n",
534
+ "\n",
535
+ " country_clean = country.strip()\n",
536
+ " lowered = country_clean.lower()\n",
537
+ "\n",
538
+ " # Handle fictional or fantasy countries\n",
539
+ " fictional_keywords = [\"fictional\", \"westeros\", \"asgard\", \"middle-earth\", \"naboo\", \"middle earth\", \"latveria\"]\n",
540
+ " if any(keyword in lowered for keyword in fictional_keywords):\n",
541
+ " return \"Unknown\"\n",
542
+ "\n",
543
+ " # Handle known region-based adjustments\n",
544
+ " if \"macau\" in lowered:\n",
545
+ " return \"Macau\"\n",
546
+ " elif \"hong kong\" in lowered:\n",
547
+ " return \"Hong Kong\"\n",
548
+ " elif \"taiwan\" in lowered:\n",
549
+ " return \"Taiwan\"\n",
550
+ "\n",
551
+ " # Normalize complex or alternate country names\n",
552
+ " lowered = lowered.replace(\"United Kingdom of Great Britain and Northern Ireland\", \"united kingdom\")\n",
553
+ " lowered = lowered.replace(\"england\", \"united kingdom\")\n",
554
+ " lowered = lowered.replace(\"united states of america\", \"united states\")\n",
555
+ "\n",
556
+ " # Remove anything in brackets and after commas\n",
557
+ " country_clean = re.sub(r\"\\(.*?\\)\", \"\", country_clean)\n",
558
+ " country_clean = country_clean.split(',')[0].strip().lower()\n",
559
+ "\n",
560
+ " # Manual overrides\n",
561
+ " replacements = {\n",
562
+ " \"united kingdom\": \"UK\",\n",
563
+ " \"united kingdom of great britain and northern ireland\": \"UK\",\n",
564
+ " \"french southern territories\": \"Other\",\n",
565
+ " \"united states\": \"US\",\n",
566
+ " \"united states of america\": \"US\",\n",
567
+ " \"turkey\": \"Türkiye\",\n",
568
+ " \"czech republic\": \"Czechia\"\n",
569
+ " }\n",
570
+ "\n",
571
+ " if country_clean in replacements:\n",
572
+ " return replacements[country_clean]\n",
573
+ "\n",
574
+ " # Final check against valid country list (case-insensitive)\n",
575
+ " for valid in valid_countries:\n",
576
+ " if country_clean == valid.lower():\n",
577
+ " return valid\n",
578
+ "\n",
579
+ " return \"Unknown\"\n",
580
+ "\n",
581
+ "\n",
582
+ "# Updated function to fully remove anything in brackets (complete or not)\n",
583
+ "def get_profession_short(profession):\n",
584
+ " if isinstance(profession, str):\n",
585
+ " # Get first part before comma\n",
586
+ " first_prof = profession.split(',')[0].strip()\n",
587
+ " # Remove all bracketed content, even malformed\n",
588
+ " first_prof = re.sub(r\"[\\[].∗?[\\[].*?[\\]]\", \"\", first_prof) # removes properly closed\n",
589
+ " first_prof = re.sub(r\"[\\(\\[].*\", \"\", first_prof) # removes malformed\n",
590
+ " cleaned = first_prof.strip()\n",
591
+ " # Normalize 'Actress' to 'Actor'\n",
592
+ " if cleaned.lower() == \"actress\":\n",
593
+ " return \"Actor\"\n",
594
+ " return cleaned\n",
595
+ " return None\n",
596
+ "\n",
597
+ "# Load your mapping file\n",
598
+ "mapping_df = pd.read_csv(profession_map, on_bad_lines='skip') # or 'warn'\n",
599
+ "\n",
600
+ "\n",
601
+ "# Ensure the mapping columns are named correctly\n",
602
+ "# (Assuming columns are: 'profession_llm' or 'profession_short', and 'category' or 'mapped_profession')\n",
603
+ "# Adjust these as needed\n",
604
+ "mapping_df.columns = [col.strip().lower() for col in mapping_df.columns]\n",
605
+ "\n",
606
+ "# Rename for clarity and consistency\n",
607
+ "if 'profession_llm' in mapping_df.columns:\n",
608
+ " mapping_df = mapping_df.rename(columns={'profession_llm': 'profession_short'})\n",
609
+ "if 'category' in mapping_df.columns:\n",
610
+ " mapping_df = mapping_df.rename(columns={'category': 'mapped_profession'})\n",
611
+ "\n",
612
+ "# Merge the mapped profession into final_df\n",
613
+ "#final_df = final_df.merge(mapping_df[['profession_short', 'mapped_profession']], on='profession_short', how='left')\n",
614
+ "\n",
615
+ "\n",
616
+ "additional_columns = df.groupby('full_name').agg(\n",
617
+ " country=('country', 'first'),\n",
618
+ " profession_llm=('profession_llm', 'first'),\n",
619
+ " gender=('gender', 'first'),\n",
620
+ " aliases=('aliases', 'first') # <-- Added this line\n",
621
+ ").reset_index()\n",
622
+ "\n",
623
+ "\n",
624
+ "# Step 3: Merge the aggregated info with the representative info\n",
625
+ "final_df = pd.merge(grouped_df, additional_columns, on='full_name', how='left')\n",
626
+ "\n",
627
+ "# Step 4: Clean and transform columns\n",
628
+ "final_df['profession_short'] = final_df['profession_llm'].apply(get_profession_short)\n",
629
+ "final_df['country'] = final_df['country'].apply(standardize_country)\n",
630
+ "\n",
631
+ "# Step 5: Merge with profession mapping\n",
632
+ "final_df = final_df.merge(mapping_df[['profession_short', 'mapped_profession']], on='profession_short', how='left')\n",
633
+ "\n",
634
+ "# Optional: Save the result to a CSV file\n",
635
+ "final_df.to_csv(output, index=False)\n"
636
+ ]
637
+ },
638
+ {
639
+ "cell_type": "code",
640
+ "execution_count": null,
641
+ "id": "704e5246",
642
+ "metadata": {},
643
+ "outputs": [],
644
+ "source": []
645
+ }
646
+ ],
647
+ "metadata": {
648
+ "kernelspec": {
649
+ "display_name": "latm",
650
+ "language": "python",
651
+ "name": "python3"
652
+ },
653
+ "language_info": {
654
+ "codemirror_mode": {
655
+ "name": "ipython",
656
+ "version": 3
657
+ },
658
+ "file_extension": ".py",
659
+ "mimetype": "text/x-python",
660
+ "name": "python",
661
+ "nbconvert_exporter": "python",
662
+ "pygments_lexer": "ipython3",
663
+ "version": "3.10.15"
664
+ }
665
+ },
666
+ "nbformat": 4,
667
+ "nbformat_minor": 5
668
+ }
jupyter_notebooks/Section_2-3-4__Figure_8a_sunburst_gender.ipynb ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Prepare *.json for Figure 8a"
8
+ ]
9
+ },
10
+ {
11
+ "cell_type": "code",
12
+ "execution_count": 8,
13
+ "metadata": {},
14
+ "outputs": [
15
+ {
16
+ "name": "stdout",
17
+ "output_type": "stream",
18
+ "text": [
19
+ "8a.json\n"
20
+ ]
21
+ }
22
+ ],
23
+ "source": [
24
+ "import pandas as pd\n",
25
+ "from collections import defaultdict\n",
26
+ "import json\n",
27
+ "from pathlib import Path \n",
28
+ "\n",
29
+ "current_dir = Path.cwd()\n",
30
+ "sunburst_json = current_dir.parent / \"public/json/8a.json\"\n",
31
+ "\n",
32
+ "\n",
33
+ "aggregated_poi = current_dir.parent / \"data/CSV/Deepseek_annotated_POI_aggregated.csv\"\n",
34
+ "df = pd.read_csv(aggregated_poi)\n",
35
+ "# ---- Normalize Gender (group Non-binary and Unknown into 'Other') ----\n",
36
+ "def normalize_gender(g):\n",
37
+ " g = str(g).strip().lower()\n",
38
+ " if g in [\"female\", \"woman\", \"female (group)\", \"female (transgender)\", \"female (virtual persona)\", \"female (group members)\"]:\n",
39
+ " return \"Female\"\n",
40
+ " elif g in [\"male\", \"male (android)\", \"male (character)\"]:\n",
41
+ " return \"Male\"\n",
42
+ " else:\n",
43
+ " return \"Other\"\n",
44
+ "\n",
45
+ "df['gender_normalized'] = df['gender'].apply(normalize_gender)\n",
46
+ "\n",
47
+ "# ---- Step 1: Limit to top 10 countries ----\n",
48
+ "df['country_cleaned'] = df['country'].apply(lambda x: x if x not in ['Unknown', '', None] else 'Other')\n",
49
+ "top_countries = df['country_cleaned'].value_counts().nlargest(12).index.tolist()\n",
50
+ "df['country_limited'] = df['country_cleaned'].apply(lambda x: x if x in top_countries else 'Other')\n",
51
+ "\n",
52
+ "# ---- Step 2: Normalize and limit professions ----\n",
53
+ "valid_categories = [\n",
54
+ " \"Actor\", \"Adult Performer\", \"Singer, Musician\", \"Model\",\n",
55
+ " \"Online Personality\", \"TV Personality\", \"Voice Actor\",\"Public Figure\", \"Sports Professional\"\n",
56
+ "]\n",
57
+ "\n",
58
+ "def remap_profession(profession):\n",
59
+ " if profession == 'Unknown' or profession not in valid_categories:\n",
60
+ " return 'Other'\n",
61
+ " elif profession == 'Fictional Character':\n",
62
+ " return 'Actor'\n",
63
+ " elif profession == 'Voice actor':\n",
64
+ " return 'Voice Actor'\n",
65
+ " return profession\n",
66
+ "\n",
67
+ "df['profession_limited'] = df['mapped_profession'].apply(remap_profession)\n",
68
+ "\n",
69
+ "# ---- Step 3: Group by gender and profession ----\n",
70
+ "sunburst_data = df.groupby(['gender_normalized', 'profession_limited']).size().reset_index(name='count')\n",
71
+ "\n",
72
+ "# ---- Step 4: Create nested structure for D3.js ----\n",
73
+ "sunburst_dict = {\"name\": \"root\", \"children\": []}\n",
74
+ "gender_map = defaultdict(list)\n",
75
+ "\n",
76
+ "for _, row in sunburst_data.iterrows():\n",
77
+ " gender = row['gender_normalized']\n",
78
+ " profession = row['profession_limited']\n",
79
+ " count = int(row['count'])\n",
80
+ " gender_map[gender].append({\"name\": profession, \"value\": count})\n",
81
+ "\n",
82
+ "# Sort 'Other' professions to appear last\n",
83
+ "for gender, professions in gender_map.items():\n",
84
+ " professions_sorted = sorted(professions, key=lambda d: (d[\"name\"] == \"Other\", d[\"name\"]))\n",
85
+ " gender_map[gender] = professions_sorted\n",
86
+ "\n",
87
+ "# Build the final nested structure\n",
88
+ "for gender, professions in gender_map.items():\n",
89
+ " sunburst_dict[\"children\"].append({\"name\": gender, \"children\": professions})\n",
90
+ "\n",
91
+ "# ---- Step 5: Save to a JSON file ----\n",
92
+ "with open(sunburst_json, \"w\", encoding='utf-8') as f:\n",
93
+ " json.dump(sunburst_dict, f, ensure_ascii=False, indent=2)\n",
94
+ "\n",
95
+ "print(\"8a.json\")\n"
96
+ ]
97
+ },
98
+ {
99
+ "cell_type": "code",
100
+ "execution_count": null,
101
+ "metadata": {},
102
+ "outputs": [],
103
+ "source": []
104
+ }
105
+ ],
106
+ "metadata": {
107
+ "kernelspec": {
108
+ "display_name": "latm",
109
+ "language": "python",
110
+ "name": "python3"
111
+ },
112
+ "language_info": {
113
+ "codemirror_mode": {
114
+ "name": "ipython",
115
+ "version": 3
116
+ },
117
+ "file_extension": ".py",
118
+ "mimetype": "text/x-python",
119
+ "name": "python",
120
+ "nbconvert_exporter": "python",
121
+ "pygments_lexer": "ipython3",
122
+ "version": "3.10.15"
123
+ }
124
+ },
125
+ "nbformat": 4,
126
+ "nbformat_minor": 2
127
+ }
jupyter_notebooks/Section_2-3-4__Figure_8b_sunburst_profession.ipynb ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Prepare *.json for Figure 8b"
8
+ ]
9
+ },
10
+ {
11
+ "cell_type": "code",
12
+ "execution_count": 5,
13
+ "metadata": {},
14
+ "outputs": [
15
+ {
16
+ "name": "stdout",
17
+ "output_type": "stream",
18
+ "text": [
19
+ "✅ Sunburst data saved to sunburst_data.json\n"
20
+ ]
21
+ }
22
+ ],
23
+ "source": [
24
+ "import pandas as pd\n",
25
+ "from collections import defaultdict\n",
26
+ "import json\n",
27
+ "from pathlib import Path\n",
28
+ "\n",
29
+ "current_dir = Path.cwd()\n",
30
+ "\n",
31
+ "sunburst_path = current_dir.parent / \"public/json/sunburst_countries_A.json\"\n",
32
+ "\n",
33
+ "\n",
34
+ "aggregated_poi = current_dir.parent / \"data/CSV/Deepseek_annotated_POI_aggregated.csv\"\n",
35
+ "\n",
36
+ "df = pd.read_csv(aggregated_poi)\n",
37
+ "df['country_cleaned'] = df['country'].apply(lambda x: x if x not in ['Unknown', '', None] else 'Other')\n",
38
+ "\n",
39
+ "# Now get top countries excluding what was forced into 'Other'\n",
40
+ "top_countries = df['country_cleaned'].value_counts().nlargest(15).index.tolist()\n",
41
+ "\n",
42
+ "# Final limited country column\n",
43
+ "df['country_limited'] = df['country_cleaned'].apply(lambda x: x if x in top_countries else 'Other')\n",
44
+ "\n",
45
+ "# ---- Step 2: Limit to top 7 professions and combine Unknown and Sports Professional with Other ----\n",
46
+ "top_professions = df['mapped_profession'].value_counts().nlargest(7).index.tolist()\n",
47
+ "\n",
48
+ "# Explicitly remove 'Unknown' and 'Sports Professional' even if they are in the top 7\n",
49
+ "top_professions = [p for p in top_professions if p not in ['Unknown', 'Sports Professional']]\n",
50
+ "\n",
51
+ "# Normalize 'Unknown' and '\n",
52
+ "# Sports Professional' into 'Other'\n",
53
+ "# Normalize 'Unknown' and 'Sports Professional' into 'Other'\n",
54
+ "df['profession_limited'] = df['mapped_profession'].apply(\n",
55
+ " lambda x: 'Other' if x in ['Unknown', 'Sports Professional'] or x not in top_professions else x\n",
56
+ ")\n",
57
+ "\n",
58
+ "\n",
59
+ "\n",
60
+ "# ---- Step 3: Group by the limited country and profession ----\n",
61
+ "sunburst_data = df.groupby(['country_limited', 'profession_limited']).size().reset_index(name='count')\n",
62
+ "\n",
63
+ "# ---- Step 4: Create a nested structure for D3.js ----\n",
64
+ "sunburst_dict = {\"name\": \"root\", \"children\": []}\n",
65
+ "country_map = defaultdict(list)\n",
66
+ "\n",
67
+ "for _, row in sunburst_data.iterrows():\n",
68
+ " country = row['country_limited']\n",
69
+ " profession = row['profession_limited']\n",
70
+ " count = int(row['count'])\n",
71
+ " country_map[country].append({\"name\": profession, \"value\": count})\n",
72
+ "\n",
73
+ "# For each country, sort the profession list so that \"Other\" appears at the end\n",
74
+ "for country, professions in country_map.items():\n",
75
+ " professions_sorted = sorted(professions, key=lambda d: (d[\"name\"] == \"Other\", d[\"name\"]))\n",
76
+ " country_map[country] = professions_sorted\n",
77
+ "\n",
78
+ "for country, professions in country_map.items():\n",
79
+ " sunburst_dict[\"children\"].append({\"name\": country, \"children\": professions})\n",
80
+ "\n",
81
+ "# ---- Step 5: Save to a JSON file ----\n",
82
+ "with open(sunburst_path, \"w\", encoding='utf-8') as f:\n",
83
+ " json.dump(sunburst_dict, f, ensure_ascii=False, indent=2)\n",
84
+ "\n",
85
+ "\n",
86
+ "print(\"✅ Sunburst data saved to sunburst_data.json\")\n"
87
+ ]
88
+ },
89
+ {
90
+ "cell_type": "markdown",
91
+ "metadata": {},
92
+ "source": [
93
+ "the resulting *.json is the input for Figure_8.html"
94
+ ]
95
+ },
96
+ {
97
+ "cell_type": "markdown",
98
+ "metadata": {},
99
+ "source": []
100
+ }
101
+ ],
102
+ "metadata": {
103
+ "kernelspec": {
104
+ "display_name": "latm",
105
+ "language": "python",
106
+ "name": "python3"
107
+ },
108
+ "language_info": {
109
+ "codemirror_mode": {
110
+ "name": "ipython",
111
+ "version": 3
112
+ },
113
+ "file_extension": ".py",
114
+ "mimetype": "text/x-python",
115
+ "name": "python",
116
+ "nbconvert_exporter": "python",
117
+ "pygments_lexer": "ipython3",
118
+ "version": "3.10.15"
119
+ }
120
+ },
121
+ "nbformat": 4,
122
+ "nbformat_minor": 2
123
+ }
jupyter_notebooks/Section_2-3_Figure_5_co-occurence_promotional_tags.ipynb ADDED
@@ -0,0 +1,314 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Create *.json for figure 5 (Co-occurence network of Tags)"
8
+ ]
9
+ },
10
+ {
11
+ "cell_type": "code",
12
+ "execution_count": 5,
13
+ "metadata": {},
14
+ "outputs": [
15
+ {
16
+ "name": "stdout",
17
+ "output_type": "stream",
18
+ "text": [
19
+ "Processing: america\n",
20
+ " ✅ Saved to /home/lauhp/000_PHD/000_010_PUBLICATION/2025_SAGE/CODE/pm-paper_uzh_gitlab/pm-paper/public/json/tags_america.json\n"
21
+ ]
22
+ }
23
+ ],
24
+ "source": [
25
+ "import pandas as pd\n",
26
+ "from itertools import combinations\n",
27
+ "from collections import Counter, defaultdict\n",
28
+ "import json\n",
29
+ "import re\n",
30
+ "import os\n",
31
+ "\n",
32
+ "from pathlib import Path\n",
33
+ "current_dir = Path.cwd()\n",
34
+ "\n",
35
+ "# === CONFIG ===\n",
36
+ "file_path = current_dir.parent / \"data/CSV/Models/Civi_models.csv\"\n",
37
+ "output_dir = current_dir.parent / \"public/json/\"\n",
38
+ "#target_terms = [\"asian\", \"indian\", \"man\", \"woman\", \"german\", \"korean\", \"american\", \"russian\", \"style\", \"japanese\", \"chinese\"] # Add any tags you want to process\n",
39
+ "#target_terms = [\"character\", \"instagram\", \"youtuber\", \"actor\", \"actress\", \"celebrity\", \"vtuber\", \"kpop\"] # Add any tags you want to process\n",
40
+ "target_terms = [\"america\"] # Add any tags you want to process\n",
41
+ "min_connections = 1 # minimum number of link connections per node\n",
42
+ "\n",
43
+ "# === LOAD DATA ===\n",
44
+ "df = pd.read_csv(file_path)\n",
45
+ "tag_columns = [f\"tag_{i}\" for i in range(1, 8)]\n",
46
+ "df_tags = df[tag_columns]\n",
47
+ "\n",
48
+ "# === MAIN LOOP ===\n",
49
+ "for target_term in target_terms:\n",
50
+ " print(f\"Processing: {target_term}\")\n",
51
+ " \n",
52
+ " pattern = re.compile(rf'\\b{re.escape(target_term)}\\b', flags=re.IGNORECASE)\n",
53
+ " df_filtered = df_tags[df_tags.apply(\n",
54
+ " lambda row: row.astype(str).apply(lambda x: bool(pattern.search(x))).any(),\n",
55
+ " axis=1\n",
56
+ " )]\n",
57
+ "\n",
58
+ " # Skip if no data matches\n",
59
+ " if df_filtered.empty:\n",
60
+ " print(f\" ⚠️ No matches for '{target_term}', skipping.\")\n",
61
+ " continue\n",
62
+ "\n",
63
+ " # === COUNT INDIVIDUAL TAGS ===\n",
64
+ " all_tags = df_filtered.values.flatten()\n",
65
+ " all_tags = [tag for tag in all_tags if pd.notna(tag)]\n",
66
+ " tag_counts = Counter(all_tags)\n",
67
+ "\n",
68
+ " # === CO-OCCURRENCE ===\n",
69
+ " co_occurrences = defaultdict(int)\n",
70
+ " for tags in df_filtered.itertuples(index=False, name=None):\n",
71
+ " tags = [tag for tag in tags if pd.notna(tag)]\n",
72
+ " for tag1, tag2 in combinations(tags, 2):\n",
73
+ " co_occurrences[frozenset([tag1, tag2])] += 1\n",
74
+ "\n",
75
+ " edges = [(list(pair)[0], list(pair)[1], weight) for pair, weight in co_occurrences.items()]\n",
76
+ "\n",
77
+ " # === FILTER BY CONNECTIONS ===\n",
78
+ " connected_tags = Counter()\n",
79
+ " for tag1, tag2, _ in edges:\n",
80
+ " connected_tags[tag1] += 1\n",
81
+ " connected_tags[tag2] += 1\n",
82
+ "\n",
83
+ " nodes = [{\"id\": tag, \"size\": tag_counts[tag]} for tag in tag_counts if connected_tags[tag] >= min_connections]\n",
84
+ " valid_ids = set(node[\"id\"] for node in nodes)\n",
85
+ " links = [{\"source\": tag1, \"target\": tag2, \"value\": weight}\n",
86
+ " for tag1, tag2, weight in edges\n",
87
+ " if tag1 in valid_ids and tag2 in valid_ids]\n",
88
+ "\n",
89
+ " if not nodes or not links:\n",
90
+ " print(f\" ⚠️ Not enough connections for '{target_term}', skipping.\")\n",
91
+ " continue\n",
92
+ "\n",
93
+ " # === EXPORT ===\n",
94
+ " d3_data = {\"nodes\": nodes, \"links\": links}\n",
95
+ " safe_term = re.sub(r'\\W+', '_', target_term.lower())\n",
96
+ " output_file = os.path.join(output_dir, f\"tags_{safe_term}.json\")\n",
97
+ " \n",
98
+ " with open(output_file, \"w\") as f:\n",
99
+ " json.dump(d3_data, f, indent=4)\n",
100
+ " \n",
101
+ " print(f\" ✅ Saved to {output_file}\")\n"
102
+ ]
103
+ },
104
+ {
105
+ "cell_type": "markdown",
106
+ "metadata": {},
107
+ "source": [
108
+ "## Different Countries"
109
+ ]
110
+ },
111
+ {
112
+ "cell_type": "code",
113
+ "execution_count": 2,
114
+ "metadata": {},
115
+ "outputs": [
116
+ {
117
+ "name": "stderr",
118
+ "output_type": "stream",
119
+ "text": [
120
+ "/tmp/ipykernel_68582/2797381217.py:15: DtypeWarning: Columns (50,51,54,55,56,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,458) have mixed types. Specify dtype option on import or set low_memory=False.\n",
121
+ " df = pd.read_csv(file_path)\n"
122
+ ]
123
+ },
124
+ {
125
+ "name": "stdout",
126
+ "output_type": "stream",
127
+ "text": [
128
+ "Processing: united states\n",
129
+ " ⚠️ No matches for 'united states', skipping.\n",
130
+ "Processing: korea\n",
131
+ " ✅ Saved to data/json/tags_korea_poi.json\n",
132
+ "Processing: uk\n",
133
+ " ✅ Saved to data/json/tags_uk_poi.json\n",
134
+ "Processing: russia\n",
135
+ " ✅ Saved to data/json/tags_russia_poi.json\n",
136
+ "Processing: china\n",
137
+ " ✅ Saved to data/json/tags_china_poi.json\n",
138
+ "Processing: canada\n",
139
+ " ✅ Saved to data/json/tags_canada_poi.json\n",
140
+ "Processing: India\n",
141
+ " ✅ Saved to data/json/tags_india_poi.json\n",
142
+ "Processing: germany\n",
143
+ " ✅ Saved to data/json/tags_germany_poi.json\n"
144
+ ]
145
+ }
146
+ ],
147
+ "source": [
148
+ "import pandas as pd\n",
149
+ "from itertools import combinations\n",
150
+ "from collections import Counter, defaultdict\n",
151
+ "import json\n",
152
+ "import re\n",
153
+ "import os\n",
154
+ "\n",
155
+ "# === CONFIG ===\n",
156
+ "file_path = \"data/model_subsets/all_models_poi_true.csv\"\n",
157
+ "output_dir = \"data/json/\"\n",
158
+ "target_terms = [\"united states\", \"korea\", \"uk\", \"russia\", \"china\", \"canada\", \"India\", \"germany\"] # Add any tags you want to process\n",
159
+ "min_connections = 1 # minimum number of link connections per node\n",
160
+ "\n",
161
+ "# === LOAD DATA ===\n",
162
+ "df = pd.read_csv(file_path)\n",
163
+ "tag_columns = [f\"tag_{i}\" for i in range(1, 8)]\n",
164
+ "df_tags = df[tag_columns]\n",
165
+ "\n",
166
+ "# === MAIN LOOP ===\n",
167
+ "for target_term in target_terms:\n",
168
+ " print(f\"Processing: {target_term}\")\n",
169
+ " \n",
170
+ " pattern = re.compile(rf'\\b{re.escape(target_term)}\\b', flags=re.IGNORECASE)\n",
171
+ " df_filtered = df_tags[df_tags.apply(\n",
172
+ " lambda row: row.astype(str).apply(lambda x: bool(pattern.search(x))).any(),\n",
173
+ " axis=1\n",
174
+ " )]\n",
175
+ "\n",
176
+ " # Skip if no data matches\n",
177
+ " if df_filtered.empty:\n",
178
+ " print(f\" ⚠️ No matches for '{target_term}', skipping.\")\n",
179
+ " continue\n",
180
+ "\n",
181
+ " # === COUNT INDIVIDUAL TAGS ===\n",
182
+ " all_tags = df_filtered.values.flatten()\n",
183
+ " all_tags = [tag for tag in all_tags if pd.notna(tag)]\n",
184
+ " tag_counts = Counter(all_tags)\n",
185
+ "\n",
186
+ " # === CO-OCCURRENCE ===\n",
187
+ " co_occurrences = defaultdict(int)\n",
188
+ " for tags in df_filtered.itertuples(index=False, name=None):\n",
189
+ " tags = [tag for tag in tags if pd.notna(tag)]\n",
190
+ " for tag1, tag2 in combinations(tags, 2):\n",
191
+ " co_occurrences[frozenset([tag1, tag2])] += 1\n",
192
+ "\n",
193
+ " edges = [(list(pair)[0], list(pair)[1], weight) for pair, weight in co_occurrences.items()]\n",
194
+ "\n",
195
+ " # === FILTER BY CONNECTIONS ===\n",
196
+ " connected_tags = Counter()\n",
197
+ " for tag1, tag2, _ in edges:\n",
198
+ " connected_tags[tag1] += 1\n",
199
+ " connected_tags[tag2] += 1\n",
200
+ "\n",
201
+ " nodes = [{\"id\": tag, \"size\": tag_counts[tag]} for tag in tag_counts if connected_tags[tag] >= min_connections]\n",
202
+ " valid_ids = set(node[\"id\"] for node in nodes)\n",
203
+ " links = [{\"source\": tag1, \"target\": tag2, \"value\": weight}\n",
204
+ " for tag1, tag2, weight in edges\n",
205
+ " if tag1 in valid_ids and tag2 in valid_ids]\n",
206
+ "\n",
207
+ " if not nodes or not links:\n",
208
+ " print(f\" ⚠️ Not enough connections for '{target_term}', skipping.\")\n",
209
+ " continue\n",
210
+ "\n",
211
+ " # === EXPORT ===\n",
212
+ " d3_data = {\"nodes\": nodes, \"links\": links}\n",
213
+ " safe_term = re.sub(r'\\W+', '_', target_term.lower())\n",
214
+ " output_file = os.path.join(output_dir, f\"tags_{safe_term}_poi.json\")\n",
215
+ " \n",
216
+ " with open(output_file, \"w\") as f:\n",
217
+ " json.dump(d3_data, f, indent=4)\n",
218
+ " \n",
219
+ " print(f\" ✅ Saved to {output_file}\")\n"
220
+ ]
221
+ },
222
+ {
223
+ "cell_type": "code",
224
+ "execution_count": 5,
225
+ "metadata": {},
226
+ "outputs": [
227
+ {
228
+ "name": "stderr",
229
+ "output_type": "stream",
230
+ "text": [
231
+ "/tmp/ipykernel_79893/1420003123.py:13: DtypeWarning: Columns (50,51,54,55,56,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,458) have mixed types. Specify dtype option on import or set low_memory=False.\n",
232
+ " df = pd.read_csv(file_path)\n"
233
+ ]
234
+ },
235
+ {
236
+ "name": "stdout",
237
+ "output_type": "stream",
238
+ "text": [
239
+ "✅ Exported 60330 nodes and 16921 links to public/json/nodes_all.json\n"
240
+ ]
241
+ }
242
+ ],
243
+ "source": [
244
+ "import pandas as pd\n",
245
+ "from itertools import combinations\n",
246
+ "from collections import Counter, defaultdict\n",
247
+ "import json\n",
248
+ "import os\n",
249
+ "\n",
250
+ "# === CONFIG ===\n",
251
+ "file_path = \"data/model_subsets/all_models_poi_false.csv\"\n",
252
+ "output_file = \"public/json/nodes_all.json\"\n",
253
+ "min_link_threshold = 10 # Only keep edges with co-occurrence >= this\n",
254
+ "\n",
255
+ "# === LOAD DATA ===\n",
256
+ "df = pd.read_csv(file_path)\n",
257
+ "tag_columns = [f\"tag_{i}\" for i in range(1, 8)]\n",
258
+ "df_tags = df[tag_columns]\n",
259
+ "\n",
260
+ "# === COUNT INDIVIDUAL TAGS ===\n",
261
+ "all_tags = df_tags.values.flatten()\n",
262
+ "all_tags = [tag for tag in all_tags if pd.notna(tag)]\n",
263
+ "tag_counts = Counter(all_tags)\n",
264
+ "\n",
265
+ "# === CO-OCCURRENCE ===\n",
266
+ "co_occurrences = defaultdict(int)\n",
267
+ "for tags in df_tags.itertuples(index=False, name=None):\n",
268
+ " tags = [tag for tag in tags if pd.notna(tag)]\n",
269
+ " for tag1, tag2 in combinations(tags, 2):\n",
270
+ " co_occurrences[frozenset([tag1, tag2])] += 1\n",
271
+ "\n",
272
+ "# === Build Edges (Filtered by co-occurrence threshold)\n",
273
+ "edges = [\n",
274
+ " {\"source\": list(pair)[0], \"target\": list(pair)[1], \"value\": weight}\n",
275
+ " for pair, weight in co_occurrences.items()\n",
276
+ " if weight >= min_link_threshold\n",
277
+ "]\n",
278
+ "\n",
279
+ "# === Build Nodes (All tags that appear, regardless of links)\n",
280
+ "nodes = [{\"id\": tag, \"size\": tag_counts[tag]} for tag in tag_counts]\n",
281
+ "\n",
282
+ "# === EXPORT ===\n",
283
+ "d3_data = {\"nodes\": nodes, \"links\": edges}\n",
284
+ "\n",
285
+ "os.makedirs(os.path.dirname(output_file), exist_ok=True)\n",
286
+ "with open(output_file, \"w\") as f:\n",
287
+ " json.dump(d3_data, f, indent=4)\n",
288
+ "\n",
289
+ "print(f\"✅ Exported {len(nodes)} nodes and {len(edges)} links to {output_file}\")\n"
290
+ ]
291
+ }
292
+ ],
293
+ "metadata": {
294
+ "kernelspec": {
295
+ "display_name": "latm",
296
+ "language": "python",
297
+ "name": "python3"
298
+ },
299
+ "language_info": {
300
+ "codemirror_mode": {
301
+ "name": "ipython",
302
+ "version": 3
303
+ },
304
+ "file_extension": ".py",
305
+ "mimetype": "text/x-python",
306
+ "name": "python",
307
+ "nbconvert_exporter": "python",
308
+ "pygments_lexer": "ipython3",
309
+ "version": "3.10.15"
310
+ }
311
+ },
312
+ "nbformat": 4,
313
+ "nbformat_minor": 2
314
+ }
jupyter_notebooks/Section_2-4_Figure_9_Training_tags_Sankey.ipynb ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Sankey Diagram from model dataset"
8
+ ]
9
+ },
10
+ {
11
+ "cell_type": "code",
12
+ "execution_count": null,
13
+ "metadata": {},
14
+ "outputs": [],
15
+ "source": [
16
+ "import pandas as pd\n",
17
+ "from pathlib import Path\n",
18
+ "current_dir = Path.cwd()"
19
+ ]
20
+ },
21
+ {
22
+ "cell_type": "code",
23
+ "execution_count": null,
24
+ "metadata": {},
25
+ "outputs": [
26
+ {
27
+ "name": "stderr",
28
+ "output_type": "stream",
29
+ "text": [
30
+ "/tmp/ipykernel_9667/181225218.py:8: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`\n",
31
+ " df['has_tags'] = df['has_tags'].fillna(False)\n",
32
+ "/tmp/ipykernel_9667/181225218.py:9: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`\n",
33
+ " df['has_sex_tags'] = df['has_sex_tags'].fillna(False)\n"
34
+ ]
35
+ },
36
+ {
37
+ "name": "stdout",
38
+ "output_type": "stream",
39
+ "text": [
40
+ "🟢 All Adapters with Tags → Explicit: 4970\n",
41
+ "🔵 All Adapters with Tags → Non-Explicit: 6181\n",
42
+ "✅ Sankey structure saved to sankey_tags_focus_output.csv\n"
43
+ ]
44
+ }
45
+ ],
46
+ "source": [
47
+ "import pandas as pd\n",
48
+ "# Load your dataset\n",
49
+ "df = pd.read_csv(\"data/CSV/all_models_with_tags.csv\", nrows=40000)\n",
50
+ "\n",
51
+ "\n",
52
+ "# Fill missing values\n",
53
+ "df['has_tags'] = df['has_tags'].fillna(False)\n",
54
+ "df['has_sex_tags'] = df['has_sex_tags'].fillna(False)\n",
55
+ "df['poi'] = df['poi'].fillna(False)\n",
56
+ "\n",
57
+ "# Normalize and map types\n",
58
+ "df['type_normalized'] = df['type'].str.lower().str.strip()\n",
59
+ "type_mapping = {\n",
60
+ " 'checkpoint': 'Checkpoint',\n",
61
+ " 'lora': 'LoRA',\n",
62
+ " 'locon': 'LOCON',\n",
63
+ " 'textualinversion': 'TextualInversion',\n",
64
+ " 'controlnet': 'Other',\n",
65
+ " 'vae': 'Other',\n",
66
+ " 'upscaler': 'Other',\n",
67
+ " 'poses': 'Other',\n",
68
+ " 'workflows': 'Other',\n",
69
+ " 'other': 'Other'\n",
70
+ "}\n",
71
+ "df['type_mapped'] = df['type_normalized'].map(type_mapping).fillna('Other')\n",
72
+ "\n",
73
+ "\n",
74
+ "\n",
75
+ "\n",
76
+ "\n",
77
+ "# Identify Adapters\n",
78
+ "adapter_types = ['LoRA', 'LOCON', 'TextualInversion', 'Controlnet']\n",
79
+ "df['is_adapter'] = df['type_mapped'].isin(adapter_types)\n",
80
+ "adapter_df = df[df['is_adapter']]\n",
81
+ "\n",
82
+ "# Build Sankey links\n",
83
+ "sankey_links = []\n",
84
+ "\n",
85
+ "# Total → each model type\n",
86
+ "for t in df['type_mapped'].unique():\n",
87
+ " count = df[df['type_mapped'] == t].shape[0]\n",
88
+ " sankey_links.append({'source': 'Total', 'target': t, 'value': count})\n",
89
+ "\n",
90
+ "# Adapter types → Adapters\n",
91
+ "for t in adapter_types:\n",
92
+ " count = df[df['type_mapped'] == t].shape[0]\n",
93
+ " if count > 0:\n",
94
+ " sankey_links.append({'source': t, 'target': 'Adapters', 'value': count})\n",
95
+ "# Redefine adapter_types to only include those relevant for POI analysis\n",
96
+ "adapter_types = ['LoRA', 'LOCON', 'TextualInversion']\n",
97
+ "df['is_adapter'] = df['type_mapped'].isin(adapter_types)\n",
98
+ "adapter_df = df[df['is_adapter']]\n",
99
+ "\n",
100
+ "# Adapters → has_tags / has_no_tags\n",
101
+ "tagged_df = adapter_df[adapter_df['has_tags'] == True]\n",
102
+ "no_tag_df = adapter_df[adapter_df['has_tags'] == False]\n",
103
+ "\n",
104
+ "# Add has_no_tags count\n",
105
+ "no_tag_count = no_tag_df.shape[0]\n",
106
+ "if no_tag_count > 0:\n",
107
+ " sankey_links.append({'source': 'Adapters', 'target': 'has_no_tags', 'value': no_tag_count})\n",
108
+ "\n",
109
+ "# Now subdivide has_tags group\n",
110
+ "has_tag_count = tagged_df.shape[0]\n",
111
+ "if has_tag_count > 0:\n",
112
+ " sankey_links.append({'source': 'Adapters', 'target': 'has_tags', 'value': has_tag_count})\n",
113
+ "\n",
114
+ " for poi_val in [True, False]:\n",
115
+ " poi_label = f'has_tags + POI {\"True\" if poi_val else \"False\"}'\n",
116
+ " subset = tagged_df[tagged_df['poi'] == poi_val]\n",
117
+ " poi_count = subset.shape[0] # ✅ Fix: missing in original code\n",
118
+ "\n",
119
+ " if poi_count > 0:\n",
120
+ " sankey_links.append({'source': 'has_tags', 'target': poi_label, 'value': poi_count})\n",
121
+ "\n",
122
+ " for explicit in [True, False]:\n",
123
+ " explicit_label = f'{poi_label} + {\"explicit\" if explicit else \"non-explicit\"}'\n",
124
+ " exp_count = subset[subset['has_sex_tags'] == explicit].shape[0]\n",
125
+ " if exp_count > 0:\n",
126
+ " sankey_links.append({'source': poi_label, 'target': explicit_label, 'value': exp_count})\n",
127
+ "\n",
128
+ "# --- New Section: Count Models (Rows) Containing Specific Tags ---\n",
129
+ "\n",
130
+ "# --- Output total explicit and non-explicit from all adapters with tags ---\n",
131
+ "\n",
132
+ "explicit_total = tagged_df[tagged_df['has_sex_tags'] == True].shape[0]\n",
133
+ "non_explicit_total = tagged_df[tagged_df['has_sex_tags'] == False].shape[0]\n",
134
+ "\n",
135
+ "# Add to sankey_links (optional)\n",
136
+ "if explicit_total > 0:\n",
137
+ " sankey_links.append({'source': 'has_tags', 'target': 'All Adapters has_tags + explicit', 'value': explicit_total})\n",
138
+ "\n",
139
+ "if non_explicit_total > 0:\n",
140
+ " sankey_links.append({'source': 'has_tags', 'target': 'All Adapters has_tags + non-explicit', 'value': non_explicit_total})\n",
141
+ "\n",
142
+ "# Also print them clearly\n",
143
+ "print(f\"🟢 All Adapters with Tags → Explicit: {explicit_total}\")\n",
144
+ "print(f\"🔵 All Adapters with Tags → Non-Explicit: {non_explicit_total}\")\n",
145
+ "\n",
146
+ "\n",
147
+ "# Define target tags to check\n",
148
+ "target_tags = ['rape', 'loli', 'shota', 'lolicon']\n",
149
+ "\n",
150
+ "# Identify all tag columns\n",
151
+ "tag_columns = [col for col in df.columns if col.startswith('tag') and col[3:].isdigit()]\n",
152
+ "\n",
153
+ "# Lowercase tag values for matching\n",
154
+ "df_tags_lower = df[tag_columns].astype(str).apply(lambda col: col.str.lower().str.strip())\n",
155
+ "\n",
156
+ "# For each target tag, check how many rows contain it\n",
157
+ "for tag in target_tags:\n",
158
+ " contains_tag = df_tags_lower.isin([tag])\n",
159
+ " \n",
160
+ " # Only count rows that are both explicit and contain the tag\n",
161
+ " matching_rows = df[(df['has_sex_tags'] == True) & contains_tag.any(axis=1)]\n",
162
+ " rows_with_tag = matching_rows.shape[0]\n",
163
+ " \n",
164
+ " if rows_with_tag > 0:\n",
165
+ " sankey_links.append({'source': 'has_tags', 'target': f'has_tag_{tag}', 'value': int(rows_with_tag)})\n",
166
+ "\n",
167
+ "\n",
168
+ "# Save to CSV\n",
169
+ "sankey_df = pd.DataFrame(sankey_links)\n",
170
+ "sankey_df.to_csv(\"data/CSV/sankey_data.csv\", index=False)\n",
171
+ "print(\"✅ Sankey structure saved to sankey_tags_focus_output.csv\")\n"
172
+ ]
173
+ },
174
+ {
175
+ "cell_type": "code",
176
+ "execution_count": null,
177
+ "metadata": {},
178
+ "outputs": [],
179
+ "source": []
180
+ }
181
+ ],
182
+ "metadata": {
183
+ "kernelspec": {
184
+ "display_name": "latm",
185
+ "language": "python",
186
+ "name": "python3"
187
+ },
188
+ "language_info": {
189
+ "codemirror_mode": {
190
+ "name": "ipython",
191
+ "version": 3
192
+ },
193
+ "file_extension": ".py",
194
+ "mimetype": "text/x-python",
195
+ "name": "python",
196
+ "nbconvert_exporter": "python",
197
+ "pygments_lexer": "ipython3",
198
+ "version": "3.10.15"
199
+ }
200
+ },
201
+ "nbformat": 4,
202
+ "nbformat_minor": 2
203
+ }
jupyter_notebooks/Section_2-4_Figure_9_ectract_LoRA_metadata_v2.ipynb ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "f36422c8",
6
+ "metadata": {},
7
+ "source": [
8
+ "# LoRA metadata"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "raw",
13
+ "id": "8a2feb6e",
14
+ "metadata": {},
15
+ "source": [
16
+ "LoRA Metadata Processing Workflow\n",
17
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
18
+ "│ Load CSV File │ --> │ Read adapter metadata CSV file. │\n",
19
+ "│ Read Model Versions │ │ Extract model version IDs and relevant data. │\n",
20
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
21
+ " ↓\n",
22
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
23
+ "│ Download Adapter │ --> │ Use stored download URLs to fetch adapter files │\n",
24
+ "│ Files Using API │ │ using rotating API keys. │\n",
25
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
26
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
27
+ "│ Parse Metadata │ --> │ Extract safetensors metadata, such as training │\n",
28
+ "│ from SafeTensor │ │ images, model type, and architecture. │\n",
29
+ "│ Files │ │ │\n",
30
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
31
+ " ↓\n",
32
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
33
+ "│ Store Parsed │ --> │ Save extracted metadata into structured JSON │\n",
34
+ "│ Metadata as JSON │ │ files for later analysis. │\n",
35
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
36
+ "\n",
37
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
38
+ "│ Process JSON Files │ --> │ Read saved JSON metadata, extract relevant │\n",
39
+ "│ for Consolidation │ │ details, and filter necessary attributes. │\n",
40
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
41
+ " ↓\n",
42
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
43
+ "│ Extract Training │ --> │ Identify most frequent training tags, architectures│\n",
44
+ "│ Tags & Model Info │ │ and systems used for model creation. │\n",
45
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
46
+ " ↓\n",
47
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
48
+ "│ Save Consolidated │ --> │ Store all processed metadata in a structured CSV │\n",
49
+ "│ Metadata to CSV │ │ format for final analysis. ���\n",
50
+ "└──────────────────────┘ └───────────────────────────────────────────────────┘\n"
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": null,
56
+ "id": "efc9939d",
57
+ "metadata": {},
58
+ "outputs": [],
59
+ "source": [
60
+ "import os\n",
61
+ "import re\n",
62
+ "import json\n",
63
+ "import csv\n",
64
+ "import struct\n",
65
+ "import requests\n",
66
+ "from pathlib import Path\n",
67
+ "import pandas as pd\n",
68
+ "from collections import Counter\n",
69
+ "from concurrent.futures import ProcessPoolExecutor\n",
70
+ "from pathlib import Path\n",
71
+ "import matplotlib.pyplot as plt\n",
72
+ "from matplotlib.font_manager import FontProperties\n",
73
+ "from matplotlib import font_manager\n",
74
+ "import pandas as pd\n",
75
+ "from collections import Counter\n",
76
+ "from concurrent.futures import ProcessPoolExecutor\n",
77
+ "\n",
78
+ "# Define the current directory and important file paths\n",
79
+ "current_dir = Path.cwd()\n",
80
+ "\n",
81
+ "# Define frequently used directories\n",
82
+ "\n",
83
+ "data_dir = current_dir.parent / 'data/csv/adapters.csv'\n",
84
+ "fonts_dir = current_dir.parent / 'misc/assets/fonts'\n",
85
+ "plots_dir = current_dir.parent / 'results/plots'\n",
86
+ "raw_data_dir = current_dir.parent / 'data/adapter_metadata/lora' ### location of the LoRA metadata (JSON)\n",
87
+ "temp_dir = current_dir.parent / 'data/raw/adapters_safetensors'\n",
88
+ "misc_dir = current_dir.parent / 'misc'\n",
89
+ "\n",
90
+ "# File paths\n",
91
+ "adapters_csv = current_dir.parent / 'data/csv/adapters.csv'\n",
92
+ "output_json_dir = raw_data_dir\n",
93
+ "api_keys_file = misc_dir / 'credentials/civit.txt'\n",
94
+ "\n",
95
+ "# Ensure directories exist\n",
96
+ "os.makedirs(output_json_dir, exist_ok=True)\n",
97
+ "os.makedirs(temp_dir, exist_ok=True)\n",
98
+ "\n",
99
+ "\n",
100
+ "# Load fonts into Matplotlib\n",
101
+ "for font_path in font_paths:\n",
102
+ " font_manager.fontManager.addfont(font_path)\n",
103
+ "\n",
104
+ "# Set default font family for plots\n",
105
+ "plt.rcParams['font.family'] = ['Noto Sans JP', 'Noto Sans SC', 'sans-serif']\n",
106
+ "\n",
107
+ "print('Paths and fonts initialized successfully.')\n",
108
+ "\n",
109
+ "print('Paths initialized successfully.')"
110
+ ]
111
+ },
112
+ {
113
+ "cell_type": "markdown",
114
+ "id": "87a58593",
115
+ "metadata": {},
116
+ "source": [
117
+ "## Step 2: Download LoRA and extract *.safetensors metadata\n",
118
+ "This script downloads LoRA adapters from the filtered Civiverse-Models dataset and extracts the metadata found within the *.safetensors' data structure"
119
+ ]
120
+ },
121
+ {
122
+ "cell_type": "code",
123
+ "execution_count": null,
124
+ "id": "abd3a0bc",
125
+ "metadata": {},
126
+ "outputs": [],
127
+ "source": [
128
+ "import os\n",
129
+ "import sys\n",
130
+ "import csv\n",
131
+ "import json\n",
132
+ "import struct\n",
133
+ "import time\n",
134
+ "import requests\n",
135
+ "import signal\n",
136
+ "import contextlib\n",
137
+ "from pathlib import Path\n",
138
+ "import re\n",
139
+ "\n",
140
+ "# === Paste your API keys here ===\n",
141
+ "API_KEYS = [\n",
142
+ " \"REDACTED-CIVITAI-API-KEY-1\", #DISCORD\n",
143
+ " \"REDACTED-CIVITAI-API-KEY-2\", #ASDD 1\n",
144
+ " \"REDACTED-CIVITAI-API-KEY-3\", #ASDD 2\n",
145
+ " \"REDACTED-CIVITAI-API-KEY-4\", #BSDD \n",
146
+ " \"REDACTED-CIVITAI-API-KEY-5\"\n",
147
+ "]\n",
148
+ "if not API_KEYS or any(not isinstance(k, str) or not k.strip() for k in API_KEYS):\n",
149
+ " raise ValueError(\"Please paste at least one valid API key into API_KEYS.\")\n",
150
+ "\n",
151
+ "# === Config (adjust paths as needed) ===\n",
152
+ "current_dir = Path.cwd()\n",
153
+ "output_json_dir = current_dir.parent / \"data/adapter_metadata/lora\" # where JSON outputs go\n",
154
+ "temp_dir = current_dir.parent / \"data/raw/adapters_safetensors\" # where downloads go\n",
155
+ "csv_path = current_dir.parent / \"data/csv/adapters_poi_false_sfw.csv\"\n",
156
+ "\n",
157
+ "os.makedirs(output_json_dir, exist_ok=True)\n",
158
+ "os.makedirs(temp_dir, exist_ok=True)\n",
159
+ "\n",
160
+ "# === API key state ===\n",
161
+ "current_key_index = 0\n",
162
+ "\n",
163
+ "\n",
164
+ "\n",
165
+ "def safe_filename(name: str, max_length: int = 100) -> str:\n",
166
+ " # Replace unsafe chars\n",
167
+ " sanitized = re.sub(r'[^a-zA-Z0-9_\\-]', '_', name)\n",
168
+ " # Truncate if too long\n",
169
+ " if len(sanitized) > max_length:\n",
170
+ " sanitized = sanitized[:max_length]\n",
171
+ " return sanitized\n",
172
+ "\n",
173
+ "\n",
174
+ "def get_headers():\n",
175
+ " global current_key_index\n",
176
+ " return {\n",
177
+ " \"Accept\": \"application/json\",\n",
178
+ " \"Authorization\": f\"Bearer {API_KEYS[current_key_index].strip()}\"\n",
179
+ " }\n",
180
+ "\n",
181
+ "def rotate_api_key():\n",
182
+ " global current_key_index\n",
183
+ " if current_key_index < len(API_KEYS) - 1:\n",
184
+ " current_key_index += 1\n",
185
+ " print(f\"🔁 Rotated to API key #{current_key_index + 1}\")\n",
186
+ " else:\n",
187
+ " raise Exception(\"All API keys have been exhausted.\")\n",
188
+ "\n",
189
+ "# === Utilities ===\n",
190
+ "def save_json(data, filename):\n",
191
+ " with open(filename, 'w', encoding=\"utf-8\") as f:\n",
192
+ " json.dump(data, f, indent=4, ensure_ascii=False)\n",
193
+ "\n",
194
+ "def parse_safetensors(file_path):\n",
195
+ " # Minimal, tolerant metadata reader; returns {} on failure.\n",
196
+ " try:\n",
197
+ " with open(file_path, 'rb') as f:\n",
198
+ " file_data = f.read()\n",
199
+ " # Many safetensors use 8-byte header length; this code follows your original logic\n",
200
+ " # (4-byte) but keeps the 8-byte skip. Keep if it's working in your dataset.\n",
201
+ " metadata_size = struct.unpack('<I', file_data[:4])[0]\n",
202
+ " metadata_bytes = file_data[8:8 + metadata_size]\n",
203
+ " metadata_str = metadata_bytes.decode('utf-8', errors='replace')\n",
204
+ " metadata = json.loads(metadata_str)\n",
205
+ " return metadata.get('__metadata__', {})\n",
206
+ " except Exception as e:\n",
207
+ " print(f\"Error parsing safetensors file: {e}\")\n",
208
+ " return {}\n",
209
+ "\n",
210
+ "# === Timeout context ===\n",
211
+ "class TimeoutException(Exception):\n",
212
+ " pass\n",
213
+ "\n",
214
+ "@contextlib.contextmanager\n",
215
+ "def time_limit(seconds):\n",
216
+ " def signal_handler(signum, frame):\n",
217
+ " raise TimeoutException(f\"Timed out after {seconds} seconds\")\n",
218
+ " # Note: SIGALRM works on Unix-like OS; on Windows this will be a no-op.\n",
219
+ " try:\n",
220
+ " signal.signal(signal.SIGALRM, signal_handler)\n",
221
+ " signal.alarm(seconds)\n",
222
+ " except Exception:\n",
223
+ " # Fallback: no hard alarm on non-Unix systems\n",
224
+ " pass\n",
225
+ " try:\n",
226
+ " yield\n",
227
+ " finally:\n",
228
+ " try:\n",
229
+ " signal.alarm(0)\n",
230
+ " except Exception:\n",
231
+ " pass\n",
232
+ "\n",
233
+ "# === Download with timeout, retries, backoff, and key rotation ===\n",
234
+ "def download_file(url, output_folder, timeout=30, overall_timeout=120, max_retries=3):\n",
235
+ " filename = url.split(\"/\")[-1]\n",
236
+ " output_path = os.path.join(output_folder, filename)\n",
237
+ "\n",
238
+ " global current_key_index\n",
239
+ " retries = 0\n",
240
+ " backoff = 2\n",
241
+ "\n",
242
+ " while current_key_index < len(API_KEYS):\n",
243
+ " try:\n",
244
+ " with time_limit(overall_timeout): # global cap per download\n",
245
+ " #print(f\"➡️ GET {url} using key #{current_key_index + 1}\")\n",
246
+ " resp = requests.get(\n",
247
+ " url,\n",
248
+ " headers=get_headers(),\n",
249
+ " stream=True,\n",
250
+ " timeout=(10, timeout), # (connect timeout, per-chunk read timeout)\n",
251
+ " )\n",
252
+ "\n",
253
+ " # Auth errors → rotate key\n",
254
+ " if resp.status_code in (401, 403):\n",
255
+ " print(f\"❌ Auth {resp.status_code} with key #{current_key_index + 1}. Rotating.\")\n",
256
+ " rotate_api_key()\n",
257
+ " retries = 0\n",
258
+ " backoff = 2\n",
259
+ " continue\n",
260
+ "\n",
261
+ " # Not found → bubble up as FileNotFoundError (do not rotate)\n",
262
+ " if resp.status_code == 404:\n",
263
+ " raise FileNotFoundError(f\"Model not found at {url}\")\n",
264
+ "\n",
265
+ " # Rate limit → either rotate or wait/backoff\n",
266
+ " if resp.status_code == 429:\n",
267
+ " print(\"⏳ Rate limited (429).\", end=\" \")\n",
268
+ " if current_key_index < len(API_KEYS) - 1:\n",
269
+ " print(\"Rotating key.\")\n",
270
+ " rotate_api_key()\n",
271
+ " retries = 0\n",
272
+ " backoff = 2\n",
273
+ " continue\n",
274
+ " else:\n",
275
+ " print(f\"Waiting {backoff}s (no other keys).\")\n",
276
+ " time.sleep(backoff)\n",
277
+ " backoff = min(backoff * 2, 60)\n",
278
+ " continue\n",
279
+ "\n",
280
+ " # Other HTTP errors → raise to RequestException path\n",
281
+ " resp.raise_for_status()\n",
282
+ "\n",
283
+ " # Save file\n",
284
+ " with open(output_path, 'wb') as fh:\n",
285
+ " for chunk in resp.iter_content(chunk_size=8192):\n",
286
+ " if chunk:\n",
287
+ " fh.write(chunk)\n",
288
+ "\n",
289
+ " return output_path, filename\n",
290
+ "\n",
291
+ " except TimeoutException as e:\n",
292
+ " # Hard overall timeout → propagate\n",
293
+ " raise e\n",
294
+ " except requests.exceptions.RequestException as e:\n",
295
+ " # Network-ish errors: retry same key with backoff up to max_retries\n",
296
+ " retries += 1\n",
297
+ " if retries <= max_retries:\n",
298
+ " print(f\"🌐 Network error (try {retries}/{max_retries}) with key #{current_key_index + 1}: {e}\")\n",
299
+ " time.sleep(backoff)\n",
300
+ " backoff = min(backoff * 2, 60)\n",
301
+ " continue\n",
302
+ " else:\n",
303
+ " raise Exception(f\"Failed to download {url} after {max_retries} retries: {e}\")\n",
304
+ "\n",
305
+ " # If we exit the loop, we truly ran out\n",
306
+ " raise Exception(\"All API keys have been exhausted or failed.\")\n",
307
+ "\n",
308
+ "# === Main processing ===\n",
309
+ "def process_csv(csv_path):\n",
310
+ " with open(csv_path, newline='', encoding='utf-8') as csvfile:\n",
311
+ " reader = csv.DictReader(csvfile)\n",
312
+ " for index, row in enumerate(reader):\n",
313
+ " # Collect up to 20 version IDs; use the most recent\n",
314
+ " version_ids = []\n",
315
+ " for i in range(1, 21):\n",
316
+ " k = f'version_id_{i}'\n",
317
+ " if k in row and row[k]:\n",
318
+ " try:\n",
319
+ " version_ids.append(int(float(row[k])))\n",
320
+ " except ValueError:\n",
321
+ " print(f\"Invalid version_id value '{row[k]}' in row: {row}\")\n",
322
+ "\n",
323
+ " if not version_ids:\n",
324
+ " print(f\"No valid version IDs found in row: {row}\")\n",
325
+ " continue\n",
326
+ "\n",
327
+ " most_recent_version_id = str(max(version_ids))\n",
328
+ " name = row.get('name', 'unknown')\n",
329
+ " sanitized_name = safe_filename(name, max_length=100)\n",
330
+ " new_json_file = os.path.join(\n",
331
+ " output_json_dir,\n",
332
+ " f\"{index:08d}_{most_recent_version_id}_{sanitized_name}.json\"\n",
333
+ " )\n",
334
+ "\n",
335
+ " # Skip if JSON already exists\n",
336
+ " if os.path.exists(new_json_file):\n",
337
+ " #print(f\"↩️ Skipping versionID {most_recent_version_id} (JSON already exists)\")\n",
338
+ " continue\n",
339
+ "\n",
340
+ " try:\n",
341
+ " adapter_file, fname = download_file(\n",
342
+ " row['downloadUrl'], str(temp_dir),\n",
343
+ " timeout=30, overall_timeout=180\n",
344
+ " )\n",
345
+ " metadata = parse_safetensors(adapter_file)\n",
346
+ "\n",
347
+ " civitaidata = {\n",
348
+ " k: (int(v) if str(v).isdigit() else v)\n",
349
+ " for k, v in row.items()\n",
350
+ " }\n",
351
+ " new_json_data = {\n",
352
+ " \"civitaidata\": civitaidata,\n",
353
+ " \"metadata\": metadata,\n",
354
+ " \"versionID\": most_recent_version_id\n",
355
+ " }\n",
356
+ " save_json(new_json_data, new_json_file)\n",
357
+ " #print(f\"✅ Created JSON for versionID {most_recent_version_id} with file {fname}\")\n",
358
+ "\n",
359
+ " except FileNotFoundError as e:\n",
360
+ " print(f\"⚠️ {e} — saving empty metadata.\")\n",
361
+ " civitaidata = {\n",
362
+ " k: (int(v) if str(v).isdigit() else v)\n",
363
+ " for k, v in row.items()\n",
364
+ " }\n",
365
+ " empty_json = {\n",
366
+ " \"civitaidata\": civitaidata,\n",
367
+ " \"metadata\": {},\n",
368
+ " \"versionID\": most_recent_version_id,\n",
369
+ " \"error\": \"Model not found (404)\"\n",
370
+ " }\n",
371
+ " save_json(empty_json, new_json_file)\n",
372
+ " except Exception as e:\n",
373
+ " print(f\"⚠️ Error processing versionID {most_recent_version_id}: {e}\")\n",
374
+ " civitaidata = {\n",
375
+ " k: (int(v) if str(v).isdigit() else v)\n",
376
+ " for k, v in row.items()\n",
377
+ " }\n",
378
+ " empty_json = {\n",
379
+ " \"civitaidata\": civitaidata,\n",
380
+ " \"metadata\": {},\n",
381
+ " \"versionID\": most_recent_version_id,\n",
382
+ " \"error\": str(e)\n",
383
+ " }\n",
384
+ " save_json(empty_json, new_json_file)\n",
385
+ " print(f\"💾 Saved empty JSON for versionID {most_recent_version_id} due to failure.\")\n",
386
+ "\n",
387
+ "# === Run ===\n",
388
+ "if __name__ == \"__main__\":\n",
389
+ " process_csv(csv_path)\n"
390
+ ]
391
+ }
392
+ ],
393
+ "metadata": {
394
+ "language_info": {
395
+ "name": "python"
396
+ }
397
+ },
398
+ "nbformat": 4,
399
+ "nbformat_minor": 5
400
+ }
jupyter_notebooks/Section_2-4_Figure_9_extract_LoRA_metadata.ipynb ADDED
@@ -0,0 +1,557 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "781346f0-fab6-45f9-9193-dd547f201864",
6
+ "metadata": {
7
+ "execution": {
8
+ "iopub.execute_input": "2025-02-08T11:14:38.839305Z",
9
+ "iopub.status.busy": "2025-02-08T11:14:38.837963Z",
10
+ "iopub.status.idle": "2025-02-08T11:14:38.845161Z",
11
+ "shell.execute_reply": "2025-02-08T11:14:38.844121Z",
12
+ "shell.execute_reply.started": "2025-02-08T11:14:38.839221Z"
13
+ }
14
+ },
15
+ "source": [
16
+ "# Section 3-4: LoRA metadata"
17
+ ]
18
+ },
19
+ {
20
+ "cell_type": "markdown",
21
+ "id": "90b42e45-31bc-4cbd-b820-cf2daccbd712",
22
+ "metadata": {
23
+ "execution": {
24
+ "iopub.execute_input": "2025-02-08T21:10:00.955922Z",
25
+ "iopub.status.busy": "2025-02-08T21:10:00.955285Z",
26
+ "iopub.status.idle": "2025-02-08T21:10:00.959160Z",
27
+ "shell.execute_reply": "2025-02-08T21:10:00.958709Z",
28
+ "shell.execute_reply.started": "2025-02-08T21:10:00.955902Z"
29
+ }
30
+ },
31
+ "source": [
32
+ "### How are models trained? \n",
33
+ "### What are the most common training tags? \n",
34
+ "### What tagging systems are used?"
35
+ ]
36
+ },
37
+ {
38
+ "cell_type": "raw",
39
+ "id": "79b10960-c7c6-4aa6-b91e-f0caf5c2a7dd",
40
+ "metadata": {},
41
+ "source": [
42
+ "LoRA Metadata Processing Workflow\n",
43
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
44
+ "│ Load CSV File │ --> │ Read adapter metadata CSV file. │\n",
45
+ "│ Read Model Versions │ │ Extract model version IDs and relevant data. │\n",
46
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
47
+ " ↓\n",
48
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
49
+ "│ Download Adapter │ --> │ Use stored download URLs to fetch adapter files │\n",
50
+ "│ Files Using API │ │ using rotating API keys. │\n",
51
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
52
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
53
+ "│ Parse Metadata │ --> │ Extract safetensors metadata, such as training │\n",
54
+ "│ from SafeTensor │ │ images, model type, and architecture. │\n",
55
+ "│ Files │ │ │\n",
56
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
57
+ " ↓\n",
58
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
59
+ "│ Store Parsed │ --> │ Save extracted metadata into structured JSON │\n",
60
+ "│ Metadata as JSON │ │ files for later analysis. │\n",
61
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
62
+ "\n",
63
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
64
+ "│ Process JSON Files │ --> │ Read saved JSON metadata, extract relevant │\n",
65
+ "│ for Consolidation │ │ details, and filter necessary attributes. │\n",
66
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
67
+ " ↓\n",
68
+ "┌──────────────────────┐ ┌───────────────────────��───────────────────────────┐\n",
69
+ "│ Extract Training │ --> │ Identify most frequent training tags, architectures│\n",
70
+ "│ Tags & Model Info │ │ and systems used for model creation. │\n",
71
+ "└─────────┬────────────┘ └───────────────────────────────────────────────────┘\n",
72
+ " ↓\n",
73
+ "┌──────────────────────┐ ┌───────────────────────────────────────────────────┐\n",
74
+ "│ Save Consolidated │ --> │ Store all processed metadata in a structured CSV │\n",
75
+ "│ Metadata to CSV │ │ format for final analysis. │\n",
76
+ "└──────────────────────┘ └───────────────────────────────────────────────────┘\n"
77
+ ]
78
+ },
79
+ {
80
+ "cell_type": "markdown",
81
+ "id": "fd202dbd-fc0f-45f1-9d88-94c7d3635207",
82
+ "metadata": {
83
+ "execution": {
84
+ "iopub.execute_input": "2025-02-08T20:54:00.478266Z",
85
+ "iopub.status.busy": "2025-02-08T20:54:00.477080Z",
86
+ "iopub.status.idle": "2025-02-08T20:54:00.481464Z",
87
+ "shell.execute_reply": "2025-02-08T20:54:00.480830Z",
88
+ "shell.execute_reply.started": "2025-02-08T20:54:00.478244Z"
89
+ }
90
+ },
91
+ "source": [
92
+ "### Define Common Paths, Fonts etc."
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "code",
97
+ "execution_count": 24,
98
+ "id": "9da03b00",
99
+ "metadata": {
100
+ "execution": {
101
+ "iopub.execute_input": "2025-02-08T21:11:15.085872Z",
102
+ "iopub.status.busy": "2025-02-08T21:11:15.084865Z",
103
+ "iopub.status.idle": "2025-02-08T21:11:15.117479Z",
104
+ "shell.execute_reply": "2025-02-08T21:11:15.116955Z",
105
+ "shell.execute_reply.started": "2025-02-08T21:11:15.085846Z"
106
+ }
107
+ },
108
+ "outputs": [
109
+ {
110
+ "name": "stdout",
111
+ "output_type": "stream",
112
+ "text": [
113
+ "Paths and fonts initialized successfully.\n",
114
+ "Paths initialized successfully.\n"
115
+ ]
116
+ }
117
+ ],
118
+ "source": [
119
+ "import os\n",
120
+ "import re\n",
121
+ "import json\n",
122
+ "import csv\n",
123
+ "import struct\n",
124
+ "import requests\n",
125
+ "from pathlib import Path\n",
126
+ "import pandas as pd\n",
127
+ "from collections import Counter\n",
128
+ "from concurrent.futures import ProcessPoolExecutor\n",
129
+ "from pathlib import Path\n",
130
+ "import matplotlib.pyplot as plt\n",
131
+ "from matplotlib.font_manager import FontProperties\n",
132
+ "from matplotlib import font_manager\n",
133
+ "import pandas as pd\n",
134
+ "from collections import Counter\n",
135
+ "from concurrent.futures import ProcessPoolExecutor\n",
136
+ "\n",
137
+ "# Define the current directory and important file paths\n",
138
+ "current_dir = Path.cwd()\n",
139
+ "\n",
140
+ "# Define frequently used directories\n",
141
+ "data_dir = current_dir.parent / 'data/CSV'\n",
142
+ "fonts_dir = current_dir.parent / 'misc/assets/fonts'\n",
143
+ "plots_dir = current_dir.parent / 'plots'\n",
144
+ "raw_data_dir = current_dir.parent / 'data/raw/adapter_metadata'\n",
145
+ "temp_dir = current_dir.parent / 'data/models/adapter_temp'\n",
146
+ "misc_dir = current_dir.parent / 'misc'\n",
147
+ "\n",
148
+ "# File paths\n",
149
+ "adapters_csv = data_dir / 'model_subsets/Civiverse_adapters_poi_false.csv'\n",
150
+ "output_json_dir = raw_data_dir\n",
151
+ "api_keys_file = misc_dir / 'api_keys.txt'\n",
152
+ "\n",
153
+ "# Ensure directories exist\n",
154
+ "os.makedirs(output_json_dir, exist_ok=True)\n",
155
+ "os.makedirs(temp_dir, exist_ok=True)\n",
156
+ "\n",
157
+ "# Font paths for Matplotlib\n",
158
+ "font_paths = [\n",
159
+ " fonts_dir / 'Noto_Sans_JP/static/NotoSansJP-ExtraBold.ttf',\n",
160
+ " fonts_dir / 'Noto_Sans_JP/static/NotoSansJP-Bold.ttf',\n",
161
+ " fonts_dir / 'Noto_Sans_JP/static/NotoSansJP-ExtraLight.ttf',\n",
162
+ " fonts_dir / 'Noto_Sans_JP/static/NotoSansJP-Light.ttf',\n",
163
+ " fonts_dir / 'Noto_Sans_JP/static/NotoSansJP-Medium.ttf',\n",
164
+ " fonts_dir / 'Noto_Sans_JP/static/NotoSansJP-SemiBold.ttf',\n",
165
+ " fonts_dir / 'Noto_Sans_JP/static/NotoSansJP-Regular.ttf',\n",
166
+ " fonts_dir / 'Noto_Sans_SC/static/NotoSansSC-ExtraBold.ttf',\n",
167
+ " fonts_dir / 'Noto_Sans_SC/static/NotoSansSC-Bold.ttf',\n",
168
+ " fonts_dir / 'Noto_Sans_SC/static/NotoSansSC-ExtraLight.ttf',\n",
169
+ " fonts_dir / 'Noto_Sans_SC/static/NotoSansSC-Light.ttf',\n",
170
+ " fonts_dir / 'Noto_Sans_SC/static/NotoSansSC-Medium.ttf',\n",
171
+ " fonts_dir / 'Noto_Sans_SC/static/NotoSansSC-SemiBold.ttf',\n",
172
+ " fonts_dir / 'Noto_Sans_SC/static/NotoSansSC-Regular.ttf'\n",
173
+ "]\n",
174
+ "\n",
175
+ "# Load fonts into Matplotlib\n",
176
+ "for font_path in font_paths:\n",
177
+ " font_manager.fontManager.addfont(font_path)\n",
178
+ "\n",
179
+ "# Set default font family for plots\n",
180
+ "plt.rcParams['font.family'] = ['Noto Sans JP', 'Noto Sans SC', 'sans-serif']\n",
181
+ "\n",
182
+ "print('Paths and fonts initialized successfully.')\n",
183
+ "\n",
184
+ "print('Paths initialized successfully.')"
185
+ ]
186
+ },
187
+ {
188
+ "cell_type": "markdown",
189
+ "id": "9c6b84ec-c23e-4047-a288-cfab2f5d8fdb",
190
+ "metadata": {},
191
+ "source": [
192
+ "## Step 1: Download LoRA and extract *.safetensors metadata"
193
+ ]
194
+ },
195
+ {
196
+ "cell_type": "markdown",
197
+ "id": "e484c678-70a8-4b70-b0aa-9ce69414c43b",
198
+ "metadata": {},
199
+ "source": [
200
+ "This script downloads LoRA adapters from the filtered Civiverse-Models dataset and extracts the metadata found within the *.safetensors' data structure"
201
+ ]
202
+ },
203
+ {
204
+ "cell_type": "code",
205
+ "execution_count": 3,
206
+ "id": "ecf31f5c",
207
+ "metadata": {
208
+ "execution": {
209
+ "iopub.execute_input": "2025-02-08T20:49:20.487791Z",
210
+ "iopub.status.busy": "2025-02-08T20:49:20.487464Z",
211
+ "iopub.status.idle": "2025-02-08T20:49:20.499881Z",
212
+ "shell.execute_reply": "2025-02-08T20:49:20.499471Z",
213
+ "shell.execute_reply.started": "2025-02-08T20:49:20.487771Z"
214
+ }
215
+ },
216
+ "outputs": [],
217
+ "source": [
218
+ "output_json_dir = output_json_dir\n",
219
+ "temp_dir = temp_dir ### better delete these later for space efficiency\n",
220
+ "api_karussell = api_keys_file\n",
221
+ "os.makedirs(output_json_dir, exist_ok=True)\n",
222
+ "os.makedirs(temp_dir, exist_ok=True)\n",
223
+ "\n",
224
+ "# Load API keys\n",
225
+ "def load_api_keys(api_path):\n",
226
+ " if not os.path.exists(api_path):\n",
227
+ " raise FileNotFoundError(f\"API keys file does not exist: {api_path}\")\n",
228
+ " with open(api_path, 'r') as file:\n",
229
+ " return [line.strip() for line in file if line.strip()]\n",
230
+ "\n",
231
+ "api_keys = load_api_keys(api_karussell) # Replace with the path to your API keys file\n",
232
+ "current_key_index = 0\n",
233
+ "\n",
234
+ "def get_headers():\n",
235
+ " \"\"\"Generate request headers with the current API key.\"\"\"\n",
236
+ " global current_key_index\n",
237
+ " return {\n",
238
+ " \"Accept\": \"application/json\",\n",
239
+ " \"Authorization\": f\"Bearer {api_keys[current_key_index]}\"\n",
240
+ " }\n",
241
+ "\n",
242
+ "def rotate_api_key():\n",
243
+ " \"\"\"Rotate to the next API key.\"\"\"\n",
244
+ " global current_key_index\n",
245
+ " if current_key_index < len(api_keys) - 1:\n",
246
+ " current_key_index += 1\n",
247
+ " else:\n",
248
+ " raise Exception(\"All API keys have been exhausted.\")\n",
249
+ "\n",
250
+ "# Function to parse .safetensors metadata\n",
251
+ "def parse_safetensors(file_path):\n",
252
+ " try:\n",
253
+ " with open(file_path, 'rb') as f:\n",
254
+ " file_data = f.read()\n",
255
+ " metadata_size = struct.unpack('<I', file_data[:4])[0]\n",
256
+ " metadata_bytes = file_data[8:8 + metadata_size]\n",
257
+ " metadata_str = metadata_bytes.decode('utf-8')\n",
258
+ " metadata = json.loads(metadata_str)\n",
259
+ " return metadata.get('__metadata__', {})\n",
260
+ " except Exception as e:\n",
261
+ " print(f\"Error parsing safetensors file: {e}\")\n",
262
+ " return {}\n",
263
+ "\n",
264
+ "def save_json(data, filename):\n",
265
+ " with open(filename, 'w') as f:\n",
266
+ " json.dump(data, f, indent=4)\n",
267
+ "\n",
268
+ "def download_file(url, output_folder):\n",
269
+ " \"\"\"Download file with API key rotation.\"\"\"\n",
270
+ " filename = url.split(\"/\")[-1]\n",
271
+ " output_path = os.path.join(output_folder, filename)\n",
272
+ "\n",
273
+ " global current_key_index\n",
274
+ " while current_key_index < len(api_keys):\n",
275
+ " try:\n",
276
+ " response = requests.get(url, headers=get_headers(), stream=True)\n",
277
+ " if response.status_code == 401: # Unauthorized\n",
278
+ " print(f\"API key {current_key_index + 1} failed. Trying next key.\")\n",
279
+ " rotate_api_key()\n",
280
+ " continue\n",
281
+ " elif response.status_code == 403: # Forbidden\n",
282
+ " print(f\"Access forbidden for API key {current_key_index + 1}. Rotating to the next key.\")\n",
283
+ " rotate_api_key()\n",
284
+ " continue\n",
285
+ " response.raise_for_status()\n",
286
+ "\n",
287
+ " # Save the file to the specified output folder\n",
288
+ " with open(output_path, 'wb') as file:\n",
289
+ " for chunk in response.iter_content(chunk_size=8192):\n",
290
+ " file.write(chunk)\n",
291
+ " return output_path, filename\n",
292
+ " except requests.exceptions.RequestException as e:\n",
293
+ " print(f\"Error downloading file: {e}\")\n",
294
+ " rotate_api_key()\n",
295
+ " raise Exception(\"All API keys failed.\")\n",
296
+ "\n",
297
+ "def process_csv(csv_path):\n",
298
+ " # Read the CSV and process each row\n",
299
+ " with open(csv_path, newline='', encoding='utf-8') as csvfile:\n",
300
+ " reader = csv.DictReader(csvfile)\n",
301
+ " for index, row in enumerate(reader):\n",
302
+ " version_ids = []\n",
303
+ " for i in range(1, 21):\n",
304
+ " key = f'version_id_{i}'\n",
305
+ " if key in row and row[key]:\n",
306
+ " try:\n",
307
+ " version_ids.append(int(float(row[key])))\n",
308
+ " except ValueError:\n",
309
+ " print(f\"Invalid version_id value '{row[key]}' in row: {row}\")\n",
310
+ "\n",
311
+ " if not version_ids:\n",
312
+ " print(f\"No valid version IDs found in row: {row}\")\n",
313
+ " continue\n",
314
+ "\n",
315
+ " most_recent_version_id = str(max(version_ids))\n",
316
+ "\n",
317
+ " try:\n",
318
+ " adapter_file, filename = download_file(row['downloadUrl'], temp_dir)\n",
319
+ " metadata = parse_safetensors(adapter_file)\n",
320
+ " # Add all CSV data under 'civitaidata'\n",
321
+ " civitaidata = {key: int(value) if value.isdigit() else value for key, value in row.items()}\n",
322
+ " new_json_data = {\n",
323
+ " \"civitaidata\": civitaidata,\n",
324
+ " \"metadata\": metadata,\n",
325
+ " \"versionID\": most_recent_version_id\n",
326
+ " }\n",
327
+ " sanitized_name = row['name'].replace(\" \", \"_\").replace(\"/\", \"_\")\n",
328
+ " new_json_file = os.path.join(\n",
329
+ " output_json_dir,\n",
330
+ " f\"{index:08d}_{most_recent_version_id}_{sanitized_name}.json\"\n",
331
+ " )\n",
332
+ " save_json(new_json_data, new_json_file)\n",
333
+ " print(f\"Created new JSON for versionID {most_recent_version_id} with filename {filename}\")\n",
334
+ " except Exception as e:\n",
335
+ " print(f\"Error processing versionID {most_recent_version_id}: {e}\")"
336
+ ]
337
+ },
338
+ {
339
+ "cell_type": "markdown",
340
+ "id": "708131db-95d8-4427-9397-74c72d3edf48",
341
+ "metadata": {},
342
+ "source": [
343
+ "Uncomment the following to download and process model adapters"
344
+ ]
345
+ },
346
+ {
347
+ "cell_type": "code",
348
+ "execution_count": 4,
349
+ "id": "51ce1943-e509-442d-8b92-1a67bc686471",
350
+ "metadata": {
351
+ "execution": {
352
+ "iopub.execute_input": "2025-02-08T20:49:21.547880Z",
353
+ "iopub.status.busy": "2025-02-08T20:49:21.547383Z",
354
+ "iopub.status.idle": "2025-02-08T20:49:21.550529Z",
355
+ "shell.execute_reply": "2025-02-08T20:49:21.550087Z",
356
+ "shell.execute_reply.started": "2025-02-08T20:49:21.547860Z"
357
+ }
358
+ },
359
+ "outputs": [],
360
+ "source": [
361
+ "csv_path = adapters_csv \n",
362
+ "#process_csv(csv_path) #### UNCOMMENT HERE"
363
+ ]
364
+ },
365
+ {
366
+ "cell_type": "markdown",
367
+ "id": "4c2c52ee-83ad-4269-a7bf-16225b71f427",
368
+ "metadata": {},
369
+ "source": [
370
+ "## Step 2: Accumulate training tags and compare with auto-tagging vocabulary"
371
+ ]
372
+ },
373
+ {
374
+ "cell_type": "code",
375
+ "execution_count": 5,
376
+ "id": "6de2d9a6",
377
+ "metadata": {
378
+ "execution": {
379
+ "iopub.execute_input": "2025-02-08T20:49:31.218029Z",
380
+ "iopub.status.busy": "2025-02-08T20:49:31.217597Z",
381
+ "iopub.status.idle": "2025-02-08T20:49:32.646108Z",
382
+ "shell.execute_reply": "2025-02-08T20:49:32.645340Z",
383
+ "shell.execute_reply.started": "2025-02-08T20:49:31.218005Z"
384
+ }
385
+ },
386
+ "outputs": [
387
+ {
388
+ "name": "stdout",
389
+ "output_type": "stream",
390
+ "text": [
391
+ "Processed JSON files and summary saved to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/CSV/model_subsets/Section_6-5/Lora_metadata.csv\n",
392
+ "Tag summary saved to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/CSV/model_subsets/Section_6-5/LoRA_training_tags_acc.csv\n"
393
+ ]
394
+ }
395
+ ],
396
+ "source": [
397
+ "interrogator_vocab = current_dir.parent / 'misc/autotagging-vocabularies/clip_interrogator.csv'\n",
398
+ "deepbooru_vocab = current_dir.parent / 'misc/autotagging-vocabularies/deepbooru_tags.txt'\n",
399
+ "\n",
400
+ "\n",
401
+ "def load_tags():\n",
402
+ " try:\n",
403
+ " clip_interrogator_tags = pd.read_csv(\n",
404
+ " interrogator_vocab, header=None, usecols=[0], on_bad_lines='skip'\n",
405
+ " )[0].tolist()\n",
406
+ " except pd.errors.ParserError as e:\n",
407
+ " print(f\"Error reading 'clip_interrogator.csv': {e}\")\n",
408
+ " clip_interrogator_tags = []\n",
409
+ "\n",
410
+ " try:\n",
411
+ " with open(deepbooru_vocab, 'r', encoding='utf-8') as file:\n",
412
+ " deepbooru_tags = {line.strip() for line in file}\n",
413
+ " except FileNotFoundError as e:\n",
414
+ " print(f\"Error reading 'deepbooru_tags.txt': {e}\")\n",
415
+ " deepbooru_tags = set()\n",
416
+ "\n",
417
+ " return clip_interrogator_tags, deepbooru_tags\n",
418
+ "\n",
419
+ "clip_interrogator_tags, deepbooru_tags = load_tags()\n",
420
+ "\n",
421
+ "# Determines tagging system based on tag matches\n",
422
+ "def determine_tagging_system(tags):\n",
423
+ " clip_interrogator_matches = sum(1 for tag in tags if tag.strip() in clip_interrogator_tags)\n",
424
+ " deepbooru_matches = sum(1 for tag in tags if tag.strip() in deepbooru_tags or tag.strip().replace(' ', '_') in deepbooru_tags)\n",
425
+ " \n",
426
+ " if deepbooru_matches > clip_interrogator_matches:\n",
427
+ " return 'deepbooru'\n",
428
+ " elif clip_interrogator_matches > deepbooru_matches:\n",
429
+ " return 'clip-interrogator'\n",
430
+ " elif clip_interrogator_matches == 0 and deepbooru_matches == 0 and tags:\n",
431
+ " return 'other'\n",
432
+ " else:\n",
433
+ " return 'no tag metadata'\n",
434
+ "\n",
435
+ "def process_json_file(file_path):\n",
436
+ " try:\n",
437
+ " with open(file_path, 'r', encoding='utf-8') as file:\n",
438
+ " data = json.load(file)\n",
439
+ " except (json.JSONDecodeError, IOError) as e:\n",
440
+ " print(f\"Error processing file {file_path}: {e}\")\n",
441
+ " return None\n",
442
+ "\n",
443
+ " # Handle ss_tag_frequency - Ensure it's parsed correctly\n",
444
+ " tag_frequency_raw = data.get('metadata', {}).get('ss_tag_frequency', {})\n",
445
+ " if isinstance(tag_frequency_raw, str):\n",
446
+ " try:\n",
447
+ " tag_frequency_raw = json.loads(tag_frequency_raw)\n",
448
+ " except json.JSONDecodeError:\n",
449
+ " print(f\"Error decoding ss_tag_frequency in {file_path}\")\n",
450
+ " return None # Skip files with unreadable tag data\n",
451
+ "\n",
452
+ " if not isinstance(tag_frequency_raw, dict) or not tag_frequency_raw:\n",
453
+ " return None # Skip files with empty or nonexistent ss_tag_frequency\n",
454
+ "\n",
455
+ " filename = os.path.basename(file_path).replace('.json', '')\n",
456
+ " modelspec_title = data.get('metadata', {}).get('ss_output_name', '') # Using ss_output_name instead\n",
457
+ " modelspec_architecture = data.get('metadata', {}).get('ss_network_module', '')\n",
458
+ " ss_num_train_images = data.get('metadata', {}).get('ss_num_train_images', 0)\n",
459
+ " ss_steps = data.get('metadata', {}).get('ss_steps', 0)\n",
460
+ " ss_sd_model_name = data.get('metadata', {}).get('ss_sd_model_name', '')\n",
461
+ "\n",
462
+ " # Extract first-level tag frequencies\n",
463
+ " tag_frequency = next(iter(tag_frequency_raw.values()), {}) # Get the first nested dict\n",
464
+ " tags = list(tag_frequency.keys())\n",
465
+ " training_system = determine_tagging_system(tags) if tags else 'undetermined'\n",
466
+ "\n",
467
+ " tag_items = list(tag_frequency.items())[:20]\n",
468
+ " tag_data = {f'tag{i+1:02d}': tag_items[i][0] if i < len(tag_items) else None for i in range(20)}\n",
469
+ " tag_data.update({f'tag{i+1:02d}_no': tag_items[i][1] if i < len(tag_items) else None for i in range(20)})\n",
470
+ "\n",
471
+ " row = {\n",
472
+ " 'filename': filename,\n",
473
+ " 'modelspec_title': modelspec_title,\n",
474
+ " 'modelspec_architecture': modelspec_architecture,\n",
475
+ " 'ss_num_train_images': ss_num_train_images,\n",
476
+ " 'ss_steps': ss_steps,\n",
477
+ " 'ss_sd_model_name': ss_sd_model_name,\n",
478
+ " 'training_system': training_system\n",
479
+ " }\n",
480
+ " row.update(tag_data)\n",
481
+ "\n",
482
+ " return row, tag_frequency\n",
483
+ "\n",
484
+ "def parallel_process_files(folder_path):\n",
485
+ " rows = []\n",
486
+ " tag_occurrences = Counter()\n",
487
+ " json_files = [os.path.join(root, file) for root, _, files in os.walk(folder_path) for file in files if file.endswith('.json')]\n",
488
+ "\n",
489
+ " with ProcessPoolExecutor(max_workers=4) as executor:\n",
490
+ " results = executor.map(process_json_file, json_files, chunksize=1) # Using small chunksize for debugging\n",
491
+ "\n",
492
+ " for result in results:\n",
493
+ " if result:\n",
494
+ " row, tag_frequency = result\n",
495
+ " rows.append(row)\n",
496
+ " tag_occurrences.update(tag_frequency)\n",
497
+ "\n",
498
+ " total_tags_count = sum(tag_occurrences.values())\n",
499
+ " unique_tags_count = len(tag_occurrences)\n",
500
+ "\n",
501
+ " return rows, total_tags_count, unique_tags_count, tag_occurrences\n",
502
+ "\n",
503
+ "def write_summary_and_csv(rows, total_tags_count, unique_tags_count, tag_occurrences, output_file, tag_summary_file):\n",
504
+ " if not rows:\n",
505
+ " print(\"No valid data to write to CSV.\")\n",
506
+ " return\n",
507
+ "\n",
508
+ " df = pd.DataFrame(rows)\n",
509
+ " df.to_csv(output_file, index=False, encoding='utf-8')\n",
510
+ "\n",
511
+ " tag_summary_df = pd.DataFrame(list(tag_occurrences.items()), columns=['Tag (ss_tag_frequency/tag_frequency)', 'No. of Occurrences'])\n",
512
+ " tag_summary_df = tag_summary_df.sort_values(by='No. of Occurrences', ascending=False)\n",
513
+ " tag_summary_df.to_csv(tag_summary_file, index=False, encoding='utf-8')\n",
514
+ "\n",
515
+ " print(f\"Processed JSON files and summary saved to {output_file}\")\n",
516
+ " print(f\"Tag summary saved to {tag_summary_file}\")\n",
517
+ "\n",
518
+ "\n",
519
+ "def main():\n",
520
+ " folder_path = '/home/lauwag/shares/laura_wagner/Civitai_page_analysis/Civitai_visualizations/data/raw/adapter_metadata' # Path to your JSON files\n",
521
+ " \n",
522
+ " output_dir = current_dir.parent / 'data/CSV/model_subsets/Section_6-5/'\n",
523
+ " os.makedirs(output_dir, exist_ok=True) # Ensure the directory exists\n",
524
+ "\n",
525
+ " output_file = output_dir / 'Lora_metadata.csv'\n",
526
+ " tag_summary_file = output_dir / 'LoRA_training_tags_acc.csv'\n",
527
+ "\n",
528
+ " rows, total_tags_count, unique_tags_count, tag_occurrences = parallel_process_files(folder_path)\n",
529
+ " write_summary_and_csv(rows, total_tags_count, unique_tags_count, tag_occurrences, output_file, tag_summary_file)\n",
530
+ "\n",
531
+ "if __name__ == \"__main__\":\n",
532
+ " main()\n"
533
+ ]
534
+ }
535
+ ],
536
+ "metadata": {
537
+ "kernelspec": {
538
+ "display_name": "Python 3 (ipykernel)",
539
+ "language": "python",
540
+ "name": "python3"
541
+ },
542
+ "language_info": {
543
+ "codemirror_mode": {
544
+ "name": "ipython",
545
+ "version": 3
546
+ },
547
+ "file_extension": ".py",
548
+ "mimetype": "text/x-python",
549
+ "name": "python",
550
+ "nbconvert_exporter": "python",
551
+ "pygments_lexer": "ipython3",
552
+ "version": "3.11.9"
553
+ }
554
+ },
555
+ "nbformat": 4,
556
+ "nbformat_minor": 5
557
+ }
{scripts → jupyter_notebooks}/Section_3-3-4_deepfakes.ipynb RENAMED
File without changes
jupyter_notebooks/SuppM_Figure_13_Danbooru_categories.ipynb ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 2,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "import json\n",
10
+ "from pathlib import Path\n",
11
+ "\n",
12
+ "current_dir = Path.cwd()\n",
13
+ "input_json = current_dir.parent / \"misc/lists/danbooru.json\"\n",
14
+ "output_json = current_dir.parent / \"public/json/danbooru_flat.json\"\n",
15
+ "\n",
16
+ "# ---- CATEGORY COLORS ----\n",
17
+ "CATEGORY_COLORS = {\n",
18
+ " \"attire_and_body_accessories\": \"#DC143C\",\n",
19
+ " \"body\": \"coral\",\n",
20
+ " \"characters\": \"silver\",\n",
21
+ " \"copyrights\": \"#264653\",\n",
22
+ " \"creatures\": \"silver\",\n",
23
+ " \"drawing software\": \"#219ebc\",\n",
24
+ " \"games\": \"silver\",\n",
25
+ " \"metatags\": \"silver\",\n",
26
+ " \"more\": \"silver\",\n",
27
+ " \"objects\": \"#6d6875\",\n",
28
+ " \"plant\": \"#7cb518\",\n",
29
+ " \"real_world\": \"#a5a58d\",\n",
30
+ " \"sex\": \"#ef476f\",\n",
31
+ " \"visual_characteristics\": \"#06d6a0\",\n",
32
+ " \"subject\": \"#ffd166\",\n",
33
+ " \"uncategorized\": \"#adb5bd\",\n",
34
+ " \"actions_and_expressions\": \"#d00000\",\n",
35
+ " \"objects_and_backgrounds\": \"#118ab2\"\n",
36
+ "}\n",
37
+ "\n",
38
+ "# ---- STEP 1: Load the original nested JSON ----\n",
39
+ "with open(input_json, \"r\", encoding=\"utf-8\") as f:\n",
40
+ " nested_data = json.load(f)\n",
41
+ "\n",
42
+ "# ---- STEP 2: Recursively extract paths and limit tags ----\n",
43
+ "def extract_tags(data, path=None, result=None):\n",
44
+ " if path is None:\n",
45
+ " path = []\n",
46
+ " if result is None:\n",
47
+ " result = []\n",
48
+ "\n",
49
+ " if isinstance(data, dict):\n",
50
+ " for key, value in data.items():\n",
51
+ " extract_tags(value, path + [key], result)\n",
52
+ " elif isinstance(data, list) and len(data) > 0:\n",
53
+ " limited_tags = [data[0]] + [\"...\"] if len(data) > 1 else data\n",
54
+ " result.append({\n",
55
+ " \"level_path\": \" > \".join(path),\n",
56
+ " \"top_tags\": limited_tags,\n",
57
+ " \"tag_count\": len(data)\n",
58
+ " })\n",
59
+ "\n",
60
+ " return result\n",
61
+ "\n",
62
+ "# ---- STEP 3: Build the nested structure ----\n",
63
+ "def build_tree_from_flat_list(flat_data):\n",
64
+ " tree = {\"name\": \"root\", \"children\": []}\n",
65
+ "\n",
66
+ " for row in flat_data:\n",
67
+ " parts = row['level_path'].split(\" > \")\n",
68
+ " current = tree\n",
69
+ " for i, part in enumerate(parts):\n",
70
+ " match = next((child for child in current.get(\"children\", []) if child[\"name\"] == part), None)\n",
71
+ " if not match:\n",
72
+ " match = {\"name\": part, \"children\": []}\n",
73
+ " current.setdefault(\"children\", []).append(match)\n",
74
+ "\n",
75
+ " # If this is the top-level category, set its color\n",
76
+ " if current[\"name\"] == \"root\":\n",
77
+ " color = CATEGORY_COLORS.get(part.lower(), \"#888888\")\n",
78
+ " match[\"color\"] = color\n",
79
+ " else:\n",
80
+ " # Inherit from parent\n",
81
+ " match[\"color\"] = current.get(\"color\", \"#888888\")\n",
82
+ "\n",
83
+ " current = match\n",
84
+ "\n",
85
+ " current[\"tags\"] = row[\"top_tags\"]\n",
86
+ " current[\"direct_tag_count\"] = row[\"tag_count\"]\n",
87
+ "\n",
88
+ " return tree\n",
89
+ "\n",
90
+ "# ---- STEP 4: Recursively compute tag and category sizes ----\n",
91
+ "def compute_sizes(node):\n",
92
+ " tag_count = node.get(\"direct_tag_count\", 0)\n",
93
+ " category_count = 0\n",
94
+ "\n",
95
+ " for child in node.get(\"children\", []):\n",
96
+ " compute_sizes(child)\n",
97
+ " tag_count += child.get(\"tag_count\", 0)\n",
98
+ " category_count += 1 + child.get(\"category_count\", 0)\n",
99
+ "\n",
100
+ " node[\"tag_count\"] = tag_count\n",
101
+ " node[\"category_count\"] = category_count\n",
102
+ "\n",
103
+ "# ---- STEP 5: Generate and save final JSON ----\n",
104
+ "flat_data = extract_tags(nested_data)\n",
105
+ "tree_data = build_tree_from_flat_list(flat_data)\n",
106
+ "compute_sizes(tree_data)\n",
107
+ "\n",
108
+ "with open(output_json, \"w\", encoding=\"utf-8\") as f:\n",
109
+ " json.dump(tree_data, f, ensure_ascii=False, indent=2)\n"
110
+ ]
111
+ },
112
+ {
113
+ "cell_type": "code",
114
+ "execution_count": null,
115
+ "metadata": {},
116
+ "outputs": [],
117
+ "source": []
118
+ }
119
+ ],
120
+ "metadata": {
121
+ "kernelspec": {
122
+ "display_name": "latm",
123
+ "language": "python",
124
+ "name": "python3"
125
+ },
126
+ "language_info": {
127
+ "codemirror_mode": {
128
+ "name": "ipython",
129
+ "version": 3
130
+ },
131
+ "file_extension": ".py",
132
+ "mimetype": "text/x-python",
133
+ "name": "python",
134
+ "nbconvert_exporter": "python",
135
+ "pygments_lexer": "ipython3",
136
+ "version": "3.10.15"
137
+ }
138
+ },
139
+ "nbformat": 4,
140
+ "nbformat_minor": 2
141
+ }
jupyter_notebooks/SuppM_Figure_S12_asset_types.ipynb ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "id": "c0d18a6a",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import pandas as pd\n",
11
+ "import matplotlib.pyplot as plt\n",
12
+ "from matplotlib.ticker import FuncFormatter\n",
13
+ "from pathlib import Path"
14
+ ]
15
+ },
16
+ {
17
+ "cell_type": "code",
18
+ "execution_count": 2,
19
+ "id": "98d25755",
20
+ "metadata": {},
21
+ "outputs": [],
22
+ "source": [
23
+ "current_dir = Path.cwd()\n",
24
+ "\n",
25
+ "def sortByFrequency_model_types_csv(csv_path, output_svg_path):\n",
26
+ " hatch_pattern = '\\\\\\\\\\\\\\\\\\\\\\\\' # Hatch pattern for the bars\n",
27
+ "\n",
28
+ " # Read the CSV file\n",
29
+ " df = pd.read_csv(csv_path)\n",
30
+ "\n",
31
+ " if 'type' not in df.columns:\n",
32
+ " return \"The CSV file does not contain a 'type' column.\"\n",
33
+ "\n",
34
+ " # Count the occurrences of each model type\n",
35
+ " type_counts = df['type'].value_counts().reset_index()\n",
36
+ " type_counts.columns = ['Type', 'Count']\n",
37
+ " total = type_counts['Count'].sum()\n",
38
+ " type_counts['Percentage'] = (type_counts['Count'] / total * 100).round(2)\n",
39
+ "\n",
40
+ " # Sort the data in ascending order\n",
41
+ " type_counts = type_counts.sort_values(by='Count', ascending=True)\n",
42
+ "\n",
43
+ " # Plotting\n",
44
+ " plt.figure(figsize=(10, 3.5))\n",
45
+ " bars = plt.barh(type_counts['Type'], type_counts['Count'], color='white', hatch=hatch_pattern, edgecolor='coral')\n",
46
+ " plt.xlabel('Counts', fontweight='bold')\n",
47
+ " plt.ylabel('Asset Type', fontweight='bold')\n",
48
+ "\n",
49
+ " ax = plt.gca()\n",
50
+ "\n",
51
+ " # Hide all axis spines\n",
52
+ " for spine in ax.spines.values():\n",
53
+ " spine.set_visible(False)\n",
54
+ "\n",
55
+ " # Keep ticks visible\n",
56
+ " ax.xaxis.set_ticks_position('bottom')\n",
57
+ " ax.yaxis.set_ticks_position('left')\n",
58
+ "\n",
59
+ " # Bold tick labels\n",
60
+ " for label in ax.get_xticklabels() + ax.get_yticklabels():\n",
61
+ " label.set_fontweight('bold')\n",
62
+ "\n",
63
+ " # Format x-axis ticks: 25000 → 25 k\n",
64
+ " ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f'{int(x/1000)} k' if x >= 1000 else f'{int(x)}'))\n",
65
+ "\n",
66
+ " # Add percentage labels to the bars\n",
67
+ " for bar, percentage in zip(bars, type_counts['Percentage']):\n",
68
+ " plt.text(bar.get_width() + 5, bar.get_y() + bar.get_height()/2,\n",
69
+ " f' {percentage}%', va='center', color='blueviolet', fontweight='bold')\n",
70
+ "\n",
71
+ " plt.tight_layout()\n",
72
+ " plt.savefig(out_file, format='svg')\n",
73
+ " plt.show()\n"
74
+ ]
75
+ },
76
+ {
77
+ "cell_type": "code",
78
+ "execution_count": 4,
79
+ "id": "9e610dec",
80
+ "metadata": {},
81
+ "outputs": [
82
+ {
83
+ "data": {
84
+ "image/png": "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",
85
+ "text/plain": [
86
+ "<Figure size 1000x350 with 1 Axes>"
87
+ ]
88
+ },
89
+ "metadata": {},
90
+ "output_type": "display_data"
91
+ }
92
+ ],
93
+ "source": [
94
+ "csv_file = current_dir.parent / \"data/CSV/Models/Civi_models.csv\"\n",
95
+ "out_file = current_dir.parent / \"plots/Figure_12.svg\"\n",
96
+ "sortByFrequency_model_types_csv(csv_file, out_file)"
97
+ ]
98
+ },
99
+ {
100
+ "cell_type": "code",
101
+ "execution_count": null,
102
+ "id": "e91e903f",
103
+ "metadata": {},
104
+ "outputs": [],
105
+ "source": []
106
+ }
107
+ ],
108
+ "metadata": {
109
+ "kernelspec": {
110
+ "display_name": "latm",
111
+ "language": "python",
112
+ "name": "python3"
113
+ },
114
+ "language_info": {
115
+ "codemirror_mode": {
116
+ "name": "ipython",
117
+ "version": 3
118
+ },
119
+ "file_extension": ".py",
120
+ "mimetype": "text/x-python",
121
+ "name": "python",
122
+ "nbconvert_exporter": "python",
123
+ "pygments_lexer": "ipython3",
124
+ "version": "3.10.15"
125
+ }
126
+ },
127
+ "nbformat": 4,
128
+ "nbformat_minor": 5
129
+ }
jupyter_notebooks/SuppM_Figure_S13_Danbooru_Taxonomy.ipynb ADDED
@@ -0,0 +1,1848 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "68a6e559-6ef4-4c7f-8736-93bc8588e3bc",
6
+ "metadata": {},
7
+ "source": [
8
+ "# \"Danbooru\" Taxonomy"
9
+ ]
10
+ },
11
+ {
12
+ "cell_type": "markdown",
13
+ "id": "36aff030",
14
+ "metadata": {},
15
+ "source": [
16
+ "### Getting Tags and Categories from Danbooru.donmai.us"
17
+ ]
18
+ },
19
+ {
20
+ "cell_type": "code",
21
+ "execution_count": 14,
22
+ "id": "a97e3a64-5990-44a9-809d-45030e85f161",
23
+ "metadata": {
24
+ "execution": {
25
+ "iopub.execute_input": "2025-03-17T12:48:07.782534Z",
26
+ "iopub.status.busy": "2025-03-17T12:48:07.781679Z",
27
+ "iopub.status.idle": "2025-03-17T12:48:07.786016Z",
28
+ "shell.execute_reply": "2025-03-17T12:48:07.785465Z",
29
+ "shell.execute_reply.started": "2025-03-17T12:48:07.782509Z"
30
+ }
31
+ },
32
+ "outputs": [],
33
+ "source": [
34
+ "from pathlib import Path\n",
35
+ "import subprocess\n",
36
+ "import json\n",
37
+ "import urllib.parse"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "markdown",
42
+ "id": "97917f01-8f5b-4aa7-8869-9a7d853843d0",
43
+ "metadata": {},
44
+ "source": [
45
+ "#### Paths credentials"
46
+ ]
47
+ },
48
+ {
49
+ "cell_type": "code",
50
+ "execution_count": 2,
51
+ "id": "1f50a910-66c2-472a-8027-dd589b6ae755",
52
+ "metadata": {
53
+ "execution": {
54
+ "iopub.execute_input": "2025-03-17T12:48:08.799790Z",
55
+ "iopub.status.busy": "2025-03-17T12:48:08.798840Z",
56
+ "iopub.status.idle": "2025-03-17T12:48:08.905544Z",
57
+ "shell.execute_reply": "2025-03-17T12:48:08.904995Z",
58
+ "shell.execute_reply.started": "2025-03-17T12:48:08.799770Z"
59
+ }
60
+ },
61
+ "outputs": [],
62
+ "source": [
63
+ "current_dir = Path.cwd()"
64
+ ]
65
+ },
66
+ {
67
+ "cell_type": "code",
68
+ "execution_count": 6,
69
+ "id": "a0f846e3-1fcc-4839-a459-c974f9c46d06",
70
+ "metadata": {
71
+ "execution": {
72
+ "iopub.execute_input": "2025-03-17T12:48:09.490977Z",
73
+ "iopub.status.busy": "2025-03-17T12:48:09.490799Z",
74
+ "iopub.status.idle": "2025-03-17T12:48:09.701470Z",
75
+ "shell.execute_reply": "2025-03-17T12:48:09.700912Z",
76
+ "shell.execute_reply.started": "2025-03-17T12:48:09.490961Z"
77
+ }
78
+ },
79
+ "outputs": [],
80
+ "source": [
81
+ "api_key = (current_dir.parent / \"misc/credentials/api-key_danbooru\").read_text().strip()\n",
82
+ "username = (current_dir.parent / \"misc/credentials/username_danbooru\").read_text().strip()"
83
+ ]
84
+ },
85
+ {
86
+ "cell_type": "markdown",
87
+ "id": "c4c2290f-7705-4061-9b29-e1b74da732b8",
88
+ "metadata": {
89
+ "execution": {
90
+ "iopub.execute_input": "2025-03-12T20:39:34.470218Z",
91
+ "iopub.status.busy": "2025-03-12T20:39:34.469746Z",
92
+ "iopub.status.idle": "2025-03-12T20:39:34.472498Z",
93
+ "shell.execute_reply": "2025-03-12T20:39:34.472084Z",
94
+ "shell.execute_reply.started": "2025-03-12T20:39:34.470199Z"
95
+ }
96
+ },
97
+ "source": [
98
+ "### Query danbooru.donmai.us for a single Tag"
99
+ ]
100
+ },
101
+ {
102
+ "cell_type": "code",
103
+ "execution_count": 7,
104
+ "id": "00ffaf5f-a4ae-408a-8a7c-e6b174f091f8",
105
+ "metadata": {
106
+ "execution": {
107
+ "iopub.execute_input": "2025-03-17T12:48:11.712064Z",
108
+ "iopub.status.busy": "2025-03-17T12:48:11.711866Z",
109
+ "iopub.status.idle": "2025-03-17T12:48:20.143088Z",
110
+ "shell.execute_reply": "2025-03-17T12:48:20.142318Z",
111
+ "shell.execute_reply.started": "2025-03-17T12:48:11.712047Z"
112
+ }
113
+ },
114
+ "outputs": [
115
+ {
116
+ "name": "stdout",
117
+ "output_type": "stream",
118
+ "text": [
119
+ "🚀 Fetching tag details for 'wombat'...\n",
120
+ "🔎 Running cURL: curl -s -L --user parodyofsomething:FkzGApb17bfJayJMqKTzeyTw -X GET https://danbooru.donmai.us/tags.json?search%5Bname%5D=wombat&only=id,name,category,post_count,is_deprecated,created_at,updated_at,wiki_page,artist,antecedent_alias,consequent_aliases,antecedent_implications,consequent_implications,dtext_links\n",
121
+ "✅ Test complete! Data saved to `/home/lauhp/000_PHD/000_010_PUBLICATION/2025_SAGE/CODE/pm-paper_uzh_gitlab/pm-paper/misc/danbooru/tag_info_test/wombat_tag_info.json`.\n"
122
+ ]
123
+ }
124
+ ],
125
+ "source": [
126
+ "output_dir = current_dir.parent / \"misc\" / \"danbooru\" / \"tag_info_test\"\n",
127
+ "output_dir.mkdir(parents=True, exist_ok=True)\n",
128
+ "\n",
129
+ "# Get the tag to test\n",
130
+ "tag_name = input(\"Enter a single tag to test: \").strip()\n",
131
+ "if not tag_name:\n",
132
+ " print(\"❌ ERROR: No tag entered.\")\n",
133
+ " exit()\n",
134
+ "\n",
135
+ "# Encode the tag name properly\n",
136
+ "encoded_tag = urllib.parse.quote(tag_name, safe=\"\") # Encode everything\n",
137
+ "\n",
138
+ "# API endpoints with correct encoding\n",
139
+ "BASE_URL = \"https://danbooru.donmai.us\"\n",
140
+ "#TAGS_API = f\"{BASE_URL}/tags.json?search%5Bname%5D={encoded_tag}\"\n",
141
+ "TAGS_API = f\"{BASE_URL}/tags.json?search%5Bname%5D={encoded_tag}&only=id,name,category,post_count,is_deprecated,created_at,updated_at,wiki_page,artist,antecedent_alias,consequent_aliases,antecedent_implications,consequent_implications,dtext_links\"\n",
142
+ "IMPLICATIONS_API = f\"{BASE_URL}/tag_implications.json?search%5Bantecedent_name%5D={encoded_tag}\"\n",
143
+ "ALIASES_API = f\"{BASE_URL}/tag_aliases.json?search%5Bantecedent_name%5D={encoded_tag}\"\n",
144
+ "WIKI_API = f\"{BASE_URL}/wiki_pages.json?search%5Btitle%5D={encoded_tag}\"\n",
145
+ "\n",
146
+ "# Function to run cURL command properly\n",
147
+ "def fetch_data(api_url, description):\n",
148
+ " print(f\"🚀 Fetching {description} for '{tag_name}'...\")\n",
149
+ "\n",
150
+ " # Build the cURL command\n",
151
+ " curl_command = [\n",
152
+ " \"curl\", \"-s\", \"-L\", # Silent & follow redirects\n",
153
+ " \"--user\", f\"{username}:{api_key}\", # Auth\n",
154
+ " \"-X\", \"GET\",\n",
155
+ " api_url\n",
156
+ " ]\n",
157
+ "\n",
158
+ " print(f\"🔎 Running cURL: {' '.join(curl_command)}\") # Show the command\n",
159
+ "\n",
160
+ " try:\n",
161
+ " result = subprocess.run(curl_command, capture_output=True, text=True, check=True)\n",
162
+ " if result.returncode == 0:\n",
163
+ " return json.loads(result.stdout) if result.stdout else None\n",
164
+ " else:\n",
165
+ " print(f\"⚠️ cURL failed (status {result.returncode}): {result.stderr}\")\n",
166
+ " return None\n",
167
+ " except subprocess.CalledProcessError as e:\n",
168
+ " print(f\"⚠️ cURL error: {e}\")\n",
169
+ " return None\n",
170
+ " except json.JSONDecodeError:\n",
171
+ " print(f\"⚠️ Failed to parse JSON: {result.stdout}\")\n",
172
+ " return None\n",
173
+ "\n",
174
+ "# Fetch tag details\n",
175
+ "tag_info = {\n",
176
+ " \"tag_details\": fetch_data(TAGS_API, \"tag details\"),\n",
177
+ " #\"implications\": fetch_data(IMPLICATIONS_API, \"tag implications\"),\n",
178
+ " #\"aliases\": fetch_data(ALIASES_API, \"tag aliases\"),\n",
179
+ " #\"wiki\": fetch_data(WIKI_API, \"wiki information\"),\n",
180
+ "}\n",
181
+ "\n",
182
+ "# Save to JSON\n",
183
+ "output_file = output_dir / f\"{tag_name}_tag_info.json\"\n",
184
+ "with open(output_file, \"w\", encoding=\"utf-8\") as f:\n",
185
+ " json.dump(tag_info, f, indent=4, ensure_ascii=False)\n",
186
+ "\n",
187
+ "print(f\"✅ Test complete! Data saved to `{output_file}`.\")\n"
188
+ ]
189
+ },
190
+ {
191
+ "cell_type": "markdown",
192
+ "id": "75a28e4c-e06f-4578-a338-4f0014953302",
193
+ "metadata": {},
194
+ "source": [
195
+ "## Hierarchy"
196
+ ]
197
+ },
198
+ {
199
+ "cell_type": "code",
200
+ "execution_count": 8,
201
+ "id": "1a58f2d1-909b-41c0-bcb8-0a55e6835f53",
202
+ "metadata": {
203
+ "execution": {
204
+ "iopub.execute_input": "2025-03-17T12:48:42.365809Z",
205
+ "iopub.status.busy": "2025-03-17T12:48:42.365447Z",
206
+ "iopub.status.idle": "2025-03-17T12:48:42.376305Z",
207
+ "shell.execute_reply": "2025-03-17T12:48:42.375752Z",
208
+ "shell.execute_reply.started": "2025-03-17T12:48:42.365788Z"
209
+ }
210
+ },
211
+ "outputs": [],
212
+ "source": [
213
+ "manual_hierarchy = {\n",
214
+ " \"subject\": {\n",
215
+ " \"female\": {\n",
216
+ " \"female_general\": [\"woman\", \"girl\", \"1girl\", \"2girls\", \"3girls\", \"4girls\", \"5girls\", \"6+girls\", \"multiple girls\"]\n",
217
+ " },\n",
218
+ " \"male\": {\n",
219
+ " \"male_general\": [\"man\", \"boy\", \"1boy\", \"2boys\", \"3boys\", \"4boys\", \"5boys\", \"6+boys\", \"multiple boys\"]\n",
220
+ " },\n",
221
+ " \"koma\": {\n",
222
+ " \"koma_general\": [\"1koma\", \"2koma\"]\n",
223
+ " },\n",
224
+ " \"anthro\": {\n",
225
+ " \"anthro_general\": [\"cat girl\", \"fox girl\", \"dog girl\", \"cat boy\", \"furry\"]\n",
226
+ " }\n",
227
+ " },\n",
228
+ "\n",
229
+ " \"visual_characteristics\": {\n",
230
+ " \"image_composition_and_style\": {\n",
231
+ " \"artistic_license\": [],\n",
232
+ " \"image_composition\": {\n",
233
+ " \"backgrounds\": [],\n",
234
+ " \"censorship\": [],\n",
235
+ " \"colors\": [],\n",
236
+ " \"focus_tags\": [],\n",
237
+ " \"lighting\": [],\n",
238
+ " \"prints\": []\n",
239
+ " },\n",
240
+ " \"patterns\": [],\n",
241
+ " \"symbols\": [],\n",
242
+ " \"text\": [],\n",
243
+ " \"year_tags\": []\n",
244
+ " }\n",
245
+ " },\n",
246
+ " \"body\": {\n",
247
+ " \"body_parts\": {\n",
248
+ " \"ass\": [],\n",
249
+ " \"breasts_tags\": [],\n",
250
+ " \"face_tags\": {\n",
251
+ " \"eyes_tags\": []\n",
252
+ " },\n",
253
+ " \"ears_tags\": [],\n",
254
+ " \"hair\": {\n",
255
+ " \"hair_color\": [],\n",
256
+ " \"hair_styles\": []\n",
257
+ " },\n",
258
+ " \"hands\": {\n",
259
+ " \"gestures\": []\n",
260
+ " },\n",
261
+ " \"neck_and_neckwear\": [],\n",
262
+ " \"posture\": [],\n",
263
+ " \"pussy\": [],\n",
264
+ " \"penis\": [],\n",
265
+ " \"shoulders\": [],\n",
266
+ " \"skin_color\": [],\n",
267
+ " \"tail\": [],\n",
268
+ " \"wings\": []\n",
269
+ " },\n",
270
+ " \"injury\": []\n",
271
+ " },\n",
272
+ " \"attire_and_body_accessories\": {\n",
273
+ " \"attire\": {\n",
274
+ " \"dress\": [],\n",
275
+ " \"handwear\": [],\n",
276
+ " \"headwear\": [],\n",
277
+ " \"legwear\": [],\n",
278
+ " \"neck_and_neckwear\": [],\n",
279
+ " \"sexual_attire\": {\n",
280
+ " \"bra\": [],\n",
281
+ " \"panties\": []\n",
282
+ " },\n",
283
+ " \"sleeves\": [],\n",
284
+ " \"swimsuit\": []\n",
285
+ " },\n",
286
+ " \"embellishment\": [],\n",
287
+ " \"eyewear\": [],\n",
288
+ " \"fashion_style\": [],\n",
289
+ " \"nudity\": []\n",
290
+ " },\n",
291
+ " \"sex\": {\n",
292
+ " \"sex_acts\": {\n",
293
+ " \"simulated_sex_acts\": []\n",
294
+ " },\n",
295
+ " \"sexual_positions\": []\n",
296
+ " },\n",
297
+ " \"objects\": {\n",
298
+ " \"computer\": [],\n",
299
+ " \"airplanes\": [],\n",
300
+ " \"armor\": [],\n",
301
+ " \"ground_vehicles\": [],\n",
302
+ " \"helicopters\": [],\n",
303
+ " \"pokemon_objects\": [],\n",
304
+ " \"ships\": [],\n",
305
+ " \"weapons\": [],\n",
306
+ " \"audio_tags\": [],\n",
307
+ " \"cards\": {\n",
308
+ " \"playing_card_faces\": []\n",
309
+ " },\n",
310
+ " \"eyewear\": [],\n",
311
+ " \"piercings\": [],\n",
312
+ " \"sex_objects\": []\n",
313
+ " },\n",
314
+ " \"creatures\": {\n",
315
+ " \"animals\": {\n",
316
+ " \"birds\": [],\n",
317
+ " \"cats\": [],\n",
318
+ " \"dogs\": []\n",
319
+ " },\n",
320
+ " \"legendary_creatures\": []\n",
321
+ " },\n",
322
+ " \"plants\": {\n",
323
+ " \"plant\": {\n",
324
+ " \"tree\": []\n",
325
+ " },\n",
326
+ " \"flowers\": []\n",
327
+ " },\n",
328
+ " \"games\": {\n",
329
+ " \"game_activities\": [],\n",
330
+ " \"board_games\": [],\n",
331
+ " \"sports\": [],\n",
332
+ " \"video_game\": []\n",
333
+ " },\n",
334
+ " \"real_world\": {\n",
335
+ " \"companies_and_brand_names\": [],\n",
336
+ " \"holidays_and_celebrations\": [],\n",
337
+ " \"jobs\": [],\n",
338
+ " \"locations\": [],\n",
339
+ " \"people\": [],\n",
340
+ " \"real_world_locations\": []\n",
341
+ " },\n",
342
+ " \"more\": {\n",
343
+ " \"dances\": [],\n",
344
+ " \"family_relationships\": [],\n",
345
+ " \"food_tags\": [],\n",
346
+ " \"fire\": [],\n",
347
+ " \"groups\": [],\n",
348
+ " \"phrases\": [],\n",
349
+ " \"scan\": [],\n",
350
+ " \"subjective\": [],\n",
351
+ " \"technology\": [],\n",
352
+ " \"verbs_and_gerunds\": [],\n",
353
+ " \"water\": [],\n",
354
+ " \"disambiguation_pages\": [],\n",
355
+ " \"magazine_publications\": [],\n",
356
+ " \"special_moves\": [],\n",
357
+ " \"uniforms\": [],\n",
358
+ " \"pokemon_media\": [],\n",
359
+ " \"tagged_songs\": [],\n",
360
+ " \"vocaloid_derivatives\": [],\n",
361
+ " \"vocaloid_songs\": [],\n",
362
+ " \"vocal_synthesizers\": [],\n",
363
+ " \"vocal_synth_derivatives\": [],\n",
364
+ " \"vocal_synth_songs\": [],\n",
365
+ " \"deemo_songs\": []\n",
366
+ " },\n",
367
+ " \"copyrights_artists_projects_and_media\": {\n",
368
+ " \"genres_of_video_games\": {\n",
369
+ " \"fighting_games\": [],\n",
370
+ " \"platform_games\": [],\n",
371
+ " \"role-playing_games\": [],\n",
372
+ " \"shooter_games\": [],\n",
373
+ " \"visual_novel_games\": []\n",
374
+ " },\n",
375
+ " \"artists\": {\n",
376
+ " \"named_drawfags\": [],\n",
377
+ " \"pixiv_projects\": []\n",
378
+ " }\n",
379
+ " },\n",
380
+ " \"characters\": {\n",
381
+ " \"ace_attorney\": [],\n",
382
+ " \"arknights\": [],\n",
383
+ " \"atelier\": [],\n",
384
+ " \"azur_lane\": [],\n",
385
+ " \"bleach\": [],\n",
386
+ " \"bokujou_monogatari\": [],\n",
387
+ " \"brave_girl_ravens\": [],\n",
388
+ " \"cardcaptor_sakura\": [],\n",
389
+ " \"danganronpa\": [],\n",
390
+ " \"digimon\": {\n",
391
+ " \"digimon_characters\": []\n",
392
+ " },\n",
393
+ " \"dragon_ball\": [],\n",
394
+ " \"dragon_quest\": [],\n",
395
+ " \"fate_series\": [],\n",
396
+ " \"final_fantasy\": [],\n",
397
+ " \"fire_emblem\": [],\n",
398
+ " \"flower_knight_girl\": [],\n",
399
+ " \"gensou_suikoden\": [],\n",
400
+ " \"girls_frontline\": [],\n",
401
+ " \"girls_und_panzer\": [],\n",
402
+ " \"gundam_mechas\": [],\n",
403
+ " \"hunter_x_hunter\": [],\n",
404
+ " \"jojo_no_kimyou_na_bouken\": [],\n",
405
+ " \"kamen_rider\": [],\n",
406
+ " \"kantai_collection\": [],\n",
407
+ " \"kingdom_hearts\": [],\n",
408
+ " \"mahou_sensei_negima\": [],\n",
409
+ " \"meitantei_conan\": [],\n",
410
+ " \"minecraft\": [],\n",
411
+ " \"naruto\": [],\n",
412
+ " \"nippon_ichi\": [],\n",
413
+ " \"one_piece\": [],\n",
414
+ " \"oshiro_project\": [],\n",
415
+ " \"pokemon\": {\n",
416
+ " \"elite_four_members\": [],\n",
417
+ " \"gym_leaders\": [],\n",
418
+ " \"families_of_pokemon_main_characters\": [],\n",
419
+ " \"pokemon_ranger_characters\": [],\n",
420
+ " \"pokemon_trainer_classes\": []\n",
421
+ " },\n",
422
+ " \"pretty_cure\": [],\n",
423
+ " \"ragnarok_online\": [],\n",
424
+ " \"rosenkreuzstilette\": [],\n",
425
+ " \"sailor_moon\": [],\n",
426
+ " \"street_fighter\": [],\n",
427
+ " \"super_smash_bros\": [],\n",
428
+ " \"world_witches_series\": [],\n",
429
+ " \"tales_of\": [],\n",
430
+ " \"toaru_majutsu_no_index\": [],\n",
431
+ " \"touhou\": [],\n",
432
+ " \"touken_ranbu\": [],\n",
433
+ " \"ultra_series\": [],\n",
434
+ " \"umamusume\": [],\n",
435
+ " \"vocaloid\": [],\n",
436
+ " \"yu_gi_oh\": [],\n",
437
+ " \"genderswap_characters\": [],\n",
438
+ " \"official_mascots\": [],\n",
439
+ " \"real_life_racehorses\": []\n",
440
+ " },\n",
441
+ " \"metatags\": {\n",
442
+ " \"tag_group_metatags\": [],\n",
443
+ " \"drawing_software\": []\n",
444
+ " }\n",
445
+ "}\n"
446
+ ]
447
+ },
448
+ {
449
+ "cell_type": "markdown",
450
+ "id": "824760af-a568-47f8-b076-b30e8606b9ac",
451
+ "metadata": {},
452
+ "source": [
453
+ "## Tag Groups"
454
+ ]
455
+ },
456
+ {
457
+ "cell_type": "code",
458
+ "execution_count": 9,
459
+ "id": "557cdd69-f6b1-4c33-9f67-a13b00c0146b",
460
+ "metadata": {
461
+ "execution": {
462
+ "iopub.execute_input": "2025-03-17T12:48:43.777297Z",
463
+ "iopub.status.busy": "2025-03-17T12:48:43.776934Z",
464
+ "iopub.status.idle": "2025-03-17T12:48:43.787862Z",
465
+ "shell.execute_reply": "2025-03-17T12:48:43.787288Z",
466
+ "shell.execute_reply.started": "2025-03-17T12:48:43.777279Z"
467
+ }
468
+ },
469
+ "outputs": [],
470
+ "source": [
471
+ "tag_groups = {\n",
472
+ " # **Subjects (Humans, Anthro, etc.)**\n",
473
+ " \"subject\": \"subject\",\n",
474
+ " \"subject.female\": \"female\",\n",
475
+ " \"subject.female_general\": \"female_general\", # Added\n",
476
+ " \"subject.female.1girl\": \"1girl\",\n",
477
+ " \"subject.female.2girls\": \"2girls\",\n",
478
+ "\n",
479
+ "\n",
480
+ " # **Male Subjects**\n",
481
+ " \"subject.male\": \"male\",\n",
482
+ " \"subject.male.1boy\": \"1boy\",\n",
483
+ " \"subject.male.2boys\": \"2boys\",\n",
484
+ " \"subject.male.3boys\": \"3boys\",\n",
485
+ " \"subject.male.4boys\": \"4boys\",\n",
486
+ " \"subject.male.5boys\": \"5boys\",\n",
487
+ " \"subject.male.6+boys\": \"6+boys\",\n",
488
+ " \"subject.male.multiple_boys\": \"multiple_boys\",\n",
489
+ " \"subject.male.man\": \"man\",\n",
490
+ " \"subject.male.boy\": \"boy\",\n",
491
+ "\n",
492
+ " # **Koma (Manga Panel Counts)**\n",
493
+ " \"subject.koma\": \"koma\",\n",
494
+ " \"subject.koma.1koma\": \"1koma\",\n",
495
+ " \"subject.koma.2koma\": \"2koma\",\n",
496
+ "\n",
497
+ " # **Anthropomorphic Characters**\n",
498
+ " \"subject.anthro\": \"anthro\",\n",
499
+ " \"subject.anthro.cat_girl\": \"cat_girl\",\n",
500
+ " \"subject.anthro.fox_girl\": \"fox_girl\",\n",
501
+ " \"subject.anthro.dog_girl\": \"dog_girl\",\n",
502
+ " \"subject.anthro.cat_boy\": \"cat_boy\",\n",
503
+ " \"subject.anthro.furry\": \"furry\",\n",
504
+ "\n",
505
+ " # **Visual Characteristics**\n",
506
+ " \"visual_characteristics\": \"visual_characteristics\",\n",
507
+ " \"visual_characteristics.image_composition_and_style\": \"image_composition_and_style\",\n",
508
+ " \"visual_characteristics.image_composition_and_style.artistic_license\": \"artistic_license\",\n",
509
+ " \"visual_characteristics.image_composition_and_style.image_composition\": \"image_composition\",\n",
510
+ " \"visual_characteristics.image_composition_and_style.image_composition.backgrounds\": \"backgrounds\",\n",
511
+ " \"visual_characteristics.image_composition_and_style.image_composition.censorship\": \"censorship\",\n",
512
+ " \"visual_characteristics.image_composition_and_style.image_composition.colors\": \"colors\",\n",
513
+ " \"visual_characteristics.image_composition_and_style.image_composition.focus_tags\": \"focus_tags\",\n",
514
+ " \"visual_characteristics.image_composition_and_style.image_composition.lighting\": \"lighting\",\n",
515
+ " \"visual_characteristics.image_composition_and_style.image_composition.prints\": \"prints\",\n",
516
+ " \"visual_characteristics.image_composition_and_style.image_composition.style_parodies\": \"style_parodies\",\n",
517
+ " \"visual_characteristics.image_composition_and_style.patterns\": \"patterns\",\n",
518
+ " \"visual_characteristics.image_composition_and_style.symbols\": \"symbols\",\n",
519
+ " \"visual_characteristics.image_composition_and_style.text\": \"text\",\n",
520
+ " \"visual_characteristics.image_composition_and_style.year_tags\": \"year_tags\",\n",
521
+ "\n",
522
+ "\n",
523
+ " # Body\n",
524
+ " \"body\": \"body\",\n",
525
+ " \"body.body_parts\": \"body_parts\",\n",
526
+ " \"body.body_parts.ass\": \"ass\",\n",
527
+ " \"body.body_parts.breasts_tags\": \"breasts_tags\",\n",
528
+ " \"body.body_parts.face_tags\": \"face_tags\",\n",
529
+ " \"body.body_parts.face_tags.eyes_tags\": \"eyes_tags\",\n",
530
+ " \"body.body_parts.ears_tags\": \"ears_tags\",\n",
531
+ " \"body.body_parts.hair\": \"hair\",\n",
532
+ " \"body.body_parts.hair.hair_color\": \"hair_color\",\n",
533
+ " \"body.body_parts.hair.hair_styles\": \"hair_styles\",\n",
534
+ " \"body.body_parts.hands\": \"hands\",\n",
535
+ " \"body.body_parts.hands.gestures\": \"gestures\",\n",
536
+ " \"body.body_parts.neck_and_neckwear\": \"neck_and_neckwear\",\n",
537
+ " \"body.body_parts.posture\": \"posture\",\n",
538
+ " \"body.body_parts.pussy\": \"pussy\",\n",
539
+ " \"body.body_parts.penis\": \"penis\",\n",
540
+ " \"body.body_parts.shoulders\": \"shoulders\",\n",
541
+ " \"body.body_parts.skin_color\": \"skin_color\",\n",
542
+ " \"body.body_parts.tail\": \"tail\",\n",
543
+ " \"body.body_parts.wings\": \"wings\",\n",
544
+ " \"body.injury\": \"injury\",\n",
545
+ "\n",
546
+ " # Attire & Accessories\n",
547
+ " \"attire_and_body_accessories\": \"attire_and_body_accessories\",\n",
548
+ " \"attire_and_body_accessories.attire\": \"attire\",\n",
549
+ " \"attire_and_body_accessories.attire.dress\": \"dress\",\n",
550
+ " \"attire_and_body_accessories.attire.handwear\": \"handwear\",\n",
551
+ " \"attire_and_body_accessories.attire.headwear\": \"headwear\",\n",
552
+ " \"attire_and_body_accessories.attire.legwear\": \"legwear\",\n",
553
+ " \"attire_and_body_accessories.attire.mask\": \"mask\",\n",
554
+ " \"attire_and_body_accessories.attire.neck_and_neckwear\": \"neck_and_neckwear\",\n",
555
+ " \"attire_and_body_accessories.attire.sexual_attire\": \"sexual_attire\",\n",
556
+ " \"attire_and_body_accessories.attire.sexual_attire.bra\": \"bra\",\n",
557
+ " \"attire_and_body_accessories.attire.sexual_attire.panties\": \"panties\",\n",
558
+ " \"attire_and_body_accessories.attire.sleeves\": \"sleeves\",\n",
559
+ " \"attire_and_body_accessories.attire.swimsuit\": \"swimsuit\",\n",
560
+ " \"attire_and_body_accessories.embellishment\": \"embellishment\",\n",
561
+ " \"attire_and_body_accessories.eyewear\": \"eyewear\",\n",
562
+ " \"attire_and_body_accessories.fashion_style\": \"fashion_style\",\n",
563
+ " \"attire_and_body_accessories.nudity\": \"nudity\",\n",
564
+ "\n",
565
+ " # Sex\n",
566
+ " \"sex\": \"sex\",\n",
567
+ " \"sex.sex_acts\": \"sex_acts\",\n",
568
+ " \"sex.sex_acts.simulated_sex_acts\": \"simulated_sex_acts\",\n",
569
+ " \"sex.sexual_positions\": \"sexual_positions\",\n",
570
+ "\n",
571
+ " # Objects\n",
572
+ " \"objects\": \"objects\",\n",
573
+ " \"objects.computer\": \"computer\",\n",
574
+ " \"objects.airplanes\": \"airplanes\",\n",
575
+ " \"objects.armor\": \"armor\",\n",
576
+ " \"objects.ground_vehicles\": \"ground_vehicles\",\n",
577
+ " \"objects.helicopters\": \"helicopters\",\n",
578
+ " \"objects.pokemon_objects\": \"pokemon_objects\",\n",
579
+ " \"objects.ships\": \"ships\",\n",
580
+ " \"objects.weapons\": \"weapons\",\n",
581
+ " \"objects.audio_tags\": \"audio_tags\",\n",
582
+ " \"objects.cards\": \"cards\",\n",
583
+ " \"objects.cards.playing_card_faces\": \"playing_card_faces\",\n",
584
+ " \"objects.eyewear\": \"eyewear\",\n",
585
+ " \"objects.piercings\": \"piercings\",\n",
586
+ " \"objects.sex_objects\": \"sex_objects\",\n",
587
+ "\n",
588
+ " # Creatures\n",
589
+ " \"creatures\": \"creatures\",\n",
590
+ " \"creatures.animals\": \"animals\",\n",
591
+ " \"creatures.animals.birds\": \"birds\",\n",
592
+ " \"creatures.animals.cats\": \"cats\",\n",
593
+ " \"creatures.animals.dogs\": \"dogs\",\n",
594
+ " \"creatures.legendary_creatures\": \"legendary_creatures\",\n",
595
+ "\n",
596
+ " # Plants\n",
597
+ " \"plants\": \"plant\",\n",
598
+ " \"plant.plant\": \"plant\",\n",
599
+ " \"plant.tree\": \"tree\",\n",
600
+ " \"plant.flowers\": \"flowers\",\n",
601
+ "\n",
602
+ " # Games\n",
603
+ " \"games\": \"games\",\n",
604
+ " \"games.game_activities\": \"game_activities\",\n",
605
+ " \"games.board_games\": \"board_games\",\n",
606
+ " \"games.sports\": \"sports\",\n",
607
+ " \"games.video_game\": \"video_game\",\n",
608
+ " \"games.fighting_games\": \"fighting_games\",\n",
609
+ "\n",
610
+ " # Real World\n",
611
+ " \"real_world\": \"real_world\",\n",
612
+ " \"real_world.companies_and_brand_names\": \"companies_and_brand_names\",\n",
613
+ " \"real_world.holidays_and_celebrations\": \"holidays_and_celebrations\",\n",
614
+ " \"real_world.jobs\": \"jobs\",\n",
615
+ " \"real_world.locations\": \"locations\",\n",
616
+ " \"real_world.people\": \"people\",\n",
617
+ " \"real_world.real_world_locations\": \"real_world_locations\",\n",
618
+ "\n",
619
+ " # More Categories\n",
620
+ " \"more\": \"more\",\n",
621
+ " \"more.dances\": \"dances\",\n",
622
+ " \"more.family_relationships\": \"family_relationships\",\n",
623
+ " \"more.food_tags\": \"food_tags\",\n",
624
+ " \"more.fire\": \"fire\",\n",
625
+ " \"more.groups\": \"groups\",\n",
626
+ " \"more.phrases\": \"phrases\",\n",
627
+ " \"more.scan\": \"scan\",\n",
628
+ " \"more.subjective\": \"subjective\",\n",
629
+ " \"more.technology\": \"technology\",\n",
630
+ " \"more.verbs_and_gerunds\": \"verbs_and_gerunds\",\n",
631
+ " \"more.water\": \"water\",\n",
632
+ "\n",
633
+ " # Genres of Video Games\n",
634
+ " \"copyrights_artists_projects_and_media\": \"copyrights_artists_projects_and_media\",\n",
635
+ " \"copyrights_artists_projects_and_media.genres_of_video_games\": \"genres_of_video_games\",\n",
636
+ " \"copyrights_artists_projects_and_media.genres_of_video_games.fighting_games\": \"fighting_games\",\n",
637
+ " \"copyrights_artists_projects_and_media.genres_of_video_games.platform_games\": \"platform_games\",\n",
638
+ " \"copyrights_artists_projects_and_media.genres_of_video_games.role-playing_games\": \"role-playing_games\",\n",
639
+ " \"copyrights_artists_projects_and_media.genres_of_video_games.shooter_games\": \"shooter_games\",\n",
640
+ " \"copyrights_artists_projects_and_media.genres_of_video_games.visual_novel_games\": \"visual_novel_games\",\n",
641
+ "\n",
642
+ " \"characters\": \"characters\",\n",
643
+ " \"characters.ace_attorney\": \"ace_attorney_characters\",\n",
644
+ " \"characters.arknights\": \"arknights_characters\",\n",
645
+ " \"characters.atelier\": \"atelier_characters\",\n",
646
+ " \"characters.azur_lane\": \"azur_lane_characters\",\n",
647
+ " \"characters.bleach\": \"bleach_characters\",\n",
648
+ " \"characters.bokujou_monogatari\": \"bokujou_monogatari_characters\",\n",
649
+ " \"characters.brave_girl_ravens\": \"brave_girl_ravens_characters\",\n",
650
+ " \"characters.cardcaptor_sakura\": \"cardcaptor_sakura_characters\",\n",
651
+ " \"characters.danganronpa\": \"danganronpa_characters\",\n",
652
+ " \"characters.digimon\": \"digimon\",\n",
653
+ " \"characters.digimon.digimon_characters\": \"digimon_characters\",\n",
654
+ " \"characters.dragon_ball\": \"dragon_ball_characters\",\n",
655
+ " \"characters.dragon_quest\": \"dragon_quest_characters\",\n",
656
+ " \"characters.fate_series\": \"fate_series_characters\",\n",
657
+ " \"characters.final_fantasy\": \"final_fantasy_characters\",\n",
658
+ " \"characters.fire_emblem\": \"fire_emblem_characters\",\n",
659
+ " \"characters.flower_knight_girl\": \"flower_knight_girl_characters\",\n",
660
+ " \"characters.gensou_suikoden\": \"gensou_suikoden_characters\",\n",
661
+ " \"characters.girls_frontline\": \"girls_frontline_characters\",\n",
662
+ " \"characters.girls_und_panzer\": \"girls_und_panzer_characters\",\n",
663
+ " \"characters.gundam_mechas\": \"gundam_mechas\",\n",
664
+ " \"characters.hunter_x_hunter\": \"hunter_x_hunter_characters\",\n",
665
+ " \"characters.jojo_no_kimyou_na_bouken\": \"jojo_no_kimyou_na_bouken_characters\",\n",
666
+ " \"characters.kamen_rider\": \"kamen_rider_characters\",\n",
667
+ " \"characters.kantai_collection\": \"kantai_collection_characters\",\n",
668
+ " \"characters.kingdom_hearts\": \"kingdom_hearts_characters\",\n",
669
+ " \"characters.mahou_sensei_negima\": \"mahou_sensei_negima_characters\",\n",
670
+ " \"characters.meitantei_conan\": \"meitantei_conan_characters\",\n",
671
+ " \"characters.minecraft\": \"minecraft_characters\",\n",
672
+ " \"characters.naruto\": \"naruto_characters\",\n",
673
+ " \"characters.nippon_ichi\": \"nippon_ichi_characters\",\n",
674
+ " \"characters.one_piece\": \"one_piece_characters\",\n",
675
+ " \"characters.oshiro_project\": \"oshiro_project_characters\",\n",
676
+ " \"characters.pokemon\": \"pokemon\",\n",
677
+ " \"characters.pokemon.elite_four_members\": \"elite_four_members\",\n",
678
+ " \"characters.pokemon.gym_leaders\": \"gym_leaders\",\n",
679
+ " \"characters.pokemon.families_of_pokemon_main_characters\": \"families_of_pokemon_main_characters\",\n",
680
+ " \"characters.pokemon.pokemon_ranger_characters\": \"pokemon_ranger_characters\",\n",
681
+ " \"characters.pokemon.pokemon_trainer_classes\": \"pokemon_trainer_classes\",\n",
682
+ " \"characters.pretty_cure\": \"pretty_cure_characters\",\n",
683
+ " \"characters.ragnarok_online\": \"ragnarok_online_characters\",\n",
684
+ " \"characters.rosenkreuzstilette\": \"rosenkreuzstilette_characters\",\n",
685
+ " \"characters.sailor_moon\": \"sailor_moon_characters\",\n",
686
+ " \"characters.street_fighter\": \"street_fighter_characters\",\n",
687
+ " \"characters.super_smash_bros\": \"super_smash_bros_characters\",\n",
688
+ " \"characters.world_witches_series\": \"world_witches_series_characters\",\n",
689
+ " \"characters.tales_of\": \"tales_of_characters\",\n",
690
+ " \"characters.toaru_majutsu_no_index\": \"toaru_majutsu_no_index_characters\",\n",
691
+ " \"characters.touhou\": \"touhou_characters\",\n",
692
+ " \"characters.touken_ranbu\": \"touken_ranbu_characters\",\n",
693
+ " \"characters.ultra_series\": \"ultra_series_characters\",\n",
694
+ " \"characters.umamusume\": \"umamusume_characters\",\n",
695
+ " \"characters.vocaloid\": \"vocaloid_characters\",\n",
696
+ " \"characters.yu_gi_oh\": \"yu_gi_oh_characters\",\n",
697
+ " \"characters.genderswap\": \"genderswap_characters\",\n",
698
+ " \"characters.official_mascots\": \"official_mascots\",\n",
699
+ " \"characters.real_life_racehorses\": \"real_life_racehorses\",\n",
700
+ "\n",
701
+ "\n",
702
+ " # metatags\n",
703
+ " \"metatags\": \"metatags\",\n",
704
+ " \"drawing_software\": \"drawing_software\",\n",
705
+ " \n",
706
+ "}\n"
707
+ ]
708
+ },
709
+ {
710
+ "cell_type": "code",
711
+ "execution_count": 10,
712
+ "id": "c4759995-396a-4ec1-9f35-e75b3875e13c",
713
+ "metadata": {
714
+ "execution": {
715
+ "iopub.execute_input": "2025-03-17T12:48:44.138013Z",
716
+ "iopub.status.busy": "2025-03-17T12:48:44.137852Z",
717
+ "iopub.status.idle": "2025-03-17T12:48:44.491853Z",
718
+ "shell.execute_reply": "2025-03-17T12:48:44.491271Z",
719
+ "shell.execute_reply.started": "2025-03-17T12:48:44.137999Z"
720
+ }
721
+ },
722
+ "outputs": [],
723
+ "source": [
724
+ "list_based_categories = [\n",
725
+ " # Image composition style\n",
726
+ " \"style_parodies\",\n",
727
+ " # Objects\n",
728
+ " \"computer\",\n",
729
+ " \"airplanes\",\n",
730
+ " \"armor\",\n",
731
+ " \"ground_vehicles\",\n",
732
+ " \"helicopters\",\n",
733
+ " \"pokemon_objects\",\n",
734
+ " \"ships\",\n",
735
+ " \"weapons\",\n",
736
+ " \"playing_card_faces\",\n",
737
+ " \n",
738
+ " # Creatures\n",
739
+ " \"animals\",\n",
740
+ " \"birds\",\n",
741
+ " \"cats\",\n",
742
+ " \"dogs\",\n",
743
+ " \"legendary_creatures\",\n",
744
+ "\n",
745
+ " # Plants\n",
746
+ " \"plant\",\n",
747
+ " \"tree\",\n",
748
+ " \"flowers\",\n",
749
+ "\n",
750
+ " # Games\n",
751
+ " \"game_activities\",\n",
752
+ " \"board_games\",\n",
753
+ " \"sports\",\n",
754
+ " \"video_game\",\n",
755
+ " \"fighting_games\",\n",
756
+ " \"platform_games\",\n",
757
+ " #\"role-playing_games\",\n",
758
+ " \"shooter_games\",\n",
759
+ " \"visual_novel_games\",\n",
760
+ "\n",
761
+ " # Real World\n",
762
+ " \"companies_and_brand_names\",\n",
763
+ " \"holidays_and_celebrations\",\n",
764
+ " \"jobs\",\n",
765
+ " \"locations\",\n",
766
+ " \"people\",\n",
767
+ " \"real_world_locations\",\n",
768
+ "\n",
769
+ " # More Categories\n",
770
+ " \"dances\",\n",
771
+ " \"family_relationships\",\n",
772
+ " \"food_tags\",\n",
773
+ " \"fire\",\n",
774
+ " \"groups\",\n",
775
+ " \"phrases\",\n",
776
+ " \"scan\",\n",
777
+ " \"subjective\",\n",
778
+ " \"technology\",\n",
779
+ " \"verbs_and_gerunds\",\n",
780
+ " \"water\",\n",
781
+ " \"airplanes\",\n",
782
+ "\n",
783
+ " # Artists\n",
784
+ " \"named_drawfags\",\n",
785
+ " \"pixiv_projects\",\n",
786
+ "\n",
787
+ " # Characters\n",
788
+ " \"ace_attorney_characters\",\n",
789
+ " \"arknights_characters\",\n",
790
+ " \"atelier_characters\",\n",
791
+ " \"azur_lane_characters\",\n",
792
+ " \"bleach_characters\",\n",
793
+ " \"bokujou_monogatari_characters\",\n",
794
+ " \"brave_girl_ravens_characters\",\n",
795
+ " \"cardcaptor_sakura_characters\",\n",
796
+ " \"danganronpa_characters\",\n",
797
+ " \"digimon\",\n",
798
+ " \"digimon_characters\",\n",
799
+ " \"dragon_ball_characters\",\n",
800
+ " \"dragon_quest_characters\",\n",
801
+ " \"fate_series_characters\",\n",
802
+ " \"final_fantasy_characters\",\n",
803
+ " \"fire_emblem_characters\",\n",
804
+ " \"flower_knight_girl_characters\",\n",
805
+ " \"gensou_suikoden_characters\",\n",
806
+ " \"girls_frontline_characters\",\n",
807
+ " \"girls_und_panzer_characters\",\n",
808
+ " \"gundam_mechas\",\n",
809
+ " \"hunter_x_hunter_characters\",\n",
810
+ " \"jojo_no_kimyou_na_bouken_characters\",\n",
811
+ " \"kamen_rider_characters\",\n",
812
+ " \"kantai_collection_characters\",\n",
813
+ " \"kingdom_hearts_characters\",\n",
814
+ " \"mahou_sensei_negima_characters\",\n",
815
+ " \"meitantei_conan_characters\",\n",
816
+ " \"minecraft_characters\",\n",
817
+ " \"naruto_characters\",\n",
818
+ " \"nippon_ichi_characters\",\n",
819
+ " \"one_piece_characters\",\n",
820
+ " \"oshiro_project_characters\",\n",
821
+ " \"pokemon_characters\",\n",
822
+ " \"elite_four_members\",\n",
823
+ " \"gym_leaders\",\n",
824
+ " \"families_of_pokemon_main_characters\",\n",
825
+ " \"pokemon_ranger_characters\",\n",
826
+ " \"pokemon_trainer_classes\",\n",
827
+ " \"pokemon\",\n",
828
+ " \"pretty_cure_characters\",\n",
829
+ " \"ragnarok_online_characters\",\n",
830
+ " \"rosenkreuzstilette_characters\",\n",
831
+ " \"sailor_moon_characters\",\n",
832
+ " \"street_fighter_characters\",\n",
833
+ " \"super_smash_bros_characters\",\n",
834
+ " \"world_witches_series_characters\",\n",
835
+ " \"tales_of_characters\",\n",
836
+ " \"toaru_majutsu_no_index_characters\",\n",
837
+ " \"touhou_characters\",\n",
838
+ " \"touken_ranbu_characters\",\n",
839
+ " \"ultra_series_characters\",\n",
840
+ " \"umamusume_characters\",\n",
841
+ " \"vocaloid_characters\",\n",
842
+ " \"yu_gi_oh_characters\",\n",
843
+ " \"genderswap_characters\",\n",
844
+ " \"official_mascots\",\n",
845
+ " \"real_life_racehorses\",\n",
846
+ "\n",
847
+ "\n",
848
+ " \n",
849
+ " # Other Lists\n",
850
+ " \"disambiguation_pages\",\n",
851
+ " \"magazine_publications\",\n",
852
+ " \"special_moves\",\n",
853
+ " \"uniforms\",\n",
854
+ " \"pokemon_media\",\n",
855
+ " \"tagged_songs\",\n",
856
+ " \"vocaloid_derivatives\",\n",
857
+ " \"vocaloid_songs\",\n",
858
+ " \"vocal_synthesizers\",\n",
859
+ " \"vocal_synth_derivatives\",\n",
860
+ " \"vocal_synth_songs\",\n",
861
+ " \"deemo_songs\",\n",
862
+ " #\"role_playing_games\",\n",
863
+ "\n",
864
+ " # Metatags\n",
865
+ " #\"metatags\",\n",
866
+ " #\"drawing_software\",\n",
867
+ "\n",
868
+ " # Pool Groups & Meta-Wikis\n",
869
+ " \"meta_wikis\"\n",
870
+ "]\n"
871
+ ]
872
+ },
873
+ {
874
+ "cell_type": "code",
875
+ "execution_count": 11,
876
+ "id": "8f8b6b29-e7bd-4814-8705-2b7a68f2d660",
877
+ "metadata": {
878
+ "execution": {
879
+ "iopub.execute_input": "2025-03-17T12:48:52.253272Z",
880
+ "iopub.status.busy": "2025-03-17T12:48:52.252077Z",
881
+ "iopub.status.idle": "2025-03-17T12:48:52.256578Z",
882
+ "shell.execute_reply": "2025-03-17T12:48:52.256016Z",
883
+ "shell.execute_reply.started": "2025-03-17T12:48:52.253248Z"
884
+ }
885
+ },
886
+ "outputs": [],
887
+ "source": [
888
+ "special_wiki_pages = [\n",
889
+ " \"plant\", \"tree\", \"computer\", \"on_object\", \"injury\", \"swimsuit\", \"on\" , \"mask\" # Add more here if needed\n",
890
+ "]"
891
+ ]
892
+ },
893
+ {
894
+ "cell_type": "code",
895
+ "execution_count": 12,
896
+ "id": "79234bc5-b287-4306-85cd-a27ddba769ea",
897
+ "metadata": {
898
+ "execution": {
899
+ "iopub.execute_input": "2025-03-17T12:48:55.161786Z",
900
+ "iopub.status.busy": "2025-03-17T12:48:55.160800Z",
901
+ "iopub.status.idle": "2025-03-17T12:48:55.165137Z",
902
+ "shell.execute_reply": "2025-03-17T12:48:55.164568Z",
903
+ "shell.execute_reply.started": "2025-03-17T12:48:55.161760Z"
904
+ }
905
+ },
906
+ "outputs": [],
907
+ "source": [
908
+ "import base64\n",
909
+ "HEADERS = {\n",
910
+ " \"Authorization\": f\"Basic {base64.b64encode(f'{username}:{api_key}'.encode()).decode()}\"\n",
911
+ "}"
912
+ ]
913
+ },
914
+ {
915
+ "cell_type": "code",
916
+ "execution_count": 13,
917
+ "id": "64d47678-78a4-4d55-a3a0-1c1b251b03b6",
918
+ "metadata": {
919
+ "execution": {
920
+ "iopub.execute_input": "2025-03-17T12:40:12.358876Z",
921
+ "iopub.status.busy": "2025-03-17T12:40:12.358573Z",
922
+ "iopub.status.idle": "2025-03-17T12:40:43.547891Z",
923
+ "shell.execute_reply": "2025-03-17T12:40:43.547451Z",
924
+ "shell.execute_reply.started": "2025-03-17T12:40:12.358851Z"
925
+ }
926
+ },
927
+ "outputs": [
928
+ {
929
+ "name": "stdout",
930
+ "output_type": "stream",
931
+ "text": [
932
+ "🚀 Starting Danbooru Tag Hierarchy API Fetcher...\n",
933
+ "❌ No data found for attire_and_body_accessories in any format\n",
934
+ "✅ Added 91 tags directly under attire_and_body_accessories.attire.dress\n",
935
+ "✅ Added 100 tags directly under attire_and_body_accessories.attire.handwear\n",
936
+ "✅ Added 251 tags directly under attire_and_body_accessories.attire.headwear\n",
937
+ "✅ Added 69 tags directly under attire_and_body_accessories.attire.legwear\n",
938
+ "✅ Added 64 tags directly under attire_and_body_accessories.attire.mask\n",
939
+ "✅ Added 264 tags directly under attire_and_body_accessories.attire.neck_and_neckwear\n",
940
+ "✅ Added 54 tags directly under attire_and_body_accessories.attire.sexual_attire.bra\n",
941
+ "✅ Added 83 tags directly under attire_and_body_accessories.attire.sexual_attire.panties\n",
942
+ "✅ Added 87 tags directly under attire_and_body_accessories.attire.sleeves\n",
943
+ "✅ Added 83 tags directly under attire_and_body_accessories.attire.swimsuit\n",
944
+ "✅ Added 4 tags directly under attire_and_body_accessories.embellishment\n",
945
+ "✅ Added 94 tags directly under attire_and_body_accessories.eyewear\n",
946
+ "✅ Added 62 tags directly under attire_and_body_accessories.fashion_style\n",
947
+ "✅ Added 189 tags directly under attire_and_body_accessories.nudity\n",
948
+ "❌ No data found for body in any format\n",
949
+ "✅ Added 40 tags directly under body.body_parts.ass\n",
950
+ "✅ Added 213 tags directly under body.body_parts.breasts_tags\n",
951
+ "✅ Added 54 tags directly under body.body_parts.ears_tags\n",
952
+ "✅ Added 200 tags directly under body.body_parts.face_tags.eyes_tags\n",
953
+ "✅ Added 31 tags directly under body.body_parts.hair.hair_color\n",
954
+ "✅ Added 157 tags directly under body.body_parts.hair.hair_styles\n",
955
+ "✅ Added 117 tags directly under body.body_parts.hands.gestures\n",
956
+ "✅ Added 264 tags directly under body.body_parts.neck_and_neckwear\n",
957
+ "✅ Added 74 tags directly under body.body_parts.penis\n",
958
+ "✅ Added 230 tags directly under body.body_parts.posture\n",
959
+ "✅ Added 50 tags directly under body.body_parts.pussy\n",
960
+ "✅ Added 64 tags directly under body.body_parts.shoulders\n",
961
+ "✅ Added 26 tags directly under body.body_parts.skin_color\n",
962
+ "✅ Added 79 tags directly under body.body_parts.tail\n",
963
+ "✅ Added 90 tags directly under body.body_parts.wings\n",
964
+ "✅ Added 55 tags directly under body.injury\n",
965
+ "❌ No data found for characters in any format\n",
966
+ "✅ Added 300 tags directly under characters.ace_attorney\n",
967
+ "✅ Added 579 tags directly under characters.arknights\n",
968
+ "✅ Added 296 tags directly under characters.atelier\n",
969
+ "✅ Added 706 tags directly under characters.azur_lane\n",
970
+ "✅ Added 223 tags directly under characters.bleach\n",
971
+ "✅ Added 98 tags directly under characters.bokujou_monogatari\n",
972
+ "✅ Added 46 tags directly under characters.brave_girl_ravens\n",
973
+ "✅ Added 144 tags directly under characters.cardcaptor_sakura\n",
974
+ "✅ Added 128 tags directly under characters.danganronpa\n",
975
+ "✅ Added 240 tags directly under characters.digimon.digimon_characters\n",
976
+ "✅ Added 613 tags directly under characters.dragon_ball\n",
977
+ "✅ Added 331 tags directly under characters.dragon_quest\n",
978
+ "✅ Added 814 tags directly under characters.fate_series\n",
979
+ "✅ Added 566 tags directly under characters.final_fantasy\n",
980
+ "✅ Added 727 tags directly under characters.fire_emblem\n",
981
+ "✅ Added 441 tags directly under characters.flower_knight_girl\n",
982
+ "✅ Added 221 tags directly under characters.genderswap\n",
983
+ "✅ Added 431 tags directly under characters.gensou_suikoden\n",
984
+ "❌ No data found for girls_frontline_characters in any format\n",
985
+ "✅ Added 0 tags directly under characters.girls_frontline\n",
986
+ "✅ Added 305 tags directly under characters.girls_und_panzer\n",
987
+ "✅ Added 267 tags directly under characters.gundam_mechas\n",
988
+ "✅ Added 313 tags directly under characters.hunter_x_hunter\n",
989
+ "✅ Added 348 tags directly under characters.jojo_no_kimyou_na_bouken\n",
990
+ "✅ Added 308 tags directly under characters.kamen_rider\n",
991
+ "✅ Added 465 tags directly under characters.kantai_collection\n",
992
+ "✅ Added 64 tags directly under characters.kingdom_hearts\n",
993
+ "✅ Added 72 tags directly under characters.mahou_sensei_negima\n",
994
+ "✅ Added 158 tags directly under characters.meitantei_conan\n",
995
+ "✅ Added 153 tags directly under characters.minecraft\n",
996
+ "✅ Added 198 tags directly under characters.naruto\n",
997
+ "✅ Added 336 tags directly under characters.nippon_ichi\n",
998
+ "✅ Added 375 tags directly under characters.official_mascots\n",
999
+ "✅ Added 464 tags directly under characters.one_piece\n",
1000
+ "✅ Added 305 tags directly under characters.oshiro_project\n",
1001
+ "✅ Added 72 tags directly under characters.pokemon.elite_four_members\n",
1002
+ "✅ Added 165 tags directly under characters.pokemon.families_of_pokemon_main_characters\n",
1003
+ "✅ Added 100 tags directly under characters.pokemon.gym_leaders\n",
1004
+ "✅ Added 37 tags directly under characters.pokemon.pokemon_ranger_characters\n",
1005
+ "✅ Added 149 tags directly under characters.pokemon.pokemon_trainer_classes\n",
1006
+ "✅ Added 925 tags directly under characters.pretty_cure\n",
1007
+ "✅ Added 1214 tags directly under characters.ragnarok_online\n",
1008
+ "✅ Added 434 tags directly under characters.real_life_racehorses\n",
1009
+ "✅ Added 23 tags directly under characters.rosenkreuzstilette\n",
1010
+ "✅ Added 320 tags directly under characters.sailor_moon\n",
1011
+ "✅ Added 170 tags directly under characters.street_fighter\n",
1012
+ "❌ No data found for super_smash_bros_characters in any format\n",
1013
+ "✅ Added 0 tags directly under characters.super_smash_bros\n",
1014
+ "❌ No data found for tales_of_characters in any format\n",
1015
+ "✅ Added 0 tags directly under characters.tales_of\n",
1016
+ "✅ Added 189 tags directly under characters.toaru_majutsu_no_index\n",
1017
+ "✅ Added 381 tags directly under characters.touhou\n",
1018
+ "✅ Added 128 tags directly under characters.touken_ranbu\n",
1019
+ "✅ Added 540 tags directly under characters.ultra_series\n",
1020
+ "✅ Added 250 tags directly under characters.umamusume\n",
1021
+ "✅ Added 140 tags directly under characters.vocaloid\n",
1022
+ "✅ Added 184 tags directly under characters.world_witches_series\n",
1023
+ "❌ No data found for yu_gi_oh_characters in any format\n",
1024
+ "✅ Added 0 tags directly under characters.yu_gi_oh\n",
1025
+ "❌ No data found for copyrights_artists_projects_and_media in any format\n",
1026
+ "❌ No data found for genres_of_video_games in any format\n",
1027
+ "✅ Added 175 tags directly under copyrights_artists_projects_and_media.genres_of_video_games.fighting_games\n",
1028
+ "✅ Added 42 tags directly under copyrights_artists_projects_and_media.genres_of_video_games.platform_games\n",
1029
+ "✅ Added 515 tags directly under copyrights_artists_projects_and_media.genres_of_video_games.role-playing_games\n",
1030
+ "✅ Added 114 tags directly under copyrights_artists_projects_and_media.genres_of_video_games.shooter_games\n",
1031
+ "✅ Added 185 tags directly under copyrights_artists_projects_and_media.genres_of_video_games.visual_novel_games\n",
1032
+ "❌ No data found for creatures in any format\n",
1033
+ "✅ Added 232 tags directly under creatures.animals.birds\n",
1034
+ "✅ Added 224 tags directly under creatures.animals.cats\n",
1035
+ "✅ Added 237 tags directly under creatures.animals.dogs\n",
1036
+ "✅ Added 285 tags directly under creatures.legendary_creatures\n",
1037
+ "✅ Added 55 tags directly under drawing_software\n",
1038
+ "❌ No data found for games in any format\n",
1039
+ "✅ Added 21 tags directly under games.board_games\n",
1040
+ "✅ Added 175 tags directly under games.fighting_games\n",
1041
+ "✅ Added 61 tags directly under games.game_activities\n",
1042
+ "✅ Added 420 tags directly under games.sports\n",
1043
+ "✅ Added 144 tags directly under games.video_game\n",
1044
+ "✅ Added 213 tags directly under metatags\n",
1045
+ "❌ No data found for more in any format\n",
1046
+ "✅ Added 110 tags directly under more.dances\n",
1047
+ "✅ Added 29 tags directly under more.family_relationships\n",
1048
+ "✅ Added 68 tags directly under more.fire\n",
1049
+ "✅ Added 1051 tags directly under more.food_tags\n",
1050
+ "✅ Added 31 tags directly under more.groups\n",
1051
+ "✅ Added 76 tags directly under more.phrases\n",
1052
+ "✅ Added 35 tags directly under more.scan\n",
1053
+ "✅ Added 45 tags directly under more.subjective\n",
1054
+ "✅ Added 237 tags directly under more.technology\n",
1055
+ "✅ Added 446 tags directly under more.verbs_and_gerunds\n",
1056
+ "✅ Added 54 tags directly under more.water\n",
1057
+ "❌ No data found for objects in any format\n",
1058
+ "✅ Added 410 tags directly under objects.airplanes\n",
1059
+ "✅ Added 145 tags directly under objects.armor\n",
1060
+ "✅ Added 377 tags directly under objects.audio_tags\n",
1061
+ "✅ Added 66 tags directly under objects.cards.playing_card_faces\n",
1062
+ "✅ Added 80 tags directly under objects.computer\n",
1063
+ "✅ Added 94 tags directly under objects.eyewear\n",
1064
+ "✅ Added 407 tags directly under objects.ground_vehicles\n",
1065
+ "✅ Added 25 tags directly under objects.helicopters\n",
1066
+ "✅ Added 48 tags directly under objects.piercings\n",
1067
+ "✅ Added 94 tags directly under objects.pokemon_objects\n",
1068
+ "✅ Added 105 tags directly under objects.sex_objects\n",
1069
+ "✅ Added 256 tags directly under objects.ships\n",
1070
+ "✅ Added 917 tags directly under objects.weapons\n",
1071
+ "✅ Added 21 tags directly under plant.flowers\n",
1072
+ "✅ Added 59 tags directly under plant.plant\n",
1073
+ "✅ Added 44 tags directly under plant.tree\n",
1074
+ "✅ Added 59 tags directly under plants\n",
1075
+ "❌ No data found for real_world in any format\n",
1076
+ "✅ Added 363 tags directly under real_world.companies_and_brand_names\n",
1077
+ "✅ Added 138 tags directly under real_world.holidays_and_celebrations\n",
1078
+ "✅ Added 75 tags directly under real_world.jobs\n",
1079
+ "✅ Added 263 tags directly under real_world.locations\n",
1080
+ "✅ Added 1526 tags directly under real_world.people\n",
1081
+ "✅ Added 463 tags directly under real_world.real_world_locations\n",
1082
+ "❌ No data found for sex in any format\n",
1083
+ "✅ Added 16 tags directly under sex.sex_acts.simulated_sex_acts\n",
1084
+ "✅ Added 56 tags directly under sex.sexual_positions\n",
1085
+ "❌ No data found for subject in any format\n",
1086
+ "❌ No data found for anthro in any format\n",
1087
+ "❌ No data found for cat_boy in any format\n",
1088
+ "✅ Added 0 tags directly under subject.anthro.cat_boy\n",
1089
+ "❌ No data found for cat_girl in any format\n",
1090
+ "✅ Added 0 tags directly under subject.anthro.cat_girl\n",
1091
+ "❌ No data found for dog_girl in any format\n",
1092
+ "✅ Added 0 tags directly under subject.anthro.dog_girl\n",
1093
+ "❌ No data found for fox_girl in any format\n",
1094
+ "✅ Added 0 tags directly under subject.anthro.fox_girl\n",
1095
+ "❌ No data found for furry in any format\n",
1096
+ "✅ Added 0 tags directly under subject.anthro.furry\n",
1097
+ "❌ No data found for female in any format\n",
1098
+ "❌ No data found for 1girl in any format\n",
1099
+ "✅ Added 0 tags directly under subject.female.1girl\n",
1100
+ "❌ No data found for 2girls in any format\n",
1101
+ "✅ Added 0 tags directly under subject.female.2girls\n",
1102
+ "❌ No data found for female_general in any format\n",
1103
+ "✅ Added 0 tags directly under subject.female_general\n",
1104
+ "❌ No data found for koma in any format\n",
1105
+ "❌ No data found for 1koma in any format\n",
1106
+ "✅ Added 0 tags directly under subject.koma.1koma\n",
1107
+ "❌ No data found for 2koma in any format\n",
1108
+ "✅ Added 0 tags directly under subject.koma.2koma\n",
1109
+ "❌ No data found for male in any format\n",
1110
+ "❌ No data found for 1boy in any format\n",
1111
+ "✅ Added 0 tags directly under subject.male.1boy\n",
1112
+ "❌ No data found for 2boys in any format\n",
1113
+ "✅ Added 0 tags directly under subject.male.2boys\n",
1114
+ "❌ No data found for 3boys in any format\n",
1115
+ "✅ Added 0 tags directly under subject.male.3boys\n",
1116
+ "❌ No data found for 4boys in any format\n",
1117
+ "✅ Added 0 tags directly under subject.male.4boys\n",
1118
+ "❌ No data found for 5boys in any format\n",
1119
+ "✅ Added 0 tags directly under subject.male.5boys\n",
1120
+ "❌ No data found for 6+boys in any format\n",
1121
+ "✅ Added 0 tags directly under subject.male.6+boys\n",
1122
+ "❌ No data found for boy in any format\n",
1123
+ "✅ Added 0 tags directly under subject.male.boy\n",
1124
+ "❌ No data found for man in any format\n",
1125
+ "✅ Added 0 tags directly under subject.male.man\n",
1126
+ "❌ No data found for multiple_boys in any format\n",
1127
+ "✅ Added 0 tags directly under subject.male.multiple_boys\n",
1128
+ "❌ No data found for visual_characteristics in any format\n",
1129
+ "❌ No data found for image_composition_and_style in any format\n",
1130
+ "✅ Added 73 tags directly under visual_characteristics.image_composition_and_style.artistic_license\n",
1131
+ "✅ Added 115 tags directly under visual_characteristics.image_composition_and_style.image_composition.backgrounds\n",
1132
+ "✅ Added 90 tags directly under visual_characteristics.image_composition_and_style.image_composition.censorship\n",
1133
+ "✅ Added 54 tags directly under visual_characteristics.image_composition_and_style.image_composition.colors\n",
1134
+ "✅ Added 28 tags directly under visual_characteristics.image_composition_and_style.image_composition.focus_tags\n",
1135
+ "✅ Added 55 tags directly under visual_characteristics.image_composition_and_style.image_composition.lighting\n",
1136
+ "✅ Added 76 tags directly under visual_characteristics.image_composition_and_style.image_composition.prints\n",
1137
+ "✅ Added 495 tags directly under visual_characteristics.image_composition_and_style.image_composition.style_parodies\n",
1138
+ "✅ Added 41 tags directly under visual_characteristics.image_composition_and_style.patterns\n",
1139
+ "✅ Added 310 tags directly under visual_characteristics.image_composition_and_style.symbols\n",
1140
+ "✅ Added 242 tags directly under visual_characteristics.image_composition_and_style.text\n",
1141
+ "✅ Added 62 tags directly under visual_characteristics.image_composition_and_style.year_tags\n",
1142
+ "✅ Finished building hierarchy.\n"
1143
+ ]
1144
+ },
1145
+ {
1146
+ "ename": "FileNotFoundError",
1147
+ "evalue": "[Errno 2] No such file or directory: '/home/lauhp/000_PHD/000_010_PUBLICATION/2025_SAGE/CODE/pm-paper_uzh_gitlab/pm-paper/misc/danbooru_donmai/danbooru_tags_step_01.json'",
1148
+ "output_type": "error",
1149
+ "traceback": [
1150
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
1151
+ "\u001b[0;31mFileNotFoundError\u001b[0m Traceback (most recent call last)",
1152
+ "Cell \u001b[0;32mIn[13], line 198\u001b[0m\n\u001b[1;32m 196\u001b[0m \u001b[38;5;66;03m# Save cleaned JSON\u001b[39;00m\n\u001b[1;32m 197\u001b[0m output_file \u001b[38;5;241m=\u001b[39m current_dir\u001b[38;5;241m.\u001b[39mparent \u001b[38;5;241m/\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmisc/danbooru_donmai/danbooru_tags_step_01.json\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m--> 198\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43moutput_file\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mw\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mencoding\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mutf-8\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[1;32m 199\u001b[0m json\u001b[38;5;241m.\u001b[39mdump(manual_hierarchy, f, indent\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m4\u001b[39m, ensure_ascii\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m)\n\u001b[1;32m 201\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[38;5;124m✅ Hierarchy saved to `\u001b[39m\u001b[38;5;132;01m{\u001b[39;00moutput_file\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m`\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n",
1153
+ "File \u001b[0;32m~/.local/lib/python3.10/site-packages/IPython/core/interactiveshell.py:324\u001b[0m, in \u001b[0;36m_modified_open\u001b[0;34m(file, *args, **kwargs)\u001b[0m\n\u001b[1;32m 317\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m {\u001b[38;5;241m0\u001b[39m, \u001b[38;5;241m1\u001b[39m, \u001b[38;5;241m2\u001b[39m}:\n\u001b[1;32m 318\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 319\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mIPython won\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mt let you open fd=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfile\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m by default \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 320\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mas it is likely to crash IPython. If you know what you are doing, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 321\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124myou can use builtins\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m open.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 322\u001b[0m )\n\u001b[0;32m--> 324\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mio_open\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfile\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n",
1154
+ "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: '/home/lauhp/000_PHD/000_010_PUBLICATION/2025_SAGE/CODE/pm-paper_uzh_gitlab/pm-paper/misc/danbooru_donmai/danbooru_tags_step_01.json'"
1155
+ ]
1156
+ }
1157
+ ],
1158
+ "source": [
1159
+ "import requests\n",
1160
+ "import json\n",
1161
+ "import time\n",
1162
+ "import re\n",
1163
+ "from urllib.parse import quote\n",
1164
+ "\n",
1165
+ "# Base API URL\n",
1166
+ "BASE_URL = \"https://danbooru.donmai.us\"\n",
1167
+ "\n",
1168
+ "# Authentication (Replace with your credentials)\n",
1169
+ "AUTH = (username, api_key)\n",
1170
+ "\n",
1171
+ "# Storage for missing tags\n",
1172
+ "missing_tags = []\n",
1173
+ "\n",
1174
+ "# ✅ Initialize manual_hierarchy\n",
1175
+ "manual_hierarchy = {}\n",
1176
+ "\n",
1177
+ "def clean_hierarchy_and_count_tags(hierarchy):\n",
1178
+ " \"\"\"\n",
1179
+ " Recursively removes empty categories from the JSON hierarchy and counts total tags.\n",
1180
+ " \"\"\"\n",
1181
+ " total_tag_count = 0\n",
1182
+ "\n",
1183
+ " def clean_dict(d):\n",
1184
+ " nonlocal total_tag_count\n",
1185
+ " keys_to_delete = []\n",
1186
+ "\n",
1187
+ " for key, value in d.items():\n",
1188
+ " if isinstance(value, dict):\n",
1189
+ " clean_dict(value) # Recursively clean subcategories\n",
1190
+ " \n",
1191
+ " # ✅ If the subcategory is empty after cleaning, mark it for removal\n",
1192
+ " if not value:\n",
1193
+ " keys_to_delete.append(key)\n",
1194
+ " elif isinstance(value, list):\n",
1195
+ " # ✅ Count the total number of tags\n",
1196
+ " total_tag_count += len(value)\n",
1197
+ "\n",
1198
+ " # ✅ If the list is empty, mark it for deletion\n",
1199
+ " if not value:\n",
1200
+ " keys_to_delete.append(key)\n",
1201
+ "\n",
1202
+ " # ✅ Remove empty keys\n",
1203
+ " for key in keys_to_delete:\n",
1204
+ " del d[key]\n",
1205
+ "\n",
1206
+ " clean_dict(hierarchy)\n",
1207
+ "\n",
1208
+ " return hierarchy, total_tag_count\n",
1209
+ "\n",
1210
+ "\n",
1211
+ "def fetch_wiki_page(tag, is_list=False):\n",
1212
+ " \"\"\"\n",
1213
+ " Fetches the wiki page of a tag group, list category, or special wiki page using the API.\n",
1214
+ " Uses list_of_ for lists, tag_group: for regular groups, \n",
1215
+ " and direct queries for special cases.\n",
1216
+ " \"\"\"\n",
1217
+ " if tag in special_wiki_pages:\n",
1218
+ " prefixes = [\"\"] # Query directly with no prefix\n",
1219
+ " #print(f\"🔍 {tag} is a special wiki page, querying directly...\")\n",
1220
+ " elif is_list:\n",
1221
+ " prefixes = [\"list_of_\", \"tag_group:\"]\n",
1222
+ " else:\n",
1223
+ " prefixes = [\"tag_group:\"]\n",
1224
+ "\n",
1225
+ " # print(f\"🚀 Fetching {prefixes} {tag}\") # Debugging print\n",
1226
+ "\n",
1227
+ " for prefix in prefixes:\n",
1228
+ " query_tag = f\"{prefix}{tag}\".strip() # Avoid unnecessary :\n",
1229
+ " encoded_query = quote(query_tag, safe=\"\") # Proper URL encoding\n",
1230
+ " url = f\"{BASE_URL}/wiki_pages.json?search[title]={encoded_query}\"\n",
1231
+ "\n",
1232
+ " # print(f\"🚀 Fetching: {query_tag}\")\n",
1233
+ "\n",
1234
+ " try:\n",
1235
+ " response = requests.get(url, auth=AUTH) # Use authentication\n",
1236
+ " response.raise_for_status() # Raise error for bad responses (4xx, 5xx)\n",
1237
+ " wiki_data = response.json()\n",
1238
+ "\n",
1239
+ " if wiki_data and isinstance(wiki_data, list) and len(wiki_data) > 0:\n",
1240
+ " #print(f\"✅ Data found using {query_tag}\")\n",
1241
+ " return wiki_data[0].get(\"body\", \"\") # Extract the \"body\" text\n",
1242
+ "\n",
1243
+ " except requests.exceptions.HTTPError as e:\n",
1244
+ " if response.status_code == 401:\n",
1245
+ " print(f\"❌ Authentication Error (401 Unauthorized) for {query_tag}. Check your API key!\")\n",
1246
+ " exit() # Stop execution if authentication fails\n",
1247
+ " else:\n",
1248
+ " print(f\"❌ Error fetching {query_tag}: {e}\")\n",
1249
+ "\n",
1250
+ " # If all attempts fail\n",
1251
+ " print(f\"❌ No data found for {tag} in any format\")\n",
1252
+ " missing_tags.append(tag)\n",
1253
+ " return None\n",
1254
+ "\n",
1255
+ "\n",
1256
+ "\n",
1257
+ "\n",
1258
+ "def build_tag_hierarchy(tag_groups):\n",
1259
+ " \"\"\"\n",
1260
+ " Builds and structures the hierarchy properly, ensuring:\n",
1261
+ " - Categories with subcategories store their direct tags in `_general`\n",
1262
+ " - Parent categories exist before adding children\n",
1263
+ " - Ensures subject and other key groups are correctly retained\n",
1264
+ " \"\"\"\n",
1265
+ " processed_groups = set()\n",
1266
+ "\n",
1267
+ " for hierarchy_path, tag_group in sorted(tag_groups.items(), key=lambda x: x[0]):\n",
1268
+ " is_list = tag_group in list_based_categories # Check if it's a list-based category\n",
1269
+ "\n",
1270
+ " levels = hierarchy_path.split(\".\")\n",
1271
+ " current_level = manual_hierarchy\n",
1272
+ "\n",
1273
+ " for key in levels[:-1]: # Ensure each parent level exists\n",
1274
+ " if key not in current_level or not isinstance(current_level[key], dict):\n",
1275
+ " current_level[key] = {} # Create dictionary if missing\n",
1276
+ " current_level = current_level[key]\n",
1277
+ "\n",
1278
+ " last_level = levels[-1]\n",
1279
+ " has_subcategories = any(k.startswith(hierarchy_path + \".\") for k in tag_groups.keys())\n",
1280
+ "\n",
1281
+ " # ✅ Ensure the category itself exists\n",
1282
+ " if last_level not in current_level:\n",
1283
+ " current_level[last_level] = {} if has_subcategories else []\n",
1284
+ "\n",
1285
+ " # ✅ Fetch tags from API\n",
1286
+ " wiki_text = fetch_wiki_page(tag_group, is_list)\n",
1287
+ " extracted_tags = extract_tags_from_wiki(wiki_text, is_list)\n",
1288
+ "\n",
1289
+ " # ✅ Store tags under `<category>_general` if there are subcategories\n",
1290
+ " if has_subcategories:\n",
1291
+ " general_key = f\"{last_level}_general\"\n",
1292
+ "\n",
1293
+ " if isinstance(current_level[last_level], list):\n",
1294
+ " # print(f\"⚠️ Warning: {last_level} was a list but has subcategories. Converting to dictionary.\")\n",
1295
+ " current_level[last_level] = {}\n",
1296
+ "\n",
1297
+ " if general_key not in current_level[last_level]:\n",
1298
+ " current_level[last_level][general_key] = [] # Initialize `_general` list\n",
1299
+ "\n",
1300
+ " current_level[last_level][general_key].extend(extracted_tags)\n",
1301
+ " #print(f\"✅ Added {len(extracted_tags)} tags under {hierarchy_path} → {general_key}\")\n",
1302
+ "\n",
1303
+ " else:\n",
1304
+ " # ✅ If no subcategories exist, store tags directly\n",
1305
+ " if isinstance(current_level[last_level], list):\n",
1306
+ " current_level[last_level].extend(extracted_tags)\n",
1307
+ " else:\n",
1308
+ " # print(f\"⚠️ Warning: {last_level} was a dictionary but has no subcategories. Converting to list.\")\n",
1309
+ " current_level[last_level] = extracted_tags\n",
1310
+ "\n",
1311
+ " print(f\"✅ Added {len(extracted_tags)} tags directly under {hierarchy_path}\")\n",
1312
+ "\n",
1313
+ " print(f\"✅ Finished building hierarchy.\")\n",
1314
+ "\n",
1315
+ "\n",
1316
+ "\n",
1317
+ "\n",
1318
+ "\n",
1319
+ "def extract_tags_from_wiki(wiki_text, is_list=False):\n",
1320
+ " \"\"\"\n",
1321
+ " Extracts valid tags from the wiki text.\n",
1322
+ " Uses different extraction logic for tag groups and list-based pages.\n",
1323
+ " \"\"\"\n",
1324
+ " if not wiki_text:\n",
1325
+ " return []\n",
1326
+ "\n",
1327
+ " if is_list:\n",
1328
+ " # Extract `[[tag_name]]` from list pages\n",
1329
+ " tag_pattern = re.compile(r\"\\[\\[(.*?)\\]\\]\")\n",
1330
+ " tags = tag_pattern.findall(wiki_text)\n",
1331
+ " else:\n",
1332
+ " # Extract `[[tag_name]]` from tag groups (skip first item if \"Tag Groups\")\n",
1333
+ " tag_pattern = re.compile(r\"\\[\\[(.*?)\\]\\]\")\n",
1334
+ " tags = tag_pattern.findall(wiki_text)\n",
1335
+ " tags = tags[1:] if tags else [] # Skip first tag\n",
1336
+ "\n",
1337
+ " # Clean tags by removing alternative names `[[Tag|Alternative]]`\n",
1338
+ " cleaned_tags = [tag.split(\"|\")[0].strip() for tag in tags]\n",
1339
+ "\n",
1340
+ " return cleaned_tags\n",
1341
+ "\n",
1342
+ "if __name__ == \"__main__\":\n",
1343
+ " print(\"🚀 Starting Danbooru Tag Hierarchy API Fetcher...\")\n",
1344
+ "\n",
1345
+ " # ✅ Ensure manual_hierarchy exists before cleaning\n",
1346
+ " manual_hierarchy = {}\n",
1347
+ "\n",
1348
+ " # Build hierarchy dynamically\n",
1349
+ " build_tag_hierarchy(tag_groups)\n",
1350
+ "\n",
1351
+ " # ✅ Clean hierarchy and count total tags\n",
1352
+ " manual_hierarchy, total_tags = clean_hierarchy_and_count_tags(manual_hierarchy)\n",
1353
+ "\n",
1354
+ " # Save cleaned JSON\n",
1355
+ " output_file = current_dir.parent / \"misc/danbooru_donmai/danbooru_tags_step_01.json\"\n",
1356
+ " with open(output_file, \"w\", encoding=\"utf-8\") as f:\n",
1357
+ " json.dump(manual_hierarchy, f, indent=4, ensure_ascii=False)\n",
1358
+ "\n",
1359
+ " print(f\"\\n✅ Hierarchy saved to `{output_file}`\")\n",
1360
+ " print(f\"📊 Total tags in the final hierarchy: {total_tags}\")\n",
1361
+ "\n",
1362
+ " if missing_tags:\n",
1363
+ " print(f\"⚠️ The following tag groups were not found: {missing_tags}\")\n"
1364
+ ]
1365
+ },
1366
+ {
1367
+ "cell_type": "markdown",
1368
+ "id": "5bf6fa19-04dd-4799-8220-e37a4434a1f8",
1369
+ "metadata": {},
1370
+ "source": [
1371
+ "### Add subject keys etc"
1372
+ ]
1373
+ },
1374
+ {
1375
+ "cell_type": "code",
1376
+ "execution_count": 15,
1377
+ "id": "9bf7c0ed-0cfd-41c8-89a4-c59f1783c823",
1378
+ "metadata": {
1379
+ "execution": {
1380
+ "iopub.execute_input": "2025-03-17T12:49:06.895515Z",
1381
+ "iopub.status.busy": "2025-03-17T12:49:06.895119Z",
1382
+ "iopub.status.idle": "2025-03-17T12:49:07.135844Z",
1383
+ "shell.execute_reply": "2025-03-17T12:49:07.135358Z",
1384
+ "shell.execute_reply.started": "2025-03-17T12:49:06.895492Z"
1385
+ }
1386
+ },
1387
+ "outputs": [
1388
+ {
1389
+ "name": "stdout",
1390
+ "output_type": "stream",
1391
+ "text": [
1392
+ "✅ Adding woman to subject.female.female_general\n",
1393
+ "✅ Adding girl to subject.female.female_general\n",
1394
+ "✅ Adding 1girl to subject.female.female_general\n",
1395
+ "🔄 Moving 2girls from more.groups to subject.female.female_general\n",
1396
+ "✅ Adding 2girls to subject.female.female_general\n",
1397
+ "🔄 Moving 3girls from more.groups to subject.female.female_general\n",
1398
+ "✅ Adding 3girls to subject.female.female_general\n",
1399
+ "🔄 Moving 4girls from more.groups to subject.female.female_general\n",
1400
+ "✅ Adding 4girls to subject.female.female_general\n",
1401
+ "🔄 Moving 5girls from more.groups to subject.female.female_general\n",
1402
+ "✅ Adding 5girls to subject.female.female_general\n",
1403
+ "🔄 Moving 6+girls from more.groups to subject.female.female_general\n",
1404
+ "✅ Adding 6+girls to subject.female.female_general\n",
1405
+ "🔄 Moving multiple girls from more.groups to subject.female.female_general\n",
1406
+ "✅ Adding multiple girls to subject.female.female_general\n",
1407
+ "🔄 Moving guitar girl from objects.audio_tags to subject.female.female_general\n",
1408
+ "✅ Adding guitar girl to subject.female.female_general\n",
1409
+ "✅ Adding man to subject.male.male_general\n",
1410
+ "✅ Adding boy to subject.male.male_general\n",
1411
+ "✅ Adding 1boy to subject.male.male_general\n",
1412
+ "🔄 Moving 2boys from more.groups to subject.male.male_general\n",
1413
+ "✅ Adding 2boys to subject.male.male_general\n",
1414
+ "🔄 Moving 3boys from more.groups to subject.male.male_general\n",
1415
+ "✅ Adding 3boys to subject.male.male_general\n",
1416
+ "🔄 Moving 4boys from more.groups to subject.male.male_general\n",
1417
+ "✅ Adding 4boys to subject.male.male_general\n",
1418
+ "🔄 Moving 5boys from more.groups to subject.male.male_general\n",
1419
+ "✅ Adding 5boys to subject.male.male_general\n",
1420
+ "🔄 Moving 6+boys from more.groups to subject.male.male_general\n",
1421
+ "✅ Adding 6+boys to subject.male.male_general\n",
1422
+ "🔄 Moving multiple boys from more.groups to subject.male.male_general\n",
1423
+ "✅ Adding multiple boys to subject.male.male_general\n",
1424
+ "✅ Adding guitar boy to subject.male.male_general\n",
1425
+ "✅ Adding 1koma to subject.koma.koma_general\n",
1426
+ "✅ Adding 2koma to subject.koma.koma_general\n",
1427
+ "🔄 Moving cat girl from creatures.animals.cats to subject.anthro.anthro_general\n",
1428
+ "✅ Adding cat girl to subject.anthro.anthro_general\n",
1429
+ "✅ Adding fox girl to subject.anthro.anthro_general\n",
1430
+ "✅ Adding dog girl to subject.anthro.anthro_general\n",
1431
+ "🔄 Moving plant girl from plant.plant to subject.anthro.anthro_general\n",
1432
+ "🔄 Moving plant girl from plants to subject.anthro.anthro_general\n",
1433
+ "✅ Adding plant girl to subject.anthro.anthro_general\n",
1434
+ "🔄 Moving plant boy from plant.plant to subject.anthro.anthro_general\n",
1435
+ "🔄 Moving plant boy from plants to subject.anthro.anthro_general\n",
1436
+ "✅ Adding plant boy to subject.anthro.anthro_general\n",
1437
+ "🔄 Moving cat boy from creatures.animals.cats to subject.anthro.anthro_general\n",
1438
+ "✅ Adding cat boy to subject.anthro.anthro_general\n",
1439
+ "✅ Adding furry to subject.anthro.anthro_general\n",
1440
+ "✅ Adding monster boy to subject.anthro.anthro_general\n",
1441
+ "✅ Adding monster girl to subject.anthro.anthro_general\n",
1442
+ "✅ Adding demon girl to subject.anthro.anthro_general\n",
1443
+ "✅ Adding demon boy to subject.anthro.anthro_general\n",
1444
+ "✅ Adding magical boy to subject.anthro.anthro_general\n",
1445
+ "🔄 Moving magical girl from characters.sailor_moon to subject.anthro.anthro_general\n",
1446
+ "✅ Adding magical girl to subject.anthro.anthro_general\n",
1447
+ "✅ Updated JSON saved as `/shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/danbooru_tags_step_02.json`\n"
1448
+ ]
1449
+ }
1450
+ ],
1451
+ "source": [
1452
+ "import json\n",
1453
+ "\n",
1454
+ "# Load the existing JSON file\n",
1455
+ "input_file = current_dir.parent / \"misc/danbooru_donmai/danbooru_tags_step_01.json\"\n",
1456
+ "output_file = current_dir.parent / \"misc/danbooru_donmai/danbooru_tags_step_02.json\"\n",
1457
+ "\n",
1458
+ "with open(input_file, \"r\", encoding=\"utf-8\") as f:\n",
1459
+ " manual_hierarchy = json.load(f)\n",
1460
+ "\n",
1461
+ "# ✅ Define the correct subject structure\n",
1462
+ "subject_structure = {\n",
1463
+ " \"subject\": {\n",
1464
+ " \"female\": {\n",
1465
+ " \"female_general\": [\"woman\", \"girl\", \"1girl\", \"2girls\", \"3girls\", \"4girls\", \"5girls\", \"6+girls\", \"multiple girls\", \"guitar girl\" ]\n",
1466
+ " },\n",
1467
+ " \"male\": {\n",
1468
+ " \"male_general\": [\"man\", \"boy\", \"1boy\", \"2boys\", \"3boys\", \"4boys\", \"5boys\", \"6+boys\", \"multiple boys\", \"guitar boy\"]\n",
1469
+ " },\n",
1470
+ " \"koma\": {\n",
1471
+ " \"koma_general\": [\"1koma\", \"2koma\"]\n",
1472
+ " },\n",
1473
+ " \"anthro\": {\n",
1474
+ " \"anthro_general\": [\"cat girl\", \"fox girl\", \"dog girl\", \"plant girl\", \"plant boy\", \"cat boy\", \"furry\", \"monster boy\", \"monster girl\", \"demon girl\" , \"demon boy\", \"magical boy\", \"magical girl\", ]\n",
1475
+ " }\n",
1476
+ " }\n",
1477
+ "}\n",
1478
+ "\n",
1479
+ "# ✅ Ensure \"subject\" exists in the hierarchy\n",
1480
+ "if \"subject\" not in manual_hierarchy:\n",
1481
+ " manual_hierarchy[\"subject\"] = {}\n",
1482
+ "\n",
1483
+ "# ✅ Ensure all subcategories exist\n",
1484
+ "for category, subcategories in subject_structure[\"subject\"].items():\n",
1485
+ " if category not in manual_hierarchy[\"subject\"]:\n",
1486
+ " manual_hierarchy[\"subject\"][category] = {}\n",
1487
+ "\n",
1488
+ " for subcategory, tags in subcategories.items():\n",
1489
+ " if subcategory not in manual_hierarchy[\"subject\"][category]:\n",
1490
+ " manual_hierarchy[\"subject\"][category][subcategory] = []\n",
1491
+ "\n",
1492
+ "# ✅ Move misplaced tags and also ensure missing tags are added\n",
1493
+ "for category, subcategories in subject_structure[\"subject\"].items():\n",
1494
+ " for subcategory, tags in subcategories.items():\n",
1495
+ " target_list = manual_hierarchy[\"subject\"][category][subcategory]\n",
1496
+ "\n",
1497
+ " for tag in tags:\n",
1498
+ " found_in_wrong_place = False # Track if tag was found elsewhere\n",
1499
+ "\n",
1500
+ " # ✅ Search for the tag in other categories and remove if found\n",
1501
+ " for key, value in list(manual_hierarchy.items()): # Use list() to avoid runtime changes\n",
1502
+ " if isinstance(value, list) and tag in value:\n",
1503
+ " print(f\"🔄 Moving {tag} from {key} to subject.{category}.{subcategory}\")\n",
1504
+ " value.remove(tag)\n",
1505
+ " found_in_wrong_place = True\n",
1506
+ "\n",
1507
+ " elif isinstance(value, dict): # Search deeper\n",
1508
+ " for subkey, subvalue in list(value.items()):\n",
1509
+ " if isinstance(subvalue, list) and tag in subvalue:\n",
1510
+ " print(f\"🔄 Moving {tag} from {key}.{subkey} to subject.{category}.{subcategory}\")\n",
1511
+ " subvalue.remove(tag)\n",
1512
+ " found_in_wrong_place = True\n",
1513
+ "\n",
1514
+ " elif isinstance(subvalue, dict):\n",
1515
+ " for deepkey, deepvalue in list(subvalue.items()):\n",
1516
+ " if isinstance(deepvalue, list) and tag in deepvalue:\n",
1517
+ " print(f\"🔄 Moving {tag} from {key}.{subkey}.{deepkey} to subject.{category}.{subcategory}\")\n",
1518
+ " deepvalue.remove(tag)\n",
1519
+ " found_in_wrong_place = True\n",
1520
+ "\n",
1521
+ " # ✅ Add the tag to the correct subject category if missing\n",
1522
+ " if tag not in target_list:\n",
1523
+ " print(f\"✅ Adding {tag} to subject.{category}.{subcategory}\")\n",
1524
+ " target_list.append(tag)\n",
1525
+ "\n",
1526
+ "# ✅ Save the updated JSON\n",
1527
+ "with open(output_file, \"w\", encoding=\"utf-8\") as f:\n",
1528
+ " json.dump(manual_hierarchy, f, indent=4, ensure_ascii=False)\n",
1529
+ "\n",
1530
+ "print(f\"✅ Updated JSON saved as `{output_file}`\")\n",
1531
+ "\n"
1532
+ ]
1533
+ },
1534
+ {
1535
+ "cell_type": "markdown",
1536
+ "id": "aff1a30d-17e7-4e55-b881-b5ce622a533a",
1537
+ "metadata": {},
1538
+ "source": [
1539
+ "### Compare with wd-14 vocabulary"
1540
+ ]
1541
+ },
1542
+ {
1543
+ "cell_type": "code",
1544
+ "execution_count": 16,
1545
+ "id": "2a8f986c-0ccd-4235-ac50-be3d6e495f9f",
1546
+ "metadata": {
1547
+ "execution": {
1548
+ "iopub.execute_input": "2025-03-17T12:49:10.749271Z",
1549
+ "iopub.status.busy": "2025-03-17T12:49:10.748481Z",
1550
+ "iopub.status.idle": "2025-03-17T12:49:11.628872Z",
1551
+ "shell.execute_reply": "2025-03-17T12:49:11.628200Z",
1552
+ "shell.execute_reply.started": "2025-03-17T12:49:10.749248Z"
1553
+ }
1554
+ },
1555
+ "outputs": [
1556
+ {
1557
+ "name": "stdout",
1558
+ "output_type": "stream",
1559
+ "text": [
1560
+ "\n",
1561
+ "📌 Tags in CSV but NOT in JSON:\n",
1562
+ "\n",
1563
+ "✅ Missing tags saved to `missing_tags.txt`\n"
1564
+ ]
1565
+ }
1566
+ ],
1567
+ "source": [
1568
+ "import json\n",
1569
+ "import pandas as pd\n",
1570
+ "\n",
1571
+ "# Load CSV file\n",
1572
+ "csv_file = current_dir.parent / \"misc/autotagging-vocabularies/danbooru.csv\" # Update with the actual CSV file name\n",
1573
+ "df = pd.read_csv(csv_file)\n",
1574
+ "\n",
1575
+ "# Load JSON file\n",
1576
+ "json_file = input_file = current_dir.parent / \"misc/danbooru_donmai/danbooru_tags_step_03.json\"\n",
1577
+ "with open(json_file, \"r\", encoding=\"utf-8\") as f:\n",
1578
+ " manual_hierarchy = json.load(f)\n",
1579
+ "\n",
1580
+ "# ✅ Extract all tags from the CSV\n",
1581
+ "csv_tags = set(df[\"name\"].astype(str).str.replace(\"_\", \" \").str.lower()) # Normalize tags\n",
1582
+ "\n",
1583
+ "# ✅ Extract all tags from the JSON recursively\n",
1584
+ "def extract_tags_from_json(data):\n",
1585
+ " tags = set()\n",
1586
+ " if isinstance(data, dict):\n",
1587
+ " for value in data.values():\n",
1588
+ " tags.update(extract_tags_from_json(value))\n",
1589
+ " elif isinstance(data, list):\n",
1590
+ " tags.update(str(tag).replace(\"_\", \" \").lower() for tag in data)\n",
1591
+ " return tags\n",
1592
+ "\n",
1593
+ "json_tags = extract_tags_from_json(manual_hierarchy)\n",
1594
+ "\n",
1595
+ "# ✅ Find tags in CSV but NOT in JSON\n",
1596
+ "missing_tags = csv_tags - json_tags\n",
1597
+ "\n",
1598
+ "# ✅ Print the missing tags\n",
1599
+ "print(\"\\n📌 Tags in CSV but NOT in JSON:\")\n",
1600
+ "for tag in sorted(missing_tags):\n",
1601
+ " print(tag)\n",
1602
+ "\n",
1603
+ "# ✅ Save missing tags to a file for review (optional)\n",
1604
+ "missing_tags_file = \"missing_tags.txt\"\n",
1605
+ "with open(missing_tags_file, \"w\", encoding=\"utf-8\") as f:\n",
1606
+ " f.write(\"\\n\".join(sorted(missing_tags)))\n",
1607
+ "\n",
1608
+ "print(f\"\\n✅ Missing tags saved to `{missing_tags_file}`\")\n"
1609
+ ]
1610
+ },
1611
+ {
1612
+ "cell_type": "markdown",
1613
+ "id": "706317c1-498d-40ab-a29a-ee658e1735e2",
1614
+ "metadata": {
1615
+ "execution": {
1616
+ "iopub.execute_input": "2025-03-17T10:53:52.106543Z",
1617
+ "iopub.status.busy": "2025-03-17T10:53:52.105944Z",
1618
+ "iopub.status.idle": "2025-03-17T10:53:52.108812Z",
1619
+ "shell.execute_reply": "2025-03-17T10:53:52.108451Z",
1620
+ "shell.execute_reply.started": "2025-03-17T10:53:52.106526Z"
1621
+ }
1622
+ },
1623
+ "source": [
1624
+ "### Fetch wikidata for collected tags"
1625
+ ]
1626
+ },
1627
+ {
1628
+ "cell_type": "code",
1629
+ "execution_count": null,
1630
+ "id": "ec696e5b-be39-4407-a04f-6ce6b0b08855",
1631
+ "metadata": {
1632
+ "execution": {
1633
+ "execution_failed": "2025-03-17T12:46:52.921Z",
1634
+ "iopub.execute_input": "2025-03-17T12:45:32.542088Z",
1635
+ "iopub.status.busy": "2025-03-17T12:45:32.541554Z"
1636
+ }
1637
+ },
1638
+ "outputs": [
1639
+ {
1640
+ "name": "stdout",
1641
+ "output_type": "stream",
1642
+ "text": [
1643
+ "✅ Extracted 35559 unique tags.\n",
1644
+ "🚀 Fetching data for amamiya_elena (1/35559)...\n",
1645
+ "✅ Saved amamiya_elena to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/amamiya_elena.json\n",
1646
+ "🚀 Fetching data for gilles_de_rais (2/35559)...\n",
1647
+ "✅ Saved gilles_de_rais to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/gilles_de_rais.json\n",
1648
+ "🚀 Fetching data for blue_scrunchie (3/35559)...\n",
1649
+ "✅ Saved blue_scrunchie to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/blue_scrunchie.json\n",
1650
+ "🚀 Fetching data for playing (4/35559)...\n",
1651
+ "✅ Saved playing to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/playing.json\n",
1652
+ "🚀 Fetching data for album_cover_redraw (5/35559)...\n",
1653
+ "✅ Saved album_cover_redraw to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/album_cover_redraw.json\n",
1654
+ "🚀 Fetching data for >3< (6/35559)...\n",
1655
+ "✅ Saved >3< to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/>3<.json\n",
1656
+ "🚀 Fetching data for damom (7/35559)...\n",
1657
+ "🚀 Fetching data for shoulder_cannon (8/35559)...\n",
1658
+ "✅ Saved shoulder_cannon to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/shoulder_cannon.json\n",
1659
+ "🚀 Fetching data for cover_image (9/35559)...\n",
1660
+ "✅ Saved cover_image to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/cover_image.json\n",
1661
+ "🚀 Fetching data for ultraman_legend (10/35559)...\n",
1662
+ "✅ Saved ultraman_legend to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/ultraman_legend.json\n",
1663
+ "🚀 Fetching data for mastiff (11/35559)...\n",
1664
+ "✅ Saved mastiff to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/mastiff.json\n",
1665
+ "🚀 Fetching data for oingo (12/35559)...\n",
1666
+ "✅ Saved oingo to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/oingo.json\n",
1667
+ "🚀 Fetching data for mario_strikers_(series) (13/35559)...\n",
1668
+ "✅ Saved mario_strikers_(series) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/mario_strikers_(series).json\n",
1669
+ "🚀 Fetching data for phallic_symbol (14/35559)...\n",
1670
+ "✅ Saved phallic_symbol to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/phallic_symbol.json\n",
1671
+ "🚀 Fetching data for 4girls (15/35559)...\n",
1672
+ "✅ Saved 4girls to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/4girls.json\n",
1673
+ "🚀 Fetching data for nice_nature_(racehorse) (16/35559)...\n",
1674
+ "✅ Saved nice_nature_(racehorse) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/nice_nature_(racehorse).json\n",
1675
+ "🚀 Fetching data for hair_beads (17/35559)...\n",
1676
+ "✅ Saved hair_beads to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/hair_beads.json\n",
1677
+ "🚀 Fetching data for fu_po_(azur_lane) (18/35559)...\n",
1678
+ "✅ Saved fu_po_(azur_lane) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/fu_po_(azur_lane).json\n",
1679
+ "🚀 Fetching data for togepi (19/35559)...\n",
1680
+ "✅ Saved togepi to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/togepi.json\n",
1681
+ "🚀 Fetching data for yasopp (20/35559)...\n",
1682
+ "✅ Saved yasopp to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/yasopp.json\n",
1683
+ "🚀 Fetching data for oyafune_suama (21/35559)...\n",
1684
+ "✅ Saved oyafune_suama to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/oyafune_suama.json\n",
1685
+ "🚀 Fetching data for phantasy_star_iii (22/35559)...\n",
1686
+ "✅ Saved phantasy_star_iii to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/phantasy_star_iii.json\n",
1687
+ "🚀 Fetching data for qingdai_guanmao (23/35559)...\n",
1688
+ "✅ Saved qingdai_guanmao to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/qingdai_guanmao.json\n",
1689
+ "🚀 Fetching data for kufei (24/35559)...\n",
1690
+ "✅ Saved kufei to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/kufei.json\n",
1691
+ "🚀 Fetching data for stefan_(atelier) (25/35559)...\n",
1692
+ "✅ Saved stefan_(atelier) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/stefan_(atelier).json\n",
1693
+ "🚀 Fetching data for dille_blood (26/35559)...\n",
1694
+ "✅ Saved dille_blood to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/dille_blood.json\n",
1695
+ "🚀 Fetching data for vivillon_(modern) (27/35559)...\n",
1696
+ "✅ Saved vivillon_(modern) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/vivillon_(modern).json\n",
1697
+ "🚀 Fetching data for sweetie_(ragnarok_online) (28/35559)...\n",
1698
+ "✅ Saved sweetie_(ragnarok_online) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/sweetie_(ragnarok_online).json\n",
1699
+ "🚀 Fetching data for whisking (29/35559)...\n",
1700
+ "✅ Saved whisking to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/whisking.json\n",
1701
+ "🚀 Fetching data for h&k_hk33 (30/35559)...\n",
1702
+ "✅ Saved h&k_hk33 to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/h&k_hk33.json\n",
1703
+ "🚀 Fetching data for winx_club (31/35559)...\n",
1704
+ "✅ Saved winx_club to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/winx_club.json\n",
1705
+ "🚀 Fetching data for anti-tank_grenade (32/35559)...\n",
1706
+ "✅ Saved anti-tank_grenade to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/anti-tank_grenade.json\n",
1707
+ "🚀 Fetching data for devin_booker (33/35559)...\n",
1708
+ "✅ Saved devin_booker to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/devin_booker.json\n",
1709
+ "🚀 Fetching data for scylla_(azur_lane) (34/35559)...\n",
1710
+ "✅ Saved scylla_(azur_lane) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/scylla_(azur_lane).json\n",
1711
+ "🚀 Fetching data for penance_(arknights) (35/35559)...\n",
1712
+ "✅ Saved penance_(arknights) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/penance_(arknights).json\n",
1713
+ "🚀 Fetching data for toba_(oshiro_project) (36/35559)...\n",
1714
+ "✅ Saved toba_(oshiro_project) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/toba_(oshiro_project).json\n",
1715
+ "🚀 Fetching data for scott_adams_(style) (37/35559)...\n",
1716
+ "✅ Saved scott_adams_(style) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/scott_adams_(style).json\n",
1717
+ "🚀 Fetching data for 502nd_joint_fighter_wing (38/35559)...\n",
1718
+ "✅ Saved 502nd_joint_fighter_wing to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/502nd_joint_fighter_wing.json\n",
1719
+ "🚀 Fetching data for komica_wiki (39/35559)...\n",
1720
+ "✅ Saved komica_wiki to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/komica_wiki.json\n",
1721
+ "🚀 Fetching data for final_fantasy_vi (40/35559)...\n",
1722
+ "✅ Saved final_fantasy_vi to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/final_fantasy_vi.json\n",
1723
+ "🚀 Fetching data for h&k_hk45 (41/35559)...\n",
1724
+ "✅ Saved h&k_hk45 to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/h&k_hk45.json\n",
1725
+ "🚀 Fetching data for saint_seiya (42/35559)...\n",
1726
+ "✅ Saved saint_seiya to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/saint_seiya.json\n",
1727
+ "🚀 Fetching data for ike_(fire_emblem) (43/35559)...\n",
1728
+ "✅ Saved ike_(fire_emblem) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/ike_(fire_emblem).json\n",
1729
+ "🚀 Fetching data for cooperative_breast_smother (44/35559)...\n",
1730
+ "✅ Saved cooperative_breast_smother to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/cooperative_breast_smother.json\n",
1731
+ "🚀 Fetching data for pamiat_merkuria_(azur_lane) (45/35559)...\n",
1732
+ "✅ Saved pamiat_merkuria_(azur_lane) to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/pamiat_merkuria_(azur_lane).json\n",
1733
+ "🚀 Fetching data for ribbon-trimmed_skirt (46/35559)...\n",
1734
+ "✅ Saved ribbon-trimmed_skirt to /shares/weddigen.ki.uzh/laura_wagner/Civitai_page_analysis/Civitai_visualizations/misc/danbooru_donmai/tags_wiki/ribbon-trimmed_skirt.json\n"
1735
+ ]
1736
+ }
1737
+ ],
1738
+ "source": [
1739
+ "import json\n",
1740
+ "import os\n",
1741
+ "import requests\n",
1742
+ "import time\n",
1743
+ "import urllib.parse\n",
1744
+ "\n",
1745
+ "# Load the cleaned hierarchy JSON\n",
1746
+ "json_file_path = current_dir.parent / \"misc/danbooru_donmai/danbooru_tags_step_03.json\"\n",
1747
+ "with open(json_file_path, \"r\", encoding=\"utf-8\") as file:\n",
1748
+ " tag_hierarchy = json.load(file)\n",
1749
+ "\n",
1750
+ "# Authentication (Replace with your credentials)\n",
1751
+ "USERNAME = username\n",
1752
+ "API_KEY = api_key\n",
1753
+ "\n",
1754
+ "# Base API URL\n",
1755
+ "BASE_URL = \"https://danbooru.donmai.us\"\n",
1756
+ "\n",
1757
+ "# Output folder for JSON results\n",
1758
+ "output_folder = current_dir.parent / \"misc/danbooru_donmai/tags_wiki\"\n",
1759
+ "os.makedirs(output_folder, exist_ok=True) # Ensure the directory exists\n",
1760
+ "\n",
1761
+ "# Function to extract all unique tags from nested dictionaries/lists\n",
1762
+ "def extract_tags(data):\n",
1763
+ " tags = set()\n",
1764
+ " if isinstance(data, dict):\n",
1765
+ " for key, value in data.items():\n",
1766
+ " tags.update(extract_tags(value)) # Recursively extract tags\n",
1767
+ " elif isinstance(data, list):\n",
1768
+ " for item in data:\n",
1769
+ " if isinstance(item, str):\n",
1770
+ " formatted_tag = item.lower().replace(\" \", \"_\") # ✅ Convert to lowercase and replace spaces\n",
1771
+ " tags.add(formatted_tag)\n",
1772
+ " else:\n",
1773
+ " tags.update(extract_tags(item)) # Handle nested lists\n",
1774
+ " return tags\n",
1775
+ "\n",
1776
+ "# Extract all unique tags\n",
1777
+ "all_tags = extract_tags(tag_hierarchy)\n",
1778
+ "print(f\"✅ Extracted {len(all_tags)} unique tags.\")\n",
1779
+ "\n",
1780
+ "# API query function\n",
1781
+ "def fetch_tag_data(tag):\n",
1782
+ " \"\"\"Fetch tag details from Danbooru API.\"\"\"\n",
1783
+ " encoded_tag = urllib.parse.quote(tag, safe=\"\")\n",
1784
+ " api_url = f\"{BASE_URL}/tags.json?search[name]={encoded_tag}&only=id,name,category,post_count,is_deprecated,created_at,updated_at,wiki_page,artist,antecedent_alias,consequent_aliases,antecedent_implications,consequent_implications,dtext_links\"\n",
1785
+ "\n",
1786
+ " try:\n",
1787
+ " response = requests.get(api_url, auth=(USERNAME, API_KEY))\n",
1788
+ " response.raise_for_status()\n",
1789
+ " return response.json()\n",
1790
+ " except requests.exceptions.RequestException as e:\n",
1791
+ " print(f\"⚠️ Error fetching data for '{tag}': {e}\")\n",
1792
+ " return None\n",
1793
+ "\n",
1794
+ "# Process each tag\n",
1795
+ "for idx, tag in enumerate(all_tags):\n",
1796
+ " tag_filename = os.path.join(output_folder, f\"{tag}.json\")\n",
1797
+ "\n",
1798
+ " # Skip if the file already exists to avoid redundant API calls\n",
1799
+ " if os.path.exists(tag_filename):\n",
1800
+ " print(f\"🔄 Skipping {tag}, already saved.\")\n",
1801
+ " continue\n",
1802
+ "\n",
1803
+ " print(f\"🚀 Fetching data for {tag} ({idx+1}/{len(all_tags)})...\")\n",
1804
+ "\n",
1805
+ " tag_data = fetch_tag_data(tag)\n",
1806
+ "\n",
1807
+ " if tag_data:\n",
1808
+ " with open(tag_filename, \"w\", encoding=\"utf-8\") as f:\n",
1809
+ " json.dump(tag_data, f, indent=4, ensure_ascii=False)\n",
1810
+ " print(f\"✅ Saved {tag} to {tag_filename}\")\n",
1811
+ "\n",
1812
+ " # Respect API rate limits\n",
1813
+ " time.sleep(1.5) # Adjust delay if necessary\n",
1814
+ "\n",
1815
+ "print(\"\\n✅ All tags processed and saved in the 'tags/' folder.\")\n"
1816
+ ]
1817
+ },
1818
+ {
1819
+ "cell_type": "code",
1820
+ "execution_count": null,
1821
+ "id": "8e364060-36e9-4418-8bdd-6018c9edcc33",
1822
+ "metadata": {},
1823
+ "outputs": [],
1824
+ "source": []
1825
+ }
1826
+ ],
1827
+ "metadata": {
1828
+ "kernelspec": {
1829
+ "display_name": "latm",
1830
+ "language": "python",
1831
+ "name": "python3"
1832
+ },
1833
+ "language_info": {
1834
+ "codemirror_mode": {
1835
+ "name": "ipython",
1836
+ "version": 3
1837
+ },
1838
+ "file_extension": ".py",
1839
+ "mimetype": "text/x-python",
1840
+ "name": "python",
1841
+ "nbconvert_exporter": "python",
1842
+ "pygments_lexer": "ipython3",
1843
+ "version": "3.10.15"
1844
+ }
1845
+ },
1846
+ "nbformat": 4,
1847
+ "nbformat_minor": 5
1848
+ }
jupyter_notebooks/SuppM_Figure_S14_co-occurence_training_data.ipynb ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Prepare *.json for Figure 13"
8
+ ]
9
+ },
10
+ {
11
+ "cell_type": "code",
12
+ "execution_count": null,
13
+ "metadata": {},
14
+ "outputs": [],
15
+ "source": [
16
+ "import pandas as pd\n",
17
+ "import json\n",
18
+ "from itertools import combinations\n",
19
+ "from collections import Counter\n",
20
+ "\n",
21
+ "# Load the data with only needed columns\n",
22
+ "csv_file_path = \"all_models_with_tags.csv\" # Update with your actual file path\n",
23
+ "\n",
24
+ "df = pd.read_csv(csv_file_path, low_memory=False)\n",
25
+ "\n",
26
+ "# Identify the starting point of tag columns (after 'civitai_id')\n",
27
+ "civitai_index = df.columns.get_loc(\"civitai_id\") + 1\n",
28
+ "\n",
29
+ "df_tags = df.iloc[:, civitai_index:]\n",
30
+ "\n",
31
+ "# Extract tag columns (tag01 to tag199) and corresponding counts\n",
32
+ "tag_columns = [col for col in df_tags.columns if col.startswith(\"tag\") and col[3:].isdigit() and int(col[3:]) <= 199]\n",
33
+ "tag_no_columns = [col for col in df_tags.columns if col.startswith(\"tag\") and col.endswith(\"_no\")]\n",
34
+ "\n",
35
+ "if not tag_columns:\n",
36
+ " print(\"No tag columns found in the dataset.\")\n",
37
+ " exit()\n",
38
+ "\n",
39
+ "df_tags = df[tag_columns]\n",
40
+ "df_tag_counts = df[tag_no_columns] if tag_no_columns else None\n",
41
+ "\n",
42
+ "# Load tag categories from JSON\n",
43
+ "json_file_path = \"danbooru_tags_step_03.json\" # Update with your actual file path\n",
44
+ "with open(json_file_path, \"r\", encoding=\"utf-8\") as f:\n",
45
+ " tag_categories = json.load(f)\n",
46
+ "\n",
47
+ "# Function to normalize tags (lowercase and replace underscores)\n",
48
+ "def normalize_tag(tag):\n",
49
+ " return tag.lower().replace(\"_\", \" \") if isinstance(tag, str) else tag\n",
50
+ "\n",
51
+ "# Flatten JSON structure to map tags to top-level categories\n",
52
+ "def extract_tag_categories(json_data, current_category=None):\n",
53
+ " tag_mapping = {}\n",
54
+ " for key, value in json_data.items():\n",
55
+ " if isinstance(value, dict):\n",
56
+ " tag_mapping.update(extract_tag_categories(value, key))\n",
57
+ " elif isinstance(value, list):\n",
58
+ " for tag in value:\n",
59
+ " normalized_tag = normalize_tag(tag)\n",
60
+ " if normalized_tag:\n",
61
+ " tag_mapping[normalized_tag] = current_category\n",
62
+ " return tag_mapping\n",
63
+ "\n",
64
+ "# Create mapping of tags to categories\n",
65
+ "tag_category_mapping = extract_tag_categories(tag_categories)\n",
66
+ "\n",
67
+ "# Flatten and count occurrences of individual tags\n",
68
+ "all_tags = []\n",
69
+ "tag_counts = Counter()\n",
70
+ "\n",
71
+ "for i, row in df_tags.iterrows():\n",
72
+ " tags = [normalize_tag(tag) for tag in row if pd.notna(tag)]\n",
73
+ " if df_tag_counts is not None:\n",
74
+ " counts = df_tag_counts.iloc[i].fillna(1).tolist()\n",
75
+ " else:\n",
76
+ " counts = [1] * len(tags)\n",
77
+ " for tag, count in zip(tags, counts):\n",
78
+ " all_tags.append(tag)\n",
79
+ " tag_counts[tag] += count\n",
80
+ "\n",
81
+ "if not all_tags:\n",
82
+ " print(\"No valid tags found in the dataset.\")\n",
83
+ " exit()\n",
84
+ "print(f\"Extracted {len(all_tags)} total tags.\")\n",
85
+ "\n",
86
+ "# Compute co-occurrence frequencies efficiently\n",
87
+ "co_occurrences = Counter()\n",
88
+ "for i, row in df_tags.iterrows():\n",
89
+ " tags = [normalize_tag(tag) for tag in row if pd.notna(tag)]\n",
90
+ " if len(tags) < 2:\n",
91
+ " continue # Skip rows with fewer than two tags\n",
92
+ " for tag1, tag2 in combinations(tags, 2):\n",
93
+ " co_occurrences[frozenset([tag1, tag2])] += 1\n",
94
+ "\n",
95
+ "if not co_occurrences:\n",
96
+ " print(\"No co-occurrence data found.\")\n",
97
+ " exit()\n",
98
+ "print(f\"Computed {len(co_occurrences)} co-occurrence pairs.\")\n",
99
+ "\n",
100
+ "# Convert co-occurrence counts to a weighted edge list\n",
101
+ "edges = [(tuple(pair)[0], tuple(pair)[1], weight) for pair, weight in co_occurrences.items() if len(pair) == 2]\n",
102
+ "\n",
103
+ "# Create a set of connected tags with a minimum connection threshold\n",
104
+ "min_connections = 5 # Increased threshold to reduce memory usage\n",
105
+ "connected_tags = Counter()\n",
106
+ "for tag1, tag2, _ in edges:\n",
107
+ " connected_tags[tag1] += 1\n",
108
+ " connected_tags[tag2] += 1\n",
109
+ "\n",
110
+ "# Filter nodes to keep only those that meet the minimum connection threshold\n",
111
+ "nodes = [\n",
112
+ " {\"id\": tag, \"size\": count, \"category\": tag_category_mapping.get(tag, \"unknown\")}\n",
113
+ " for tag, count in tag_counts.items()\n",
114
+ " if connected_tags[tag] >= min_connections\n",
115
+ "]\n",
116
+ "\n",
117
+ "if not nodes:\n",
118
+ " print(\"No nodes meet the connection threshold.\")\n",
119
+ " exit()\n",
120
+ "print(f\"Final node count: {len(nodes)}\")\n",
121
+ "\n",
122
+ "links = [\n",
123
+ " {\"source\": tag1, \"target\": tag2, \"value\": weight}\n",
124
+ " for tag1, tag2, weight in edges\n",
125
+ " if connected_tags[tag1] >= min_connections and connected_tags[tag2] >= min_connections\n",
126
+ "]\n",
127
+ "\n",
128
+ "if not links:\n",
129
+ " print(\"No links meet the connection threshold.\")\n",
130
+ " exit()\n",
131
+ "print(f\"Final link count: {len(links)}\")\n",
132
+ "\n",
133
+ "# Prepare JSON output\n",
134
+ "d3_data = {\"nodes\": nodes, \"links\": links}\n",
135
+ "\n",
136
+ "# Save as JSON for visualization\n",
137
+ "output_file = \"co_occurrence_network.json\"\n",
138
+ "with open(output_file, \"w\", encoding=\"utf-8\") as f:\n",
139
+ " json.dump(d3_data, f, indent=4)\n",
140
+ "\n",
141
+ "print(f\"D3.js data saved to {output_file}\")"
142
+ ]
143
+ }
144
+ ],
145
+ "metadata": {
146
+ "language_info": {
147
+ "name": "python"
148
+ }
149
+ },
150
+ "nbformat": 4,
151
+ "nbformat_minor": 2
152
+ }
misc/api_keys_example.txt DELETED
@@ -1,2 +0,0 @@
1
- api_key_01
2
- api_key_02
 
 
 
misc/danbooru/tag_info_test/wombat_tag_info.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tag_details": [
3
+ {
4
+ "id": 462478,
5
+ "name": "wombat",
6
+ "post_count": 32,
7
+ "category": 0,
8
+ "created_at": "2013-02-28T04:35:29.653-05:00",
9
+ "updated_at": "2019-08-30T03:37:52.993-04:00",
10
+ "is_deprecated": false,
11
+ "consequent_aliases": [],
12
+ "antecedent_implications": [],
13
+ "consequent_implications": [],
14
+ "dtext_links": [
15
+ {
16
+ "id": 11937824,
17
+ "created_at": "2024-07-30T13:49:16.169-04:00",
18
+ "updated_at": "2024-07-30T13:49:16.169-04:00",
19
+ "model_type": "WikiPage",
20
+ "model_id": 57363,
21
+ "link_type": "wiki_link",
22
+ "link_target": "wombat"
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
misc/deepseek_query_index.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ 4
misc/deletemememe.txt DELETED
The diff for this file is too large to render. See raw diff
 
misc/lists/countries.csv ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ num_code,alpha_2_code,alpha_3_code,en_short_name,nationality
2
+ 4,AF,AFG,Afghanistan,Afghan
3
+ 248,AX,ALA,Åland Islands,Åland Island
4
+ 8,AL,ALB,Albania,Albanian
5
+ 12,DZ,DZA,Algeria,Algerian
6
+ 16,AS,ASM,American Samoa,American Samoan
7
+ 20,AD,AND,Andorra,Andorran
8
+ 24,AO,AGO,Angola,Angolan
9
+ 660,AI,AIA,Anguilla,Anguillan
10
+ 10,AQ,ATA,Antarctica,Antarctic
11
+ 28,AG,ATG,Antigua and Barbuda,Antiguan or Barbudan
12
+ 32,AR,ARG,Argentina,Argentine
13
+ 51,AM,ARM,Armenia,Armenian
14
+ 533,AW,ABW,Aruba,Aruban
15
+ 36,AU,AUS,Australia,Australian
16
+ 40,AT,AUT,Austria,Austrian
17
+ 31,AZ,AZE,Azerbaijan,"Azerbaijani, Azeri"
18
+ 44,BS,BHS,Bahamas,Bahamian
19
+ 48,BH,BHR,Bahrain,Bahraini
20
+ 50,BD,BGD,Bangladesh,Bangladeshi
21
+ 52,BB,BRB,Barbados,Barbadian
22
+ 112,BY,BLR,Belarus,Belarusian
23
+ 56,BE,BEL,Belgium,Belgian
24
+ 84,BZ,BLZ,Belize,Belizean
25
+ 204,BJ,BEN,Benin,"Beninese, Beninois"
26
+ 60,BM,BMU,Bermuda,"Bermudian, Bermudan"
27
+ 64,BT,BTN,Bhutan,Bhutanese
28
+ 68,BO,BOL,Bolivia (Plurinational State of),Bolivian
29
+ 535,BQ,BES,"Bonaire, Sint Eustatius and Saba",Bonaire
30
+ 70,BA,BIH,Bosnia and Herzegovina,Bosnian or Herzegovinian
31
+ 72,BW,BWA,Botswana,"Motswana, Botswanan"
32
+ 74,BV,BVT,Bouvet Island,Bouvet Island
33
+ 76,BR,BRA,Brazil,Brazilian
34
+ 86,IO,IOT,British Indian Ocean Territory,BIOT
35
+ 96,BN,BRN,Brunei Darussalam,Bruneian
36
+ 100,BG,BGR,Bulgaria,Bulgarian
37
+ 854,BF,BFA,Burkina Faso,Burkinabé
38
+ 108,BI,BDI,Burundi,Burundian
39
+ 132,CV,CPV,Cabo Verde,Cabo Verdean
40
+ 116,KH,KHM,Cambodia,Cambodian
41
+ 120,CM,CMR,Cameroon,Cameroonian
42
+ 124,CA,CAN,Canada,Canadian
43
+ 136,KY,CYM,Cayman Islands,Caymanian
44
+ 140,CF,CAF,Central African Republic,Central African
45
+ 148,TD,TCD,Chad,Chadian
46
+ 152,CL,CHL,Chile,Chilean
47
+ 156,CN,CHN,China,Chinese
48
+ 162,CX,CXR,Christmas Island,Christmas Island
49
+ 166,CC,CCK,Cocos (Keeling) Islands,Cocos Island
50
+ 170,CO,COL,Colombia,Colombian
51
+ 174,KM,COM,Comoros,"Comoran, Comorian"
52
+ 178,CG,COG,Congo (Republic of the),Congolese
53
+ 180,CD,COD,Congo (Democratic Republic of the),Congolese
54
+ 184,CK,COK,Cook Islands,Cook Island
55
+ 188,CR,CRI,Costa Rica,Costa Rican
56
+ 384,CI,CIV,Côte d'Ivoire,Ivorian
57
+ 191,HR,HRV,Croatia,Croatian
58
+ 192,CU,CUB,Cuba,Cuban
59
+ 531,CW,CUW,Curaçao,Curaçaoan
60
+ 196,CY,CYP,Cyprus,Cypriot
61
+ 203,CZ,CZE,Czech Republic,Czech
62
+ 208,DK,DNK,Denmark,Danish
63
+ 262,DJ,DJI,Djibouti,Djiboutian
64
+ 212,DM,DMA,Dominica,Dominican
65
+ 214,DO,DOM,Dominican Republic,Dominican
66
+ 218,EC,ECU,Ecuador,Ecuadorian
67
+ 818,EG,EGY,Egypt,Egyptian
68
+ 222,SV,SLV,El Salvador,Salvadoran
69
+ 226,GQ,GNQ,Equatorial Guinea,"Equatorial Guinean, Equatoguinean"
70
+ 232,ER,ERI,Eritrea,Eritrean
71
+ 233,EE,EST,Estonia,Estonian
72
+ 231,ET,ETH,Ethiopia,Ethiopian
73
+ 238,FK,FLK,Falkland Islands (Malvinas),Falkland Island
74
+ 234,FO,FRO,Faroe Islands,Faroese
75
+ 242,FJ,FJI,Fiji,Fijian
76
+ 246,FI,FIN,Finland,Finnish
77
+ 250,FR,FRA,France,French
78
+ 254,GF,GUF,French Guiana,French Guianese
79
+ 258,PF,PYF,French Polynesia,French Polynesian
80
+ 260,TF,ATF,French Southern Territories,French Southern Territories
81
+ 266,GA,GAB,Gabon,Gabonese
82
+ 270,GM,GMB,Gambia,Gambian
83
+ 268,GE,GEO,Georgia,Georgian
84
+ 276,DE,DEU,Germany,German
85
+ 288,GH,GHA,Ghana,Ghanaian
86
+ 292,GI,GIB,Gibraltar,Gibraltar
87
+ 300,GR,GRC,Greece,"Greek, Hellenic"
88
+ 304,GL,GRL,Greenland,Greenlandic
89
+ 308,GD,GRD,Grenada,Grenadian
90
+ 312,GP,GLP,Guadeloupe,Guadeloupe
91
+ 316,GU,GUM,Guam,"Guamanian, Guambat"
92
+ 320,GT,GTM,Guatemala,Guatemalan
93
+ 831,GG,GGY,Guernsey,Channel Island
94
+ 324,GN,GIN,Guinea,Guinean
95
+ 624,GW,GNB,Guinea-Bissau,Bissau-Guinean
96
+ 328,GY,GUY,Guyana,Guyanese
97
+ 332,HT,HTI,Haiti,Haitian
98
+ 334,HM,HMD,Heard Island and McDonald Islands,Heard Island or McDonald Islands
99
+ 336,VA,VAT,Vatican City State,Vatican
100
+ 340,HN,HND,Honduras,Honduran
101
+ 344,HK,HKG,Hong Kong,"Hong Kong, Hong Kongese"
102
+ 348,HU,HUN,Hungary,"Hungarian, Magyar"
103
+ 352,IS,ISL,Iceland,Icelandic
104
+ 356,IN,IND,India,Indian
105
+ 360,ID,IDN,Indonesia,Indonesian
106
+ 364,IR,IRN,Iran,"Iranian, Persian"
107
+ 368,IQ,IRQ,Iraq,Iraqi
108
+ 372,IE,IRL,Ireland,Irish
109
+ 833,IM,IMN,Isle of Man,Manx
110
+ 376,IL,ISR,Israel,Israeli
111
+ 380,IT,ITA,Italy,Italian
112
+ 388,JM,JAM,Jamaica,Jamaican
113
+ 392,JP,JPN,Japan,Japanese
114
+ 832,JE,JEY,Jersey,Channel Island
115
+ 400,JO,JOR,Jordan,Jordanian
116
+ 398,KZ,KAZ,Kazakhstan,"Kazakhstani, Kazakh"
117
+ 404,KE,KEN,Kenya,Kenyan
118
+ 296,KI,KIR,Kiribati,I-Kiribati
119
+ 408,KP,PRK,Korea (Democratic People's Republic of),North Korean
120
+ 410,KR,KOR,Korea (Republic of),South Korean
121
+ 414,KW,KWT,Kuwait,Kuwaiti
122
+ 417,KG,KGZ,Kyrgyzstan,"Kyrgyzstani, Kyrgyz, Kirgiz, Kirghiz"
123
+ 418,LA,LAO,Lao People's Democratic Republic,"Lao, Laotian"
124
+ 428,LV,LVA,Latvia,Latvian
125
+ 422,LB,LBN,Lebanon,Lebanese
126
+ 426,LS,LSO,Lesotho,Basotho
127
+ 430,LR,LBR,Liberia,Liberian
128
+ 434,LY,LBY,Libya,Libyan
129
+ 438,LI,LIE,Liechtenstein,Liechtenstein
130
+ 440,LT,LTU,Lithuania,Lithuanian
131
+ 442,LU,LUX,Luxembourg,"Luxembourg, Luxembourgish"
132
+ 446,MO,MAC,Macao,"Macanese, Chinese"
133
+ 807,MK,MKD,Macedonia (the former Yugoslav Republic of),Macedonian
134
+ 450,MG,MDG,Madagascar,Malagasy
135
+ 454,MW,MWI,Malawi,Malawian
136
+ 458,MY,MYS,Malaysia,Malaysian
137
+ 462,MV,MDV,Maldives,Maldivian
138
+ 466,ML,MLI,Mali,"Malian, Malinese"
139
+ 470,MT,MLT,Malta,Maltese
140
+ 584,MH,MHL,Marshall Islands,Marshallese
141
+ 474,MQ,MTQ,Martinique,"Martiniquais, Martinican"
142
+ 478,MR,MRT,Mauritania,Mauritanian
143
+ 480,MU,MUS,Mauritius,Mauritian
144
+ 175,YT,MYT,Mayotte,Mahoran
145
+ 484,MX,MEX,Mexico,Mexican
146
+ 583,FM,FSM,Micronesia (Federated States of),Micronesian
147
+ 498,MD,MDA,Moldova (Republic of),Moldovan
148
+ 492,MC,MCO,Monaco,"Monégasque, Monacan"
149
+ 496,MN,MNG,Mongolia,Mongolian
150
+ 499,ME,MNE,Montenegro,Montenegrin
151
+ 500,MS,MSR,Montserrat,Montserratian
152
+ 504,MA,MAR,Morocco,Moroccan
153
+ 508,MZ,MOZ,Mozambique,Mozambican
154
+ 104,MM,MMR,Myanmar,Burmese
155
+ 516,NA,NAM,Namibia,Namibian
156
+ 520,NR,NRU,Nauru,Nauruan
157
+ 524,NP,NPL,Nepal,"Nepali, Nepalese"
158
+ 528,NL,NLD,Netherlands,"Dutch, Netherlandic"
159
+ 540,NC,NCL,New Caledonia,New Caledonian
160
+ 554,NZ,NZL,New Zealand,"New Zealand, NZ"
161
+ 558,NI,NIC,Nicaragua,Nicaraguan
162
+ 562,NE,NER,Niger,Nigerien
163
+ 566,NG,NGA,Nigeria,Nigerian
164
+ 570,NU,NIU,Niue,Niuean
165
+ 574,NF,NFK,Norfolk Island,Norfolk Island
166
+ 580,MP,MNP,Northern Mariana Islands,Northern Marianan
167
+ 578,NO,NOR,Norway,Norwegian
168
+ 512,OM,OMN,Oman,Omani
169
+ 586,PK,PAK,Pakistan,Pakistani
170
+ 585,PW,PLW,Palau,Palauan
171
+ 275,PS,PSE,"Palestine, State of",Palestinian
172
+ 591,PA,PAN,Panama,Panamanian
173
+ 598,PG,PNG,Papua New Guinea,"Papua New Guinean, Papuan"
174
+ 600,PY,PRY,Paraguay,Paraguayan
175
+ 604,PE,PER,Peru,Peruvian
176
+ 608,PH,PHL,Philippines,"Philippine, Filipino"
177
+ 612,PN,PCN,Pitcairn,Pitcairn Island
178
+ 616,PL,POL,Poland,Polish
179
+ 620,PT,PRT,Portugal,Portuguese
180
+ 630,PR,PRI,Puerto Rico,Puerto Rican
181
+ 634,QA,QAT,Qatar,Qatari
182
+ 638,RE,REU,Réunion,"Réunionese, Réunionnais"
183
+ 642,RO,ROU,Romania,Romanian
184
+ 643,RU,RUS,Russian Federation,Russian
185
+ 646,RW,RWA,Rwanda,Rwandan
186
+ 652,BL,BLM,Saint Barthélemy,Barthélemois
187
+ 654,SH,SHN,"Saint Helena, Ascension and Tristan da Cunha",Saint Helenian
188
+ 659,KN,KNA,Saint Kitts and Nevis,Kittitian or Nevisian
189
+ 662,LC,LCA,Saint Lucia,Saint Lucian
190
+ 663,MF,MAF,Saint Martin (French part),Saint-Martinoise
191
+ 666,PM,SPM,Saint Pierre and Miquelon,Saint-Pierrais or Miquelonnais
192
+ 670,VC,VCT,Saint Vincent and the Grenadines,"Saint Vincentian, Vincentian"
193
+ 882,WS,WSM,Samoa,Samoan
194
+ 674,SM,SMR,San Marino,Sammarinese
195
+ 678,ST,STP,Sao Tome and Principe,São Toméan
196
+ 682,SA,SAU,Saudi Arabia,"Saudi, Saudi Arabian"
197
+ 686,SN,SEN,Senegal,Senegalese
198
+ 688,RS,SRB,Serbia,Serbian
199
+ 690,SC,SYC,Seychelles,Seychellois
200
+ 694,SL,SLE,Sierra Leone,Sierra Leonean
201
+ 702,SG,SGP,Singapore,Singaporean
202
+ 534,SX,SXM,Sint Maarten (Dutch part),Sint Maarten
203
+ 703,SK,SVK,Slovakia,Slovak
204
+ 705,SI,SVN,Slovenia,"Slovenian, Slovene"
205
+ 90,SB,SLB,Solomon Islands,Solomon Island
206
+ 706,SO,SOM,Somalia,"Somali, Somalian"
207
+ 710,ZA,ZAF,South Africa,South African
208
+ 239,GS,SGS,South Georgia and the South Sandwich Islands,South Georgia or South Sandwich Islands
209
+ 728,SS,SSD,South Sudan,South Sudanese
210
+ 724,ES,ESP,Spain,Spanish
211
+ 144,LK,LKA,Sri Lanka,Sri Lankan
212
+ 729,SD,SDN,Sudan,Sudanese
213
+ 740,SR,SUR,Suriname,Surinamese
214
+ 744,SJ,SJM,Svalbard and Jan Mayen,Svalbard
215
+ 748,SZ,SWZ,Swaziland,Swazi
216
+ 752,SE,SWE,Sweden,Swedish
217
+ 756,CH,CHE,Switzerland,Swiss
218
+ 760,SY,SYR,Syrian Arab Republic,Syrian
219
+ 158,TW,TWN,"Taiwan, Province of China","Chinese, Taiwanese"
220
+ 762,TJ,TJK,Tajikistan,Tajikistani
221
+ 834,TZ,TZA,"Tanzania, United Republic of",Tanzanian
222
+ 764,TH,THA,Thailand,Thai
223
+ 626,TL,TLS,Timor-Leste,Timorese
224
+ 768,TG,TGO,Togo,Togolese
225
+ 772,TK,TKL,Tokelau,Tokelauan
226
+ 776,TO,TON,Tonga,Tongan
227
+ 780,TT,TTO,Trinidad and Tobago,Trinidadian or Tobagonian
228
+ 788,TN,TUN,Tunisia,Tunisian
229
+ 792,TR,TUR,Turkey,Turkish
230
+ 795,TM,TKM,Turkmenistan,Turkmen
231
+ 796,TC,TCA,Turks and Caicos Islands,Turks and Caicos Island
232
+ 798,TV,TUV,Tuvalu,Tuvaluan
233
+ 800,UG,UGA,Uganda,Ugandan
234
+ 804,UA,UKR,Ukraine,Ukrainian
235
+ 784,AE,ARE,United Arab Emirates,"Emirati, Emirian, Emiri"
236
+ 826,GB,GBR,United Kingdom of Great Britain and Northern Ireland,"British, UK"
237
+ 581,UM,UMI,United States Minor Outlying Islands,American
238
+ 840,US,USA,United States of America,American
239
+ 858,UY,URY,Uruguay,Uruguayan
240
+ 860,UZ,UZB,Uzbekistan,"Uzbekistani, Uzbek"
241
+ 548,VU,VUT,Vanuatu,"Ni-Vanuatu, Vanuatuan"
242
+ 862,VE,VEN,Venezuela (Bolivarian Republic of),Venezuelan
243
+ 704,VN,VNM,Vietnam,Vietnamese
244
+ 92,VG,VGB,Virgin Islands (British),British Virgin Island
245
+ 850,VI,VIR,Virgin Islands (U.S.),U.S. Virgin Island
246
+ 876,WF,WLF,Wallis and Futuna,"Wallis and Futuna, Wallisian or Futunan"
247
+ 732,EH,ESH,Western Sahara,"Sahrawi, Sahrawian, Sahraouian"
248
+ 887,YE,YEM,Yemen,Yemeni
249
+ 894,ZM,ZMB,Zambia,Zambian
250
+ 716,ZW,ZWE,Zimbabwe,Zimbabwean
misc/lists/danbooru.json ADDED
The diff for this file is too large to render. See raw diff
 
misc/lists/mapped_professions.csv ADDED
@@ -0,0 +1,1074 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ profession_short,category
2
+ Actor,Actor
3
+ R&B singer,"Singer, Musician"
4
+ Adult film actor,Adult Performer
5
+ J-pop singer,"Singer, Musician"
6
+ Adult film actress,Adult Performer
7
+ Paramedic,Other
8
+ American football quarterback,Sports Professional
9
+ Film actor,Actor
10
+ Musician,"Singer, Musician"
11
+ Baseball player,Sports Professional
12
+ Men's Fashion YouTuber,Influencer
13
+ Unknown,Other
14
+ Television actor,Actor
15
+ Public FIgure,Public FIgure
16
+ Television producer,TV Personality
17
+ Social media influencer,Online Personality
18
+ Fashion model,Model
19
+ Singer,"Singer, Musician"
20
+ Model,Model
21
+ Political commentator,TV Personality
22
+ Fitness influencer,Online Personality
23
+ News anchor,TV Personality
24
+ Television actress,Actor
25
+ DJ,"Singer, Musician"
26
+ Instagram Influencer,Online Personality
27
+ Reality TV personality,Fictional Character
28
+ Fighting game character,Fictional Character
29
+ Video game character,Fictional Character
30
+ Stand-up comedian,Actor
31
+ Comedian,TV Personality
32
+ Medical doctor,Other
33
+ Beauty YouTuber,Online Personality
34
+ Instagram model,Online Personality
35
+ Conservative commentator,TV Personality
36
+ Professional footballer,Sports Professional
37
+ Spy,Fictional Character
38
+ Television presenter,TV Personality
39
+ Professional wrestler,Sports Professional
40
+ U.S. Congressman,Public FIgure
41
+ Film producer,Other
42
+ YouTube vlogger,Online Personality
43
+ U.S. Senator,Public FIgure
44
+ Soul singer,"Singer, Musician"
45
+ Blues singer-songwriter,Online Personality
46
+ Streamer,Online Personality
47
+ Instagram influencer,Online Personality
48
+ Superhero,Fictional Character
49
+ Adult content creator,Adult Performer
50
+ Professional soccer player,Sports Professional
51
+ Yoga instructor,Online Personality
52
+ Prince,Other
53
+ YouTuber,Online Personality
54
+ Amphibian,Other
55
+ Sentient program,Fictional Character
56
+ Pro-democracy activist,Public FIgure
57
+ Electronic musician,"Singer, Musician"
58
+ Pop singer,"Singer, Musician"
59
+ Ballet dancer,Performer
60
+ Voice actress,Voice Actor
61
+ K-pop singer,"Singer, Musician"
62
+ Jedi Padawan,Fictional Character
63
+ Table tennis player,Sports Professional
64
+ Gravure model,Model
65
+ Opera singer,Performer
66
+ Jazz singer,"Singer, Musician"
67
+ Singer-songwriter,"Singer, Musician"
68
+ Dancer,Sports Professional
69
+ Lifestyle YouTuber,Online Personality
70
+ JAV actress,Adult Performer
71
+ AV actress,Adult Performer
72
+ Japanese idol,Idol
73
+ Japanese idol singer,"Singer, Musician"
74
+ Gravure idol,Model
75
+ AV idol,Adult Performer
76
+ Poet,Fictional Character
77
+ Politician,Public Figure
78
+ Manga artist,Fictional Character
79
+ Shoe salesman,Other
80
+ Film director,Other
81
+ Formula One racing driver,Sports Professional
82
+ Mathematician,Other
83
+ Video game protagonist,Fictional Character
84
+ Video game journalist,Journalist
85
+ Auror,Fictional Character
86
+ Landscape painter,Fictional Character
87
+ Theoretical physicist,Other
88
+ Headmaster of Hogwarts School of Witchcraft and Wizardry,Fictional Character
89
+ Wrestler,Sports Professional
90
+ Pop singer-songwriter,"Singer, Musician"
91
+ Rock musician,"Singer, Musician"
92
+ Adult film performer,Adult Performer
93
+ Footballer,Sports Professional
94
+ Professional Dancer,Sports Professional
95
+ Film actress,Actor
96
+ Volleyball player,Sports Professional
97
+ Opposition Politician,Public Figure
98
+ Political strategist,Public Figure
99
+ Medical ethicist,Other
100
+ Tennis player,Sports Professional
101
+ Music producer,"Singer, Musician"
102
+ Radio personality,Voice Actor
103
+ Visionary artist,Fictional Character
104
+ Racing driver,Sports Professional
105
+ Child actor,Actor
106
+ Twitch streamer,Online Personality
107
+ Minecraft YouTuber,Online Personality
108
+ Formula One Driver,Sports Professional
109
+ Radio talk show host,Voice Actor
110
+ Software Engineer,Other
111
+ Stage actor,Performer
112
+ Fictional character,Fictional Character
113
+ Podcast host,Voice Actor
114
+ Freelance Writer,Other
115
+ Socialite,Other
116
+ Chess streamer,Other
117
+ Figure skater,Sports Professional
118
+ Firearms instructor,Other
119
+ Student,Other
120
+ Television writer,Other
121
+ U.S. Representative,Public FIgure
122
+ Journalist,Journalist
123
+ YouTube content creator,"Singer, Musician"
124
+ Parody musician,"Singer, Musician"
125
+ Rapper,"Singer, Musician"
126
+ Supreme Leader of Iran,Public Figure
127
+ Track and field athlete,Sports Professional
128
+ Shock rock musician,"Singer, Musician"
129
+ Vampire,Fictional Character
130
+ Radio presenter,Voice Actor
131
+ Historian,Other
132
+ Webcam model,Adult Performer
133
+ Anime character,Fictional Character
134
+ Author's muse,Other
135
+ Anthropologist,Other
136
+ Noblewoman,Fictional Character
137
+ R&B singer-songwriter,"Singer, Musician"
138
+ Bassist,"Singer, Musician"
139
+ Cosplayer,Model
140
+ Lawyer,Other
141
+ Rhythmic gymnast,Sports Professional
142
+ Classical guitarist,"Singer, Musician"
143
+ Playback singer,"Singer, Musician"
144
+ Professional Footballer,Sports Professional
145
+ Digital artist,Other
146
+ Photorealistic digital artist,Other
147
+ Television host,TV Personality
148
+ Science fiction writer,Other
149
+ Metal vocalist,"Singer, Musician"
150
+ Football goalkeeper,Sports Professional
151
+ Satirical comedian,TV Personality
152
+ News Anchor,TV Personality
153
+ Virtual YouTuber,Fictional Character
154
+ Professional tennis player,Sports Professional
155
+ French pop singer,"Singer, Musician"
156
+ Professional basketball player,Sports Professional
157
+ Country music singer,"Singer, Musician"
158
+ Film critic,Other
159
+ Pole vaulter,Sports Professional
160
+ Hunter-gatherer,Other
161
+ Organized crime boss,Other
162
+ Photorealistic character artist,Other
163
+ Astronaut trainee,Sports Professional
164
+ Mixed martial artist,Sports Professional
165
+ Starfleet officer,Fictional Character
166
+ Hacker,Other
167
+ Aviator,Other
168
+ Techno DJ,Other
169
+ Neuroscientist,Other
170
+ ASMR content creator,Voice Actor
171
+ Alternative rock singer,"Singer, Musician"
172
+ Spaceflight historian,Other
173
+ Associate Justice of the Supreme Court of the United States,Public FIgure
174
+ Sniper,Sports Professional
175
+ Waitress,Other
176
+ MPB singer,"Singer, Musician"
177
+ Jedi Knight,Fictional Character
178
+ Professional surfer,Sports Professional
179
+ Child model,Model
180
+ Glamour model,Model
181
+ Heavy metal vocalist,"Singer, Musician"
182
+ Football midfielder,Sports Professional
183
+ Television character,TV Personality
184
+ Resistance fighter,Public FIgure
185
+ Instagrammer,Online Personality
186
+ Economist,Other
187
+ Motorcycle racer,Sports Professional
188
+ Children's television presenter,TV Personality
189
+ Kickboxer,Sports Professional
190
+ Visual artist,Other
191
+ Electrical engineer,Other
192
+ Cantopop singer,"Singer, Musician"
193
+ Ranchera singer,"Singer, Musician"
194
+ Cybersecurity expert,Other
195
+ Author,Other
196
+ Chef,TV Personality
197
+ Death metal vocalist,"Singer, Musician"
198
+ Live streamer,"Singer, Musician"
199
+ Artistic gymnast,Sports Professional
200
+ Internet personality,Online Personality
201
+ Secret agent,Fictional Character
202
+ Electric guitarist,"Singer, Musician"
203
+ Pornographic actor,Adult Performer
204
+ Android technician,Fictional Character
205
+ Media critic,Other
206
+ News presenter,TV Personality
207
+ Fitness model,Model
208
+ Chess Streamer,Online Personality
209
+ J-pop idol,"Singer, Musician"
210
+ Artist,Other
211
+ Painter,Other
212
+ Rock singer,"Singer, Musician"
213
+ Chess player,Sports Professional
214
+ Fraudster,Fictional Character
215
+ Queen consort,Fictional Character
216
+ Digital model,Model
217
+ Classical violinist,"Singer, Musician"
218
+ Progressive metal vocalist,"Singer, Musician"
219
+ Community college student,Other
220
+ CrossFit athlete,Sports Professional
221
+ Astronaut,Other
222
+ Music critic,Other
223
+ Professional wrestling executive,Sports Professional
224
+ Celebrity chef,TV Personality
225
+ Rugby union player,Sports Professional
226
+ Hitman,Other
227
+ Fashion designer,Other
228
+ Occultist,Other
229
+ Theatre director,Other
230
+ Drug trafficker,Other
231
+ Diplomat,Public FIgure
232
+ Paranormal investigator,Fictional Character
233
+ Voice actor,Voice Actor
234
+ Badminton player,Sports Professional
235
+ Ring card girl,Sports Professional
236
+ Animated film character,Fictional Character
237
+ K-pop idol,"Singer, Musician"
238
+ Military officer,Other
239
+ Action film actor,Actor
240
+ Holographic technician,Other
241
+ Gymnast,Sports Professional
242
+ Environmental activist,Public Figure
243
+ Photorealistic artist,Other
244
+ Businessman,Other
245
+ Elf nobility,Fictional Character
246
+ Pokémon Trainer,Fictional Character
247
+ Idol singer,"Singer, Musician"
248
+ Child actress,Actor
249
+ Pop rock singer,"Singer, Musician"
250
+ Horror film protagonist,Fictional Character
251
+ Musical theatre actress,Performer
252
+ Astronomer,Other
253
+ Labyrinth guardian,Fictional Character
254
+ Mecha pilot,Fictional Character
255
+ Deity of wisdom,Fictional Character
256
+ Science communicator,Other
257
+ Wildlife subject,Other
258
+ J-rock musician,"Singer, Musician"
259
+ Freestyle skier,Sports Professional
260
+ TikTok content creator,Online Personality
261
+ Television personality,TV Personality
262
+ Reality television personality,TV Personality
263
+ Professional gamer,Online Personality
264
+ Contemporary dancer,Performer
265
+ Football coach,Sports Professional
266
+ J-pop singer-songwriter,"Singer, Musician"
267
+ Enka singer,"Singer, Musician"
268
+ Bodybuilder,Fictional Character
269
+ Starfleet Engineer,Fictional Character
270
+ Monarch,Public Figure
271
+ 44th President of the United States,Public FIgure
272
+ Fashion doll,Model
273
+ Science fiction film character,Fictional Character
274
+ Children's television character,Actor
275
+ Feudal ruler,Public FIgure
276
+ Forensic scientist,Other
277
+ Crime novelist,Other
278
+ Dark Jedi,Fictional Character
279
+ Stage actress,Actor
280
+ Schlager singer,"Singer, Musician"
281
+ Assassin,Fictional Character
282
+ Knight,Fictional Character
283
+ Social media personality,Online Personality
284
+ Luxury hotel,Other
285
+ Action film character,Fictional Character
286
+ Dark Side Force user,Fictional Character
287
+ Dictator,Public FIgure
288
+ Reggaeton singer,"Singer, Musician"
289
+ Statesman,Public FIgure
290
+ Others Island leader,Fictional Character
291
+ Rugby player,Sports Professional
292
+ Microbiologist,Other
293
+ Business executive,Other
294
+ Pin-up model,Model
295
+ Chief Medical Officer,Other
296
+ Fetish model,Adult Performer
297
+ Influencer,Online Personality
298
+ Undead henchman,Fictional Character
299
+ Laptop technician,Other
300
+ Novelist,Other
301
+ Mandalorian warrior,Fictional Character
302
+ Bounty hunter,Fictional Character
303
+ Jaffa warrior,Fictional Character
304
+ High-performance vehicle tuning,Other
305
+ Starfleet Ensign,Fictional Character
306
+ Fantasy character,Fictional Character
307
+ Fictional noble,Fictional Character
308
+ Former professional motorcycle racer,Sports Professional
309
+ Soccer player,Sports Professional
310
+ Legal tender,Other
311
+ Punk rock singer,"Singer, Musician"
312
+ Soap opera actress,Actor
313
+ Physician,Other
314
+ Federal judge,Public FIgure
315
+ Particle physicist,Other
316
+ Drag performer,Performer
317
+ Political activist,Public FIgure
318
+ Fictional knight,Fictional Character
319
+ Educator,Other
320
+ Sports influencer,Online Personality
321
+ Martial artist,Sports Professional
322
+ Police officer,Other
323
+ Crimefighter,Fictional Character
324
+ Asgardian warrior,Fictional Character
325
+ Professional baseball player,Sports Professional
326
+ Vampire Slayer,Fictional Character
327
+ Soap opera actor,Actor
328
+ Basketball player,Sports Professional
329
+ Cryogenic engineer,Other
330
+ Romance novelist,Other
331
+ Street racer,Sports Professional
332
+ Synthetic humanoid,Other
333
+ Ballet Dancer,Performer
334
+ Sculptor,Other
335
+ Mercenary,Other
336
+ Biologist,Other
337
+ Climate activist,Public FIgure
338
+ Television news anchor,TV Personality
339
+ Photorealistic 3D artist,Other
340
+ Comic book superhero,Fictional Character
341
+ Survivor,Other
342
+ Voice Actress,Voice Actor
343
+ Criminal defendant,Other
344
+ Filmmaker,Other
345
+ Forensic Scientist,Other
346
+ Comic book character,Fictional Character
347
+ Horror film actress,Actor
348
+ Philanthropist,Other
349
+ Sports broadcaster,TV Personality
350
+ Beauty Pageant Titleholder,Model
351
+ Meteorologist,Other
352
+ Musical theatre actor,Performer
353
+ Martial arts actor,Actor
354
+ Statistical analysis and data reconfiguration,Other
355
+ Cryptocurrency executive,Other
356
+ Silent film actor,Actor
357
+ Cryptocurrency entrepreneur,Other
358
+ Organized crime leader,Other
359
+ Cult leader,Public FIgure
360
+ Naturalist,Other
361
+ TikTok personality,Online Personality
362
+ Adult performer,Adult Performer
363
+ Triathlete,Sports Professional
364
+ Internet celebrity,Online Personality
365
+ Alternative metal vocalist,"Singer, Musician"
366
+ Fashion influencer,Online Personality
367
+ Philosophy professor,Other
368
+ Voice acting,Voice Actor
369
+ Drummer,"Singer, Musician"
370
+ Sports presenter,Sports Professional
371
+ K-pop singer-songwriter,"Singer, Musician"
372
+ Technology journalist,Other
373
+ Professional bodybuilder,Other
374
+ Video game designer,Other
375
+ Percussionist,"Singer, Musician"
376
+ Music composer,"Singer, Musician"
377
+ Enchantress,Fictional Character
378
+ Witcher,Fictional Character
379
+ Funeral director,Other
380
+ Special operations agent,Fictional Character
381
+ Adult model,Adult Performer
382
+ Medical practitioner,Other
383
+ Scientist,Other
384
+ Football club president,Sports Professional
385
+ Soldier,Other
386
+ Erotic film actor,Adult Performer
387
+ Flutist,"Singer, Musician"
388
+ Investor,Other
389
+ Stunt performer,Sports Professional
390
+ Fictional warrior,Other
391
+ Political scientist,Other
392
+ Game show model,TV Personality
393
+ Mixed Martial Artist,Sports Professional
394
+ Alpine skier,Sports Professional
395
+ Nobleman,Other
396
+ Metalcore vocalist,"Singer, Musician"
397
+ YouTube Personality,Online Personality
398
+ Software engineer,Other
399
+ Sertanejo singer,"Singer, Musician"
400
+ Explorer,Other
401
+ Documentary filmmaker,Other
402
+ Jazz vocalist,"Singer, Musician"
403
+ Military commander,Other
404
+ Sprinter,Sports Professional
405
+ New wave singer,"Singer, Musician"
406
+ Japanese adult film actor,Adult Performer
407
+ Concert cellist,"Singer, Musician"
408
+ FBI Special Agent,Fictional Character
409
+ Professional race car driver,Sports Professional
410
+ Professional motorcycle racer,Sports Professional
411
+ Film editor,Other
412
+ American football wide receiver,Sports Professional
413
+ Leadership Speaker,Public FIgure
414
+ Video game developer,Other
415
+ Heavy metal singer,"Singer, Musician"
416
+ Japanese comedian,TV Personality
417
+ Heavy metal guitarist,"Singer, Musician"
418
+ Biathlete,Sports Professional
419
+ Automotive model,Model
420
+ Wrestling journalist,Sports Professional
421
+ Broadcaster,TV Personality
422
+ Ultramarathon runner,Sports Professional
423
+ Spaceship technician,Fictional Character
424
+ Religious leader,Other
425
+ Fitness trainer,Other
426
+ Ship's counselor,Fictional Character
427
+ Magician,Other
428
+ Xiangsheng performer,Performer
429
+ Traffic reporter,Other
430
+ Entrepreneur,Other
431
+ Cybersecurity researcher,Other
432
+ Torturer,Other
433
+ Car dealership owner,Other
434
+ Bartender,Other
435
+ Male model,Model
436
+ American football player,Sports Professional
437
+ Television comedian,TV Personality
438
+ Gigolo,Adult Performer
439
+ Forensic blood spatter analyst,Other
440
+ Swordswoman,Fictional Character
441
+ Symphonic metal vocalist,"Singer, Musician"
442
+ Record producer,Other
443
+ Photographer,Other
444
+ Track and Field Athlete,Sports Professional
445
+ Fashion YouTuber,Online Personality
446
+ Thai actor,Actor
447
+ Automotive designer,Other
448
+ Burlesque performer,Actor
449
+ Film translator,Other
450
+ Gambler,Other
451
+ Country music singer-songwriter,"Singer, Musician"
452
+ Host,TV Personality
453
+ Bureaucrat,Other
454
+ Alternative rock singer-songwriter,"Singer, Musician"
455
+ Professional wrestling manager,Sports Professional
456
+ Advertising executive,Other
457
+ Real estate developer,Other
458
+ Romulan Military Officer,Other
459
+ High school student,Other
460
+ Sitcom character,Actor
461
+ Sith Lord,Fictional Character
462
+ Computer scientist,Other
463
+ Silent film actress,Actor
464
+ Supervillain,Fictional Character
465
+ Intergalactic warrior,Fictional Character
466
+ American football cornerback,Sports Professional
467
+ Anthropomorphic personification of dreams,Fictional Character
468
+ Bail bondsman,Fictional Character
469
+ Science fiction television character,Fictional Character
470
+ Paper salesman,Other
471
+ Investigative journalist,Other
472
+ Motorcycle mechanic,Other
473
+ Professional dancer,Sports Professional
474
+ Esports host,Online Personality
475
+ Punk musician,"Singer, Musician"
476
+ New-age musician,"Singer, Musician"
477
+ Sitcom actor,Actor
478
+ Supreme Court Justice,Other
479
+ Anime voice actor,Voice Actor
480
+ Prophet,Public FIgure
481
+ Pre-Raphaelite model,Other
482
+ Equestrian athlete,Sports Professional
483
+ Rock vocalist,"Singer, Musician"
484
+ Talk show host,TV Personality
485
+ Warrant Officer,Other
486
+ Performance artist,Other
487
+ Cricketer,Sports Professional
488
+ Movie character,Fictional Character
489
+ CEO of Tesla,Other
490
+ Horror host,Other
491
+ Rock and roll singer,"Singer, Musician"
492
+ Artificial Intelligence Researcher,Other
493
+ Turkish classical music singer,"Singer, Musician"
494
+ Automotive YouTuber,Online Personality
495
+ Latin pop singer,"Singer, Musician"
496
+ Fictional animatronic character,Fictional Character
497
+ Professional boxer,Sports Professional
498
+ Saxophonist,"Singer, Musician"
499
+ Rock singer-songwriter,"Singer, Musician"
500
+ Time traveler,Fictional Character
501
+ Shieldmaiden,Fictional Character
502
+ Vampire hunter,Fictional Character
503
+ Gospel singer,"Singer, Musician"
504
+ Violinist,"Singer, Musician"
505
+ Convicted felon,Other
506
+ Classical pianist,"Singer, Musician"
507
+ Fashion Stylist,Other
508
+ Game designer,Other
509
+ Revolutionary leader,Public FIgure
510
+ Revolutionary,Public FIgure
511
+ Neo-soul singer-songwriter,"Singer, Musician"
512
+ Formula One driver,Sports Professional
513
+ Evangelist,Other
514
+ Game show host,TV Personality
515
+ Travel vlogger,Online Personality
516
+ Fitness YouTuber,Online Personality
517
+ Professional poker player,Other
518
+ Football club owner,Sports Professional
519
+ Archaeologist,Other
520
+ Folk metal vocalist,"Singer, Musician"
521
+ Country singer,"Singer, Musician"
522
+ Professional darts player,Sports Professional
523
+ Sports commentator,TV Personality
524
+ Kryptonian Military Officer,Other
525
+ Information Technology Specialist,Other
526
+ Fictional villain,Other
527
+ Newsreader,TV Personality
528
+ Retail Manager,Other
529
+ Insurance spokesperson,Other
530
+ Symphonic metal singer,"Singer, Musician"
531
+ Fine art photographer,Other
532
+ Civil rights attorney,Other
533
+ Indie rock singer-songwriter,"Singer, Musician"
534
+ Competitive swimmer,Sports Professional
535
+ Artist's model,Model
536
+ Criminal,Other
537
+ Vigilante,Other
538
+ U.S. President,Public FIgure
539
+ Abolitionist,Public FIgure
540
+ Fantasy warrior,Fictional Character
541
+ Open-world action-adventure game protagonist,Fictional Character
542
+ Viking explorer,Other
543
+ Cross-country skier,Sports Professional
544
+ Philosopher,Other
545
+ Historical figure,Other
546
+ Angel,Fictional Character
547
+ Starship captain,Fictional Character
548
+ Telenovela actress,Actor
549
+ Warrior,Fictional Character
550
+ Olympic gymnast,Sports Professional
551
+ Urban fantasy author,Other
552
+ Roman general,Other
553
+ Elf ruler,Fictional Character
554
+ Wizard,Fictional Character
555
+ Gladiator,Fictional Character
556
+ Archery combatant,Other
557
+ Wandmaker,Fictional Character
558
+ Funk musician,"Singer, Musician"
559
+ Comic book writer,Other
560
+ Heptathlete,Fictional Character
561
+ Monster hunter,Fictional Character
562
+ Professional Basketball Player,Sports Professional
563
+ K-pop rapper,"Singer, Musician"
564
+ Cantopop singer-songwriter,"Singer, Musician"
565
+ Leisure center manager,Other
566
+ Corporate raider,Other
567
+ Michelin-starred chef,Other
568
+ Wildlife species,Other
569
+ Klingon Chancellor,Fictional Character
570
+ Golf influencer,Online Personality
571
+ Tattoo artist,Other
572
+ Diagnostic physician,Other
573
+ Gaming YouTuber,Online Personality
574
+ Drug lord,Other
575
+ Olympic triathlete,Sports Professional
576
+ Writer,Other
577
+ High jumper,Sports Professional
578
+ Ancient ruler,Public FIgure
579
+ Smuggler,Other
580
+ VTuber,Online Personality
581
+ Outdoorswoman,Other
582
+ YouTube personality,Online Personality
583
+ Professional snowboarder,Sports Professional
584
+ Esports player,Online Personality
585
+ Restaurateur,Other
586
+ Criminal psychologist,Other
587
+ Fictional supervillain,Fictional Character
588
+ Fictional police detective,Fictional Character
589
+ Fictional wizard,Fictional Character
590
+ Vocaloid singer,"Singer, Musician"
591
+ Head of State,Public FIgure
592
+ Real estate agent,Other
593
+ Comic book villain,Fictional Character
594
+ Bollywood actress,Actor
595
+ Mythological figure,Fictional Character
596
+ Office worker,Other
597
+ Supernatural entity,Fictional Character
598
+ Starfighter pilot,Fictional Character
599
+ Mining engineer,Other
600
+ Fictional scientist,Fictional Character
601
+ Mythological hero,Fictional Character
602
+ Political Strategist,Public FIgure
603
+ Professional streamer,Online Personality
604
+ Video game director,"Singer, Musician"
605
+ Visual kei musician,"Singer, Musician"
606
+ Ice hockey player,Sports Professional
607
+ Fictional character actor,Fictional Character
608
+ Adult film director,Other
609
+ Trot singer,"Singer, Musician"
610
+ Bollywood actor,Actor
611
+ Makeup artist,Other
612
+ Political aide,Other
613
+ 3D artist,Other
614
+ Heavy metal musician,"Singer, Musician"
615
+ Schoolteacher,Other
616
+ Television presenters,TV Personality
617
+ Roman noblewoman,Other
618
+ Mutant superhero,Fictional Character
619
+ Intelligence officer,Other
620
+ Ancient Egyptian priest,Fictional Character
621
+ Companion,Other
622
+ Idol,TV Personality
623
+ Flamenco singer,"Singer, Musician"
624
+ Princess,Other
625
+ Swordsman,Fictional Character
626
+ Film character,Fictional Character
627
+ Sports journalist,TV Personality
628
+ Olympic athlete,Sports Professional
629
+ Mythologist,Other
630
+ Speed Skater,Sports Professional
631
+ Psychologist,Other
632
+ Chanson singer,"Singer, Musician"
633
+ Crown dependency,Other
634
+ Fictional boxer,Fictional Character
635
+ Mad scientist,Fictional Character
636
+ Satirical religious figure,Fictional Character
637
+ Aviation tracking developer,Other
638
+ K-pop artist,"Singer, Musician"
639
+ First Lady of the United States,Public FIgure
640
+ Art model,Other
641
+ Police detective,Fictional Character
642
+ Video game producer,Other
643
+ Starfleet Science Officer,Fictional Character
644
+ Fictional monarch,Other
645
+ Marine,Other
646
+ Mughal Emperor,Fictional Character
647
+ Fictional spy,Fictional Character
648
+ Fictional super soldier,Other
649
+ Financial analyst,Other
650
+ Oncologist,Other
651
+ Rock guitarist,"Singer, Musician"
652
+ Folk musician,"Singer, Musician"
653
+ Professional football player,Sports Professional
654
+ Professional cyclist,Sports Professional
655
+ Can-can dancer,Actor
656
+ Art historian,Other
657
+ Subway sandwich chain spokesperson,Other
658
+ Eurodance singer,"Singer, Musician"
659
+ Fictional princess,Fictional Character
660
+ Physicist,Other
661
+ Computer engineer,Other
662
+ Political analyst,Other
663
+ Martial artist actor,Actor
664
+ Illustrator,Other
665
+ Financier,Other
666
+ Unemployed slacker,Other
667
+ Serial killer,Other
668
+ Political advisor,Other
669
+ Clock tower caretaker,Other
670
+ Perfume reviewer,Other
671
+ Football referee,Sports Professional
672
+ Private investigator,Other
673
+ Special Operations Agent,Other
674
+ Samurai,Other
675
+ Webtoon artist,Other
676
+ Secretary,Other
677
+ Military leader,Other
678
+ Professional Go player,Sports Professional
679
+ Convicted murderer,Other
680
+ Children's entertainer,Actor
681
+ Special forces operative,Other
682
+ Death row inmate,Other
683
+ Internet marketer,Online Personality
684
+ Political consultant,Other
685
+ Superhero actor,Fictional Character
686
+ Chief Justice,Public FIgure
687
+ American football coach,Sports Professional
688
+ Young adult fiction author,Other
689
+ Baseball manager,Sports Professional
690
+ Punk rock musician,"Singer, Musician"
691
+ Martial arts instructor,Sports Professional
692
+ Holographic AI companion,Other
693
+ Mandopop singer,"Singer, Musician"
694
+ Astrophysicist,Other
695
+ Professional road cyclist,Sports Professional
696
+ Adventurer,Other
697
+ Martial arts film actor,Other
698
+ Clinical psychologist,Other
699
+ Olympic wrestler,Sports Professional
700
+ Stockbroker,Other
701
+ Catholic priest,Other
702
+ Janitor,Other
703
+ Law enforcement officer,Other
704
+ Political leader,Public FIgure
705
+ Television meteorologist,TV Personality
706
+ Folk singer,"Singer, Musician"
707
+ Copla singer,"Singer, Musician"
708
+ Speed skater,Sports Professional
709
+ Latin pop singer-songwriter,"Singer, Musician"
710
+ Amateur model,Model
711
+ Reality TV star,TV Personality
712
+ YouTube commentator,Online Personality
713
+ Professional American football player,Sports Professional
714
+ Professional golfer,Sports Professional
715
+ Tech YouTuber,Online Personality
716
+ Professional speed skater,Sports Professional
717
+ Rebel operative,Other
718
+ Military personnel,Other
719
+ Lunar noblewoman,Fictional Character
720
+ Fantasy television actor,Actor
721
+ Vice President of the United States,Public FIgure
722
+ Guitarist,"Singer, Musician"
723
+ Hip-hop artist,"Singer, Musician"
724
+ Catholic Pope,Public Figure
725
+ Film historian,Other
726
+ Fugitive,Other
727
+ Sports television host,TV Personality
728
+ Fictional archer,Fictional Character
729
+ Jewelry retail,Other
730
+ Spaceship mechanic,Fictional Character
731
+ Jazz guitarist,"Singer, Musician"
732
+ Electronic music vocalist,"Singer, Musician"
733
+ Media proprietor,Other
734
+ Sports podcaster,Voice Actor
735
+ Paralympic skier,Sports Professional
736
+ Country pop singer,"Singer, Musician"
737
+ Classical vocalist,"Singer, Musician"
738
+ Royal family member,Other
739
+ Supreme Leader of North Korea,Public Figure
740
+ K-drama actress,Actor
741
+ Counterterrorism agent,Other
742
+ Defense attorney,Other
743
+ Video game antagonist,Fictional Character
744
+ Bajoran Militia Officer,Other
745
+ Conductor,Other
746
+ Yakuza member,Other
747
+ Countertenor singer,"Singer, Musician"
748
+ Rocket scientist,Other
749
+ Victim of crime,Other
750
+ Travel blogger,Online Personality
751
+ Japanese idol actor,Actor
752
+ Grunge musician,"Singer, Musician"
753
+ Disco singer,"Singer, Musician"
754
+ Tokusatsu actor,Actor
755
+ Textual inversion researcher,Other
756
+ Broadway actress,Actor
757
+ Fantasy author,Other
758
+ Investment manager,Other
759
+ Radio host,Voice Actor
760
+ Horror film character,Other
761
+ Powerlifter,Fictional Character
762
+ Starship propulsion engineer,Fictional Character
763
+ Architectural landmark,Other
764
+ Olympic fencer,Sports Professional
765
+ Rebel leader,Other
766
+ Criminologist,Other
767
+ Songbird,Other
768
+ Cumbia singer,"Singer, Musician"
769
+ Royalty,Other
770
+ Television director,TV Personality
771
+ Fictional vampire,Fictional Character
772
+ Choreographer,Sports Professional
773
+ Organized crime enforcer,Other
774
+ Defense Attorney,Other
775
+ Video essayist,Online Personality
776
+ Technology YouTuber,Online Personality
777
+ Professional thief,Other
778
+ Eurovision Song Contest winner,"Singer, Musician"
779
+ Innkeeper,Fictional Character
780
+ Sexologist,Other
781
+ Wildlife filmmaker,Other
782
+ Operatic tenor,Other
783
+ Nightclub owner,Other
784
+ Gladiator's wife,Fictional Character
785
+ Vault dweller,Fictional Character
786
+ Classical composer,"Singer, Musician"
787
+ Football administrator,Sports Professional
788
+ Art patron,Other
789
+ Gothic fashion model,Model
790
+ High school cheerleader,Other
791
+ Country singer-songwriter,"Singer, Musician"
792
+ Fictional protagonist,Fictional Character
793
+ Telepath,Other
794
+ Jazz musician,"Singer, Musician"
795
+ Gothic literature character,Fictional Character
796
+ Corporate executive,Other
797
+ Counter-terrorism operative,Other
798
+ Human rights activist,Public Figure
799
+ Public health official,Other
800
+ Racing Driver,Sports Professional
801
+ Farmer,Other
802
+ Javelin thrower,Fictional Character
803
+ Industrial metal musician,"Singer, Musician"
804
+ Photorealistic model,Model
805
+ Technology executive,Other
806
+ Strongman competitor,Other
807
+ YouTube Let's Player,Online Personality
808
+ Technology entrepreneur,Other
809
+ Civil rights activist,Public Figure
810
+ Anime character designer,Other
811
+ Video game streamer,Online Personality
812
+ Biblical figure,Fictional Character
813
+ Nanny,Other
814
+ Witch,Fictional Character
815
+ Anime director,Other
816
+ Criminal defense attorney,Other
817
+ Sailor,Other
818
+ Demon bounty hunter,Fictional Character
819
+ Monster,Fictional Character
820
+ Television journalist,Journalist
821
+ Former First Lady of the United States,Public Figure
822
+ Video game actress,Fictional Character
823
+ Music video director,Other
824
+ Priestess,Fictional Character
825
+ Metal singer,"Singer, Musician"
826
+ Automotive manufacturer,Other
827
+ Crime-fighting operative,Other
828
+ Crime boss,Other
829
+ Grime artist,"Singer, Musician"
830
+ Regional manager,Other
831
+ Video game composer,Other
832
+ Digital Artist,Other
833
+ Hurdler,Sports Professional
834
+ Cartoon character,Fictional Character
835
+ Ottoman princess,Other
836
+ Family Medicine Physician,Other
837
+ Psychiatric nurse,Other
838
+ Starfleet operations officer,Fictional Character
839
+ Classical soprano singer,"Singer, Musician"
840
+ Professor of Transfiguration,Fictional Character
841
+ Military strategist,Other
842
+ Crown Prince,Public Figure
843
+ Homemaker,Other
844
+ Food blogger,Online Personality
845
+ K-pop idol group,"Singer, Musician"
846
+ Fictional pirate captain,Fictional Character
847
+ Barista,Other
848
+ Freedom fighter,Other
849
+ Gothic homemaker,Fictional Character
850
+ Central Bank Governor,Public Figure
851
+ Virtual influencer,Online Personality
852
+ Shapeshifter,Fictional Character
853
+ Photographic model,Model
854
+ Gospel musician,"Singer, Musician"
855
+ Supermodel,Model
856
+ Maker,Other
857
+ Fictional ninja,Other
858
+ Japanese AV idol,Adult Performer
859
+ Missing person,Other
860
+ Intelligence operative,Other
861
+ Investment banker,Other
862
+ Fire Dragon Slayer,Fictional Character
863
+ Ecosystem regulator,Other
864
+ Interstellar diplomat,Fictional Character
865
+ Queen of Egypt,Public Figure
866
+ Pro Hero,Other
867
+ Dangdut singer,Other
868
+ Anti-apartheid activist,Public Figure
869
+ Light novel character,Fictional Character
870
+ Architect,Other
871
+ Na'vi warrior,Fictional Character
872
+ K-pop group,"Singer, Musician"
873
+ Livestreamer,Online Personality
874
+ Professional Bodybuilder,TV Personality
875
+ Television cook,TV Personality
876
+ Handball player,Sports Professional
877
+ Business magnate,Public Figure
878
+ Fitness Influencer,Online Personality
879
+ Blues rock musician,"Singer, Musician"
880
+ Mongolian traditional singer,"Singer, Musician"
881
+ Communications Officer,Other
882
+ Security officer,Fictional Character
883
+ TV presenter,TV Personality
884
+ AI model,Other
885
+ Mobile Legends: Bang Bang player,Online Personality
886
+ Mobile Legends player,Online Personality
887
+ Terrorist leader,Public Figure
888
+ Literary translator,Other
889
+ Wuxia novelist,Other
890
+ Bullfighter,Other
891
+ Galactic Senator,Fictional Character
892
+ Professional Golfer,Sports Professional
893
+ Botanist,Other
894
+ K-pop producer,"Singer, Musician"
895
+ Librarian,Other
896
+ Jazz pianist,"Singer, Musician"
897
+ Fitness Trainer,Other
898
+ Fictional nobleman,Fictional Character
899
+ Judoka,Sports Professional
900
+ Software Developer,Other
901
+ A cappella musician,"Singer, Musician"
902
+ Televangelist,TV Personality
903
+ Methamphetamine manufacturer,Other
904
+ College student,Other
905
+ Electronic dance music producer,"Singer, Musician"
906
+ BSAA operative,Other
907
+ Roman Prefect of Judea,Other
908
+ Omnipotent being,Fictional Character
909
+ Jedi Master,Fictional Character
910
+ Virtual program,Other
911
+ Digital entity,Fictional Character
912
+ Fictional legal associate,Fictional Character
913
+ Viking warrior,Fictional Character
914
+ Delivery courier,Other
915
+ Supernatural antagonist,Fictional Character
916
+ Japanese adult film actress,Adult Performer
917
+ Industrialist,Other
918
+ Soul musician,"Singer, Musician"
919
+ Criminal mastermind,Other
920
+ Fictional criminal mastermind,Other
921
+ Evangelion Pilot,Other
922
+ Shrine maiden,Fictional Character
923
+ Bandit leader,Other
924
+ Jedi,Fictional Character
925
+ Fictional royal heir,Fictional Character
926
+ Heavy metal bassist,"Singer, Musician"
927
+ Internet meme personality,Other
928
+ Electronic music producer,"Singer, Musician"
929
+ Blade Runner,Fictional Character
930
+ Software developer,Other
931
+ Fitness instructor,Online Personality
932
+ Sheriff's deputy,Other
933
+ Dungeons & Dragons player,Other
934
+ Ranch foreman,Other
935
+ Neurologist,Other
936
+ Reggae musician,"Singer, Musician"
937
+ Nuclear physicist,Other
938
+ Wildlife conservationist,Other
939
+ Pulp fiction writer,Other
940
+ Environmental lawyer,Public Figure
941
+ Business consultant,Other
942
+ Television painter,TV Personality
943
+ Spacecraft pilot,Fictional Character
944
+ Cuarteto singer,"Singer, Musician"
945
+ Quidditch referee,Fictional Character
946
+ Luxury automobile manufacturer,Other
947
+ Time traveler's companion,Fictional Character
948
+ Content creator,Online Personality
949
+ Paleontologist,Other
950
+ Darknet market operator,Other
951
+ Replicant,Other
952
+ Gamekeeper,Fictional Character
953
+ Shia cleric,Public Figure
954
+ Businessperson,Other
955
+ Fairy tale character,Fictional Character
956
+ Detective,Other
957
+ Composer,Other
958
+ Showgirl,TV Personality
959
+ Screenwriter,Other
960
+ FBI Agent,Fictional Character
961
+ Video game writer,Other
962
+ Professional wrestling ring announcer,Sports Professional
963
+ Firearms inventor,Other
964
+ Europop singer,"Singer, Musician"
965
+ Tokusatsu actress,Actor
966
+ Cartoonist,Other
967
+ Pianist,"Singer, Musician"
968
+ Dark Wizard,Fictional Character
969
+ Cryptocurrency developer,Other
970
+ Curler,Sports Professional
971
+ Mahjong player,Other
972
+ Tejano singer,"Singer, Musician"
973
+ Vampire assassin,Fictional Character
974
+ Vampire warrior,Fictional Character
975
+ Thief,Other
976
+ Police sergeant,Other
977
+ Potions Master,Fictional Character
978
+ Motorcycle Racer,Sports Professional
979
+ Cybersecurity journalist,Other
980
+ Jazz saxophonist,"Singer, Musician"
981
+ Australian rules footballer,Sports Professional
982
+ Galactic Emperor,Fictional Character
983
+ Lightsaber duelist,Fictional Character
984
+ Ballroom dancer,Actor
985
+ Mercenary leader,Other
986
+ Media mogul,Other
987
+ Special forces operator,Other
988
+ Intelligence Agent,Other
989
+ Muay Thai fighter,Other
990
+ Shogi player,"Singer, Musician"
991
+ Science Officer,Other
992
+ Science fiction film franchise,Other
993
+ Horror novelist,Other
994
+ Late-night talk show host,TV Personality
995
+ Neurosurgeon,Other
996
+ Fictional superhero,Fictional Character
997
+ Post-punk singer,"Singer, Musician"
998
+ Lexicographer,Other
999
+ Thai actress,Actor
1000
+ Playwright,Other
1001
+ Short track speed skater,Sports Professional
1002
+ Divination teacher,Fictional Character
1003
+ Imperial Security Officer,Other
1004
+ Rally driver,Sports Professional
1005
+ Lifestyle blogger,Online Personality
1006
+ Photojournalist,Other
1007
+ Demon Slayer,Fictional Character
1008
+ Swimmer,Sports Professional
1009
+ Opera Singer,Performer
1010
+ City pop musician,"Singer, Musician"
1011
+ Elf warrior,Fictional Character
1012
+ Buddhist monk,Other
1013
+ Fantasy novelist,Other
1014
+ Catholic nun,Other
1015
+ Computer programmer,Other
1016
+ Rock musicians,"Singer, Musician"
1017
+ Inventor,Other
1018
+ Satirical songwriter,"Singer, Musician"
1019
+ Alternative rock musician,"Singer, Musician"
1020
+ Olympic diver,Sports Professional
1021
+ Dark sorcerer,Fictional Character
1022
+ Digital Illustrator,Other
1023
+ Industrial metal vocalist,"Singer, Musician"
1024
+ Rock climber,Sports Professional
1025
+ Dark wizard,Fictional Character
1026
+ Canoe slalom athlete,Sports Professional
1027
+ Compact MPV,Other
1028
+ Gangsta rapper,"Singer, Musician"
1029
+ Soap maker,Other
1030
+ Military general,Other
1031
+ Terrorist,Other
1032
+ Federal republic,Other
1033
+ Animated film protagonist,Fictional Character
1034
+ Comedic actor,TV Personality
1035
+ Beauty pageant winner,Model
1036
+ Naval officer,Other
1037
+ Fictional Asgardian warrior,Fictional Character
1038
+ Director,Other
1039
+ Professional Cyclist,Sports Professional
1040
+ Cybernetic Surgeon,Other
1041
+ Furry artist,Other
1042
+ Dungeons & Dragons content creator,Online Personality
1043
+ Cryptocurrency Developer,Other
1044
+ Fictional crime boss,Fictional Character
1045
+ Futurist poet,Other
1046
+ Minivan,Other
1047
+ High school chemistry teacher,Other
1048
+ Intelligence Officer,Other
1049
+ Hard rock singer,"Singer, Musician"
1050
+ Television pitchman,TV Personality
1051
+ Blues rock guitarist,Singer, Musician
1052
+ Science fiction author,Other
1053
+ Military General,Other
1054
+ Confectioner,Other
1055
+ Witchcraft practitioner,Fictional Character
1056
+ Prime Minister,Public Figure
1057
+ Alternative pop musician,"Singer, Musician"
1058
+ Electric vehicle,Other
1059
+ Engineer,Other
1060
+ Shrine Priestess,Fictional Character
1061
+ Electric vehicle manufacturer,Other
1062
+ Contemporary artist,Other
1063
+ Sorceress,Fictional Character
1064
+ New Age musician,"Singer, Musician"
1065
+ Conceptual artist,Other
1066
+ Prosecutor,Other
1067
+ Concept artist,Other
1068
+ Bariatric surgeon,other
1069
+ Archer,Sports Professional
1070
+ Japanese adult video actress,Adult Performer
1071
+ Olympic swimmer,Sports Professional
1072
+ Football manager,Sports Professional
1073
+ Mentor,Other
1074
+ Regional Mexican singer,"Singer, Musician"
misc/lists/professions.csv ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ profession,alias_1,alias_2,alias_3
2
+ celebrity,celeb,,
3
+ actor,actress,,
4
+ singer,,,
5
+ musician,,,
6
+ rapper,rap,,
7
+ influencer,,,
8
+ model,,,
9
+ tv host,,,
10
+ tv moderator,,,
11
+ streamer,,,
12
+ twitch streamer,twitch,,
13
+ youtuber,youtube,,
14
+ content creator,,,
15
+ media personality,,,
16
+ reality tv star,reality,,
17
+ e-girl,egirl,,
18
+ e-boy,eboy,,
19
+ cosplayer,,,
20
+ voice actor,voice actress,,
21
+ asmr creator,asmr,,
22
+ comedian,,,
23
+ talk show host,talkshow,,
24
+ radio host,,,
25
+ journalist,,,
26
+ politician,,,
27
+ activist,,,
28
+ public speaker,,,
29
+ fitness influencer,fitness,,
30
+ chef,,,
31
+ author,writer,,
32
+ adult entertainer,,,
33
+ onlyfans creator,onlyfans,,
34
+ cam model,camgirl,webcam,
35
+ pornographic actor,porn,pornstar,
36
+ instagrammer,instagram,insta,
plots/2024-06_image_contributions.svg DELETED
plots/Figure_12.svg ADDED
plots/Sec-6-5_lora_tags_categories.svg DELETED
plots/Sec_6-5_taggers.svg DELETED
plots/Section_6-7_model_contributions_sankey.svg DELETED
plots/models_nsfw_monthly_histogram_with_quarters.svg DELETED
public/Figure_13.html ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Danbooru Tree from JSON</title>
6
+ <script src="https://d3js.org/d3.v7.min.js"></script>
7
+ <style>
8
+ body {
9
+ font-family: sans-serif;
10
+ }
11
+ svg {
12
+ width: 100%;
13
+ height: 1500px;
14
+ }
15
+ .node circle {
16
+ fill: steelblue;
17
+ }
18
+ .node text {
19
+ font-size: 12px;
20
+ fill: #333;
21
+ }
22
+ .link {
23
+ fill: none;
24
+ stroke: #ccc;
25
+ stroke-width: 1.5px;
26
+ }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <h2>Danbooru Categories</h2>
31
+ <button id="downloadBtn">Download SVG</button>
32
+ <svg></svg>
33
+
34
+ <script>
35
+ d3.json("json/danbooru_flat.json").then(function(data) {
36
+ const svg = d3.select("svg");
37
+ const width = 750;
38
+ const height = 1200;
39
+
40
+ // ---- STEP 1: Limit second-level children ----
41
+ function limitSecondLevel(node, depth = 0) {
42
+ if (node.children && depth === 1 && node.children.length > 5) {
43
+ const visible = node.children.slice(0, 5);
44
+ visible.push({ name: "...", children: [] });
45
+ node.children = visible;
46
+ }
47
+ if (node.children) {
48
+ node.children.forEach(child => limitSecondLevel(child, depth + 1));
49
+ }
50
+ return node;
51
+ }
52
+
53
+ const limited = limitSecondLevel(structuredClone(data));
54
+
55
+ const root = d3.hierarchy(limited, d => d.children);
56
+
57
+ // ---- STEP 2: Track root category for coloring ----
58
+ root.eachAfter(d => {
59
+ if (d.depth === 1) {
60
+ d.data.rootCategory = d.data.name;
61
+ } else if (d.parent) {
62
+ d.data.rootCategory = d.parent.data.rootCategory;
63
+ }
64
+ });
65
+
66
+ const treeLayout = d3.tree().size([height, width]);
67
+ treeLayout(root);
68
+
69
+ // ---- STEP 3: Scale for tag_count → radius ----
70
+ const allTagCounts = root.descendants()
71
+ .filter(d => d.depth > 0)
72
+ .map(d => d.data.tag_count || 0);
73
+ const sizeScale = d3.scaleSqrt()
74
+ .domain([0, d3.max(allTagCounts)])
75
+ .range([4, 20]);
76
+
77
+ // ---- STEP 4: Color Map ----
78
+ const categoryColors = {
79
+ "attire": "#f4a261",
80
+ "body": "#e76f51",
81
+ "characters": "#2a9d8f",
82
+ "copyrights": "#264653",
83
+ "creatures": "#8ecae6",
84
+ "drawing software": "#219ebc",
85
+ "games": "#3a86ff",
86
+ "metatags": "#ffbe0b",
87
+ "more": "#b5179e",
88
+ "objects": "#6d6875",
89
+ "plant": "#7cb518",
90
+ "real_world": "#a5a58d",
91
+ "sex": "#ef476f",
92
+ "visual_characteristics": "#06d6a0",
93
+ "subject": "#ffd166",
94
+ "uncategorized": "#adb5bd",
95
+ "actions_and_expressions": "#d00000",
96
+ "objects_and_backgrounds": "#118ab2"
97
+ };
98
+
99
+ function getColor(d) {
100
+ if (d.data.name === "...") return "gray";
101
+ const category = d.data.rootCategory?.toLowerCase();
102
+ return categoryColors[category] || "steelblue";
103
+ }
104
+
105
+ // ---- STEP 5: Draw Links ----
106
+ svg.selectAll(".link")
107
+ .data(root.links())
108
+ .join("path")
109
+ .attr("class", "link")
110
+ .attr("d", d3.linkHorizontal()
111
+ .x(d => d.y)
112
+ .y(d => d.x)
113
+ );
114
+
115
+ // ---- STEP 6: Draw Nodes ----
116
+ const node = svg.selectAll(".node")
117
+ .data(root.descendants())
118
+ .join("g")
119
+ .attr("class", "node")
120
+ .attr("transform", d => `translate(${d.y},${d.x})`);
121
+
122
+ node.append("circle")
123
+ .attr("r", d => d.depth === 0 ? 6 : sizeScale(d.data.tag_count || 0))
124
+ .style("fill", d => d.data.color || "steelblue");
125
+
126
+
127
+ node.append("title")
128
+ .text(d => `${d.data.name}\nTags: ${d.data.tag_count || 0}`);
129
+
130
+ node.append("text")
131
+ .attr("x", 10)
132
+ .style("font-weight", "bold")
133
+ .attr("dy", "0.32em")
134
+ .text(d => d.data.name);
135
+ });
136
+
137
+ document.getElementById("downloadBtn").addEventListener("click", () => {
138
+ const svgNode = document.querySelector("svg");
139
+
140
+ // Clone the SVG node to preserve original
141
+ const clonedSvg = svgNode.cloneNode(true);
142
+ const outer = document.createElement("div");
143
+ outer.appendChild(clonedSvg);
144
+
145
+ // Add xmlns so it can be saved properly
146
+ clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
147
+
148
+ // Serialize the SVG
149
+ const svgData = new XMLSerializer().serializeToString(clonedSvg);
150
+ const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
151
+
152
+ // Create download link
153
+ const url = URL.createObjectURL(svgBlob);
154
+ const a = document.createElement("a");
155
+ a.href = url;
156
+ a.download = "danbooru_tree.svg";
157
+ document.body.appendChild(a);
158
+ a.click();
159
+ document.body.removeChild(a);
160
+ URL.revokeObjectURL(url);
161
+ });
162
+ </script>
163
+
164
+
165
+ </body>
166
+ </html>
public/Figure_8a_sunburst.html ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <meta charset="utf-8">
3
+ <style>
4
+ body {
5
+ font-family: "Segoe UI", sans-serif;
6
+ text-align: center;
7
+ margin-top: 20px;
8
+ }
9
+
10
+ svg {
11
+ font: 10px sans-serif;
12
+ }
13
+
14
+ .label {
15
+ font-weight: bold;
16
+ font-size: 12px;
17
+ text-anchor: middle;
18
+ }
19
+
20
+ .legend {
21
+ font-size: 12px;
22
+ text-anchor: start;
23
+ }
24
+
25
+ .legend rect {
26
+ stroke-width: 1;
27
+ stroke: #000;
28
+ }
29
+ </style>
30
+ <body>
31
+ <h2>Sunburst Chart: Country → Profession</h2>
32
+ <button id="downloadBtn">Download SVG</button>
33
+ <svg width="800" height="800"></svg>
34
+ <script src="https://d3js.org/d3.v6.min.js"></script>
35
+ <script>
36
+ const width = 800;
37
+ const radius = width / 3;
38
+
39
+ const colorMap = {
40
+ "Adult Performer": "#8A2BE2",
41
+ "Model": "#DC143C",
42
+ "Actor": "#FF7F50",
43
+ "Performer": "magenta",
44
+ "Singer, Musician": "wheat",
45
+ "TV Personality": "#708090",
46
+ "Sports Professional": "gold",
47
+ "Public Figure": "maroon",
48
+ "Voice Actor": "lightgreen",
49
+ "Online Personality": "#4682B4",
50
+ "Other": "#ccc"
51
+ };
52
+
53
+
54
+ // Count total values per gender (assuming gender is at depth 1)
55
+
56
+
57
+ const professionOrder = [
58
+ "Adult Performer", "Actor", "Singer, Musician", "Model", "Online Personality", "Public Figure", "Sports Professional", "Voice Actor", "TV Personality", "Other"
59
+ ];
60
+ const manualOrder = [
61
+ "United States", "Japan", "South Korea", "United Kingdom",
62
+ "Russia", "China", "Canada", "India", "Australia", "France", "Germany", "Other"
63
+ ];
64
+
65
+ function customSort(a, b, order) {
66
+ const aIndex = order.indexOf(a.data.name);
67
+ const bIndex = order.indexOf(b.data.name);
68
+ return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex);
69
+ }
70
+
71
+ function partition(data) {
72
+ const root = d3.hierarchy(data).sum(d => d.value);
73
+
74
+ // Sort countries by manual order before partitioning
75
+ root.children.sort((a, b) => customSort(a, b, manualOrder));
76
+
77
+ // Sort professions by value (descending), with "Other" always last
78
+ root.children.forEach(country => {
79
+ if (country.children) {
80
+ country.children.sort((a, b) => {
81
+ if (a.data.name === "Other") return 1;
82
+ if (b.data.name === "Other") return -1;
83
+ return b.value - a.value;
84
+ });
85
+ }
86
+ });
87
+
88
+ return d3.partition().size([2 * Math.PI, root.height + 1])(root);
89
+ }
90
+
91
+
92
+
93
+ d3.json("json/sunburst_gender.json").then(data => {
94
+ const root = partition(data);
95
+ root.each(d => d.current = d);
96
+
97
+ // Sort countries with 'Other' last
98
+ root.children.sort((a, b) => customSort(a, b, manualOrder));
99
+
100
+ // Sort professions within countries with 'Other' last
101
+ root.children.forEach(country => {
102
+ if (country.children) {
103
+ country.children.sort((a, b) => customSort(a, b, professionOrder));
104
+ }
105
+ });
106
+
107
+ const svg = d3.select("svg")
108
+ .attr("viewBox", [0, 0, width, width])
109
+ .style("font", "10px sans-serif");
110
+
111
+ const g = svg.append("g")
112
+ .attr("transform", `translate(${width / 2},${width / 2})`);
113
+
114
+ const angleOffset = -80 * Math.PI / 180;
115
+
116
+ const arc = d3.arc()
117
+ .startAngle(d => d.x0 + angleOffset)
118
+ .endAngle(d => d.x1 + angleOffset)
119
+ .innerRadius(d => {
120
+ if (d.depth === 1) return radius * 0.1;
121
+ if (d.depth === 2) return radius * 0.5;
122
+ })
123
+ .outerRadius(d => {
124
+ if (d.depth === 1) return radius * 0.5;
125
+ if (d.depth === 2) return radius * 0.65;
126
+ });
127
+
128
+
129
+ const path = g.append("g")
130
+ .selectAll("path")
131
+ .data(root.descendants().slice(1))
132
+ .join("path")
133
+ .attr("fill", d => {
134
+ if (d.depth === 2) return colorMap[d.data.name] || "#ccc";
135
+ if (d.depth === 1) return "#fff";
136
+ return "none";
137
+ })
138
+ .attr("stroke", d => d.depth === 1 ? "#000" : null)
139
+ .attr("stroke-width", d => d.depth > 0 ? 2 : null)
140
+ .attr("d", arc);
141
+
142
+ path.append("title")
143
+ .text(d => `${d.ancestors().map(d => d.data.name).reverse().join(" → ")}\n${d.value}`);
144
+ // Count total values per gender (assuming gender is at depth 1)
145
+ const genderCounts = {};
146
+ root.children.forEach(d => {
147
+ const gender = d.data.name;
148
+ genderCounts[gender] = d.value;
149
+ });
150
+
151
+ // Add bold labels to country segments
152
+ g.selectAll("text")
153
+ .data(root.children)
154
+ .join("text")
155
+ .attr("transform", function(d) {
156
+ const angle = ((d.x0 + d.x1) / 2) + angleOffset;
157
+ const x = Math.cos(angle - Math.PI / 2) * (radius / root.height + -50);
158
+ const y = Math.sin(angle - Math.PI / 2) * (radius / root.height + -50);
159
+ return `translate(${x},${y}) rotate(${(angle / Math.PI)})`;
160
+ })
161
+ .attr("dy", "0.35em")
162
+ .attr("class", "label")
163
+ .text(d => `${d.data.name}: ${genderCounts[d.data.name] || 0}`);
164
+
165
+
166
+ // Count total values per profession (depth 2)
167
+ const professionCounts = {};
168
+ root.descendants().forEach(d => {
169
+ if (d.depth === 2) {
170
+ const name = d.data.name;
171
+ professionCounts[name] = (professionCounts[name] || 0) + d.value;
172
+ }
173
+ });
174
+
175
+ // Legend for profession colors
176
+ const legend = svg.append("g")
177
+ .attr("class", "legend")
178
+ .attr("transform", `translate(20,20)`);
179
+
180
+ professionOrder.forEach((prof, i) => {
181
+ const row = legend.append("g")
182
+ .attr("transform", `translate(0,${i * 20})`);
183
+
184
+ row.append("rect")
185
+ .attr("width", 16)
186
+ .attr("height", 16)
187
+ .attr("fill", colorMap[prof]);
188
+
189
+ row.append("text")
190
+ .attr("x", 22)
191
+ .attr("y", 12)
192
+ .text(`${prof} (${professionCounts[prof] || 0})`);
193
+ });
194
+ });
195
+
196
+ document.getElementById("downloadBtn").addEventListener("click", () => {
197
+ const svgNode = document.querySelector("svg");
198
+
199
+ // Clone the SVG node to preserve original
200
+ const clonedSvg = svgNode.cloneNode(true);
201
+ const outer = document.createElement("div");
202
+ outer.appendChild(clonedSvg);
203
+
204
+ // Add xmlns so it can be saved properly
205
+ clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
206
+
207
+ // Serialize the SVG
208
+ const svgData = new XMLSerializer().serializeToString(clonedSvg);
209
+ const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
210
+
211
+ // Create download link
212
+ const url = URL.createObjectURL(svgBlob);
213
+ const a = document.createElement("a");
214
+ a.href = url;
215
+ a.download = "sunburst_chart.svg";
216
+ document.body.appendChild(a);
217
+ a.click();
218
+ document.body.removeChild(a);
219
+ URL.revokeObjectURL(url);
220
+ });
221
+
222
+ </script>
223
+ </body>
public/Figure_8b_sunburst.html ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <meta charset="utf-8">
3
+ <style>
4
+ body {
5
+ font-family: "Segoe UI", sans-serif;
6
+ text-align: center;
7
+ margin-top: 20px;
8
+ }
9
+
10
+ svg {
11
+ font: 10px sans-serif;
12
+ }
13
+
14
+ .label {
15
+ font-weight: bold;
16
+ font-size: 12px;
17
+ text-anchor: middle;
18
+ }
19
+
20
+ .legend {
21
+ font-size: 12px;
22
+ text-anchor: start;
23
+ }
24
+
25
+ .legend rect {
26
+ stroke-width: 1;
27
+ stroke: #000;
28
+ }
29
+ </style>
30
+ <body>
31
+ <h2>Sunburst Chart: Country → Profession</h2>
32
+ <button id="downloadBtn">Download SVG</button>
33
+ <svg width="800" height="800"></svg>
34
+ <script src="https://d3js.org/d3.v6.min.js"></script>
35
+ <script>
36
+ const width = 800;
37
+ const radius = width / 3;
38
+
39
+ const colorMap = {
40
+ "Adult Performer": "#8A2BE2",
41
+ "Model": "#DC143C",
42
+ "Actor": "#FF7F50",
43
+ "Performer": "magenta",
44
+ "Singer, Musician": "wheat",
45
+ //"Fictional Character": "#708090",
46
+ "Sports Professional": "gold",
47
+ "Voice Actor": "lightgreen",
48
+ "Online Personality": "#4682B4",
49
+ "Other": "#ccc"
50
+ };
51
+
52
+ const professionOrder = [
53
+ "Actor", "Adult Performer", "Singer, Musician", "Model", "Online Personality", "Sports Professional", "Voice Actor", "Other"
54
+ ];
55
+
56
+ const manualOrder = [
57
+ "United States",
58
+ "Japan",
59
+ "United Kingdom",
60
+ "South Korea",
61
+ "Russia",
62
+ "India",
63
+ "France",
64
+ "China",
65
+ "Canada",
66
+ "Brazil",
67
+ "Germany",
68
+ "Italy",
69
+ "Australia",
70
+ "Spain",
71
+ "Ukraine",
72
+ "Turkey",
73
+ "Netherlands",
74
+ "Indonesia",
75
+ "Poland",
76
+ "Czech Republic",
77
+ "Argentina",
78
+ "Sweden",
79
+ "Mexico",
80
+ "Taiwan",
81
+ "Thailand",
82
+ "Ireland",
83
+ "Philippines",
84
+ "Hong Kong",
85
+ "Macau",
86
+ "British Virgin Islands",
87
+ "Other"
88
+ ];
89
+
90
+
91
+ function customSort(a, b, order) {
92
+ const aIndex = order.indexOf(a.data.name);
93
+ const bIndex = order.indexOf(b.data.name);
94
+ return (aIndex === -1 ? Infinity : aIndex) - (bIndex === -1 ? Infinity : bIndex);
95
+ }
96
+
97
+ function partition(data) {
98
+ const root = d3.hierarchy(data).sum(d => d.value);
99
+
100
+ // Sort countries by manual order before partitioning
101
+ root.children.sort((a, b) => customSort(a, b, manualOrder));
102
+
103
+ // Sort professions by value (descending), with "Other" always last
104
+ root.children.forEach(country => {
105
+ if (country.children) {
106
+ country.children.sort((a, b) => {
107
+ if (a.data.name === "Other") return 1;
108
+ if (b.data.name === "Other") return -1;
109
+ return b.value - a.value;
110
+ });
111
+ }
112
+ });
113
+
114
+ return d3.partition().size([2 * Math.PI, root.height + 1])(root);
115
+ }
116
+
117
+
118
+
119
+ d3.json("json/sunburst_countries_A.json").then(data => {
120
+ const root = partition(data);
121
+ root.each(d => d.current = d);
122
+
123
+ // Sort countries with 'Other' last
124
+ root.children.sort((a, b) => customSort(a, b, manualOrder));
125
+
126
+ // Sort professions within countries with 'Other' last
127
+ root.children.forEach(country => {
128
+ if (country.children) {
129
+ country.children.sort((a, b) => customSort(a, b, professionOrder));
130
+ }
131
+ });
132
+
133
+ const svg = d3.select("svg")
134
+ .attr("viewBox", [0, 0, width, width])
135
+ .style("font", "10px sans-serif");
136
+
137
+ const g = svg.append("g")
138
+ .attr("transform", `translate(${width / 2},${width / 2})`);
139
+
140
+ const arc = d3.arc()
141
+ .startAngle(d => d.x0)
142
+ .endAngle(d => d.x1)
143
+ .innerRadius(d => {
144
+ if (d.depth === 1) return radius * 0.1;
145
+ if (d.depth === 2) return radius * 0.5;
146
+ })
147
+ .outerRadius(d => {
148
+ if (d.depth === 1) return radius * 0.5;
149
+ if (d.depth === 2) return radius * 0.65;
150
+ });
151
+
152
+ const path = g.append("g")
153
+ .selectAll("path")
154
+ .data(root.descendants().slice(1))
155
+ .join("path")
156
+ .attr("fill", d => {
157
+ if (d.depth === 2) return colorMap[d.data.name] || "#ccc";
158
+ if (d.depth === 1) return "#fff";
159
+ return "none";
160
+ })
161
+ .attr("stroke", d => d.depth === 1 ? "#000" : null)
162
+ .attr("stroke-width", d => d.depth > 0 ? 2 : null)
163
+ .attr("d", arc);
164
+
165
+ path.append("title")
166
+ .text(d => `${d.ancestors().map(d => d.data.name).reverse().join(" → ")}\n${d.value}`);
167
+
168
+ // Add bold labels to country segments
169
+ g.selectAll("text")
170
+ .data(root.children)
171
+ .join("text")
172
+ .attr("transform", function(d) {
173
+ const angle = (d.x0 + d.x1) / 2;
174
+ const x = Math.cos(angle - Math.PI / 2) * (radius / root.height + -50);
175
+ const y = Math.sin(angle - Math.PI / 2) * (radius / root.height + -50);
176
+ return `translate(${x},${y}) rotate(${(angle / Math.PI)})`;
177
+ })
178
+ .attr("dy", "0.35em")
179
+ .attr("class", "label")
180
+ .text(d => d.data.name);
181
+
182
+ // Count total values per profession (depth 2)
183
+ const professionCounts = {};
184
+ root.descendants().forEach(d => {
185
+ if (d.depth === 2) {
186
+ const name = d.data.name;
187
+ professionCounts[name] = (professionCounts[name] || 0) + d.value;
188
+ }
189
+ });
190
+
191
+ // Legend for profession colors
192
+ const legend = svg.append("g")
193
+ .attr("class", "legend")
194
+ .attr("transform", `translate(20,20)`);
195
+
196
+ professionOrder.forEach((prof, i) => {
197
+ const row = legend.append("g")
198
+ .attr("transform", `translate(0,${i * 20})`);
199
+
200
+ row.append("rect")
201
+ .attr("width", 16)
202
+ .attr("height", 16)
203
+ .attr("fill", colorMap[prof]);
204
+
205
+ row.append("text")
206
+ .attr("x", 22)
207
+ .attr("y", 12)
208
+ .text(`${prof} (${professionCounts[prof] || 0})`);
209
+ });
210
+
211
+ document.getElementById("downloadBtn").addEventListener("click", () => {
212
+ const svgNode = document.querySelector("svg");
213
+
214
+ // Clone the SVG node to preserve the original
215
+ const clonedSvg = svgNode.cloneNode(true);
216
+ const outer = document.createElement("div");
217
+ outer.appendChild(clonedSvg);
218
+
219
+ // Add xmlns so it can be saved properly
220
+ clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
221
+
222
+ // Inline all computed styles
223
+ const allElements = clonedSvg.querySelectorAll("*");
224
+ const computedStyles = window.getComputedStyle(svgNode);
225
+ clonedSvg.style.font = computedStyles.font;
226
+
227
+ allElements.forEach(el => {
228
+ const style = window.getComputedStyle(el);
229
+ el.setAttribute("style", `
230
+ font: ${style.font};
231
+ fill: ${style.fill};
232
+ stroke: ${style.stroke};
233
+ stroke-width: ${style.strokeWidth};
234
+ `);
235
+ });
236
+
237
+ // Serialize the SVG
238
+ const svgData = new XMLSerializer().serializeToString(clonedSvg);
239
+ const svgBlob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });
240
+
241
+ // Create download link
242
+ const url = URL.createObjectURL(svgBlob);
243
+ const a = document.createElement("a");
244
+ a.href = url;
245
+ a.download = "sunburst_chart.svg";
246
+ document.body.appendChild(a);
247
+ a.click();
248
+ document.body.removeChild(a);
249
+ URL.revokeObjectURL(url);
250
+ });
251
+ });
252
+ </script> <!-- ✅ You were missing this -->
253
+ </body>
254
+ </html>
public/Figure_9.html ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Updated Sankey Diagram</title>
6
+ <script src="https://d3js.org/d3.v7.min.js"></script>
7
+ <script src="https://cdn.jsdelivr.net/npm/d3-sankey@0.12.3/dist/d3-sankey.min.js"></script>
8
+ <style>
9
+ body { font-family: sans-serif; margin: 0; padding: 0; }
10
+ svg { font: bold 14px sans-serif; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <svg id="sankey"></svg>
15
+ <button onclick="saveSVG()" style="margin: 10px; padding: 8px 16px;">Download SVG</button>
16
+ <svg id="sankey"></svg>
17
+
18
+ <script>
19
+ const data = [
20
+ { source: "Total", target: "Checkpoint", value: 4520 },
21
+ { source: "Total", target: "TextualInversion", value: 1590 },
22
+ { source: "Total", target: "LoRA", value: 30687 },
23
+ { source: "Total", target: "Other", value: 1327 },
24
+ { source: "Total", target: "LOCON", value: 1876 },
25
+ { source: "LoRA", target: "Adapters", value: 30687 },
26
+ { source: "LOCON", target: "Adapters", value: 1876 },
27
+ { source: "TextualInversion", target: "Adapters", value: 1590 },
28
+ { source: "Adapters", target: "has_no_tags", value: 23002 },
29
+ { source: "Adapters", target: "has_tags", value: 11151 },
30
+ { source: "has_tags", target: "has_tags + POI True", value: 2327 },
31
+ { source: "has_tags + POI True", target: "Danbooru", value: 991 },
32
+ { source: "Danbooru", target: "explicit", value: 233 },
33
+ { source: "Danbooru", target: "non-explicit", value: 758 },
34
+ { source: "has_tags + POI True", target: "CLIP Interrogator", value: 772 },
35
+ { source: "CLIP Interrogator", target: "explicit", value: 5 },
36
+ { source: "CLIP Interrogator", target: "non-explicit", value: 767 },
37
+ { source: "has_tags + POI True", target: "unknown", value: 564 },
38
+ { source: "unknown", target: "non-explicit", value: 564 },
39
+ { source: "has_tags", target: "has_tags + POI False", value: 8824 },
40
+ { source: "has_tags + POI False", target: "Danbooru", value: 6839 },
41
+ { source: "Danbooru", target: "explicit", value: 4692 },
42
+ { source: "Danbooru", target: "non-explicit", value: 2147 },
43
+ { source: "has_tags + POI False", target: "CLIP Interrogator", value: 800 },
44
+ { source: "CLIP Interrogator", target: "explicit", value: 37 },
45
+ { source: "CLIP Interrogator", target: "non-explicit", value: 763 },
46
+ { source: "has_tags + POI False", target: "unknown", value: 1185 },
47
+ { source: "unknown", target: "explicit", value: 3 },
48
+ { source: "unknown", target: "non-explicit", value: 1182 },
49
+ { source: "explicit", target: "explicit_keyword_1", value: 558 },
50
+ { source: "explicit", target: "explicit_keyword_2", value: 69 },
51
+ { source: "explicit", target: "explicit_keyword_3", value: 189 }
52
+ ];
53
+
54
+ const width = 1600;
55
+ const height = 800;
56
+
57
+ const svg = d3.select("#sankey")
58
+ .attr("width", width)
59
+ .attr("height", height);
60
+
61
+ // Extract unique node names
62
+ const nodes = Array.from(
63
+ new Set(data.flatMap(d => [d.source, d.target])),
64
+ name => ({ name })
65
+ );
66
+
67
+ const sankeyData = {
68
+ nodes,
69
+ links: data.map(d => Object.assign({}, d))
70
+ };
71
+
72
+ // Sankey layout
73
+ const sankey = d3.sankey()
74
+ .nodeId(d => d.name)
75
+ .nodeAlign(d3.sankeyLeft)
76
+ .nodeWidth(10)
77
+ .nodePadding(60)
78
+ .extent([[1, 1], [width - 1, height - 6]]);
79
+
80
+ const { nodes: layoutNodes, links: layoutLinks } = sankey(sankeyData);
81
+
82
+ // Color map (updated)
83
+ const color = name => {
84
+ const map = {
85
+ "Total": "#C0C0C0",
86
+ "Adapters": "rosybrown",
87
+ "Checkpoint": "#C0C0C0",
88
+ "LoRA": "rosybrown",
89
+ "LOCON": "rosybrown",
90
+ "Danbooru": "crimson",
91
+ "Other": "#C0C0C0",
92
+ "Textual Training Data": "coral",
93
+ "No Textual Training Data": "#ccc",
94
+ "has_tags + POI False": "silver",
95
+ "has_tags + POI True": "coral",
96
+ "Real person": "FF7F50",
97
+ "POI False": "#BC8F8F",
98
+ "explicit": "maroon",
99
+ "non-explicit": "#C0C0C0",
100
+ "has_tags": "coral",
101
+ "explicit_keyword_1": "#8A2BE2",
102
+ "explicit_keyword_2": "#8A2BE2",
103
+ "explicit_keyword_3": "#8A2BE2"
104
+ };
105
+ return map[name] || "#ccc";
106
+ };
107
+
108
+ // Label map (optional, can extend if needed)
109
+ const labelMap = {
110
+ "has_tags + POI True": "Real person",
111
+ "has_tags + POI False": "Not real person",
112
+ "has_no_tags": "No text training data",
113
+ "explicit_keyword_1": "Loli",
114
+ "explicit_keyword_2": "Shota",
115
+ "explicit_keyword_3": "Rape",
116
+ "has_tags": "Textual training data",
117
+ "explicit": "Explicit",
118
+ "non-explicit": "Not explicit",
119
+
120
+ // keep other mappings if needed
121
+ };
122
+
123
+ const defs = svg.append("defs");
124
+
125
+ layoutLinks.forEach((d, i) => {
126
+ const grad = defs.append("linearGradient")
127
+ .attr("id", d.uid = `link-gradient-${i}`)
128
+ .attr("gradientUnits", "userSpaceOnUse")
129
+ .attr("x1", d.source.x1)
130
+ .attr("x2", d.target.x0);
131
+
132
+ grad.append("stop")
133
+ .attr("offset", "0%")
134
+ .attr("stop-color", color(d.source.name));
135
+
136
+ grad.append("stop")
137
+ .attr("offset", "100%")
138
+ .attr("stop-color", color(d.target.name));
139
+ });
140
+
141
+
142
+ // Draw links
143
+ svg.append("g")
144
+ .attr("fill", "none")
145
+ .attr("stroke-opacity", 0.5)
146
+ .selectAll("path")
147
+ .data(layoutLinks)
148
+ .join("path")
149
+ .attr("d", d3.sankeyLinkHorizontal())
150
+ .attr("stroke", d => `url(#${d.uid})`)
151
+ .attr("stroke-width", d => Math.max(1, d.width));
152
+
153
+ // Draw nodes
154
+ svg.append("g")
155
+ // .attr("stroke", "#000")
156
+ .selectAll("rect")
157
+ .data(layoutNodes)
158
+ .join("rect")
159
+ .attr("x", d => d.x0)
160
+ .attr("y", d => d.y0)
161
+ .attr("height", d => d.y1 - d.y0)
162
+ .attr("width", d => d.x1 - d.x0)
163
+ .attr("fill", d => color(d.name))
164
+ .append("title")
165
+ .text(d => `${d.name}\n${d.value}`);
166
+
167
+ // Draw labels
168
+ svg.append("g")
169
+ .selectAll("text")
170
+ .data(layoutNodes)
171
+ .join("text")
172
+ .attr("x", d => d.x0 < width / 2 ? d.x1 + 10 : d.x0 - 10)
173
+ .attr("y", d => (d.y1 + d.y0) / 2)
174
+ .attr("dy", "0.35em")
175
+ .attr("text-anchor", d => d.x0 < width / 2 ? "start" : "end")
176
+ .style("font-size", "22px")
177
+ .each(function(d) {
178
+ const text = d3.select(this);
179
+ const label = labelMap[d.name] || d.name;
180
+ const lines = label.split(/(?<=\w)\s+(?=\w)/);
181
+
182
+ lines.forEach((line, i) => {
183
+ text.append("tspan")
184
+ .attr("x", d.x0 < width / 2 ? d.x1 + 10 : d.x0 - 10)
185
+ .attr("dy", i === 0 ? "0.35em" : "1.1em")
186
+ .text(line);
187
+ });
188
+
189
+ const format = d3.format(",");
190
+ text.append("tspan")
191
+ .attr("x", d.x0 < width / 2 ? d.x1 + 10 : d.x0 - 10)
192
+ .attr("dy", "1.5em")
193
+ .style("font-size", "20px")
194
+ .style("fill", "#555")
195
+ .text(`${format(d.value)}`);
196
+ });
197
+
198
+ function inlineStyles(svgElement) {
199
+ const styles = `
200
+ text { font-family: sans-serif; fill: black; font-weight: bold; }
201
+ rect { stroke: none; }
202
+ path { stroke-opacity: 0.5; }
203
+ `;
204
+ const styleElem = document.createElementNS("http://www.w3.org/2000/svg", "style");
205
+ styleElem.textContent = styles;
206
+ svgElement.insertBefore(styleElem, svgElement.firstChild);
207
+ }
208
+
209
+ function saveSVG() {
210
+ const svgElement = document.querySelector("svg");
211
+ inlineStyles(svgElement); // inject inline CSS
212
+ const serializer = new XMLSerializer();
213
+ const source = serializer.serializeToString(svgElement);
214
+ const svgBlob = new Blob([source], { type: "image/svg+xml;charset=utf-8" });
215
+ const svgUrl = URL.createObjectURL(svgBlob);
216
+ const downloadLink = document.createElement("a");
217
+ downloadLink.href = svgUrl;
218
+ downloadLink.download = "sankey-diagram.svg";
219
+ document.body.appendChild(downloadLink);
220
+ downloadLink.click();
221
+ document.body.removeChild(downloadLink);
222
+ }
223
+
224
+ </script>
225
+ </body>
226
+ </html>
public/Figure_9_old.html ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Unified Sankey Diagram</title>
6
+ <script src="https://d3js.org/d3.v7.min.js"></script>
7
+ <script src="https://cdn.jsdelivr.net/npm/d3-sankey@0.12.3/dist/d3-sankey.min.js"></script>
8
+ <style>
9
+ body { font-family: sans-serif; }
10
+ svg { font: bold 14px sans-serif; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <svg id="sankey"></svg>
15
+ <script>
16
+ const data = [
17
+ { source: "Total", target: "Checkpoint", value: 4520 },
18
+ { source: "Total", target: "Other", value: 1327 },
19
+ { source: "Total", target: "Adapters", value: 34153 },
20
+ { source: "Adapters", target: "Textual Training Data", value: 11151 },
21
+ { source: "Adapters", target: "Unknown Textual Training Data", value: 23002 },
22
+ { source: "Textual Training Data", target: "POI True", value: 2327 },
23
+ { source: "Textual Training Data", target: "POI False", value: 8824 },
24
+ { source: "POI True", target: "explicit", value: 238 },
25
+ { source: "POI True", target: "non-explicit", value: 2089 },
26
+ { source: "POI False", target: "explicit", value: 4732 },
27
+ { source: "POI False", target: "non-explicit", value: 4092 },
28
+ { source: "explicit", target: "loli", value: 558 },
29
+ { source: "explicit", target: "shota", value: 69 },
30
+ { source: "explicit", target: "rape", value: 189 }
31
+ ];
32
+
33
+ ////////////////////////////////////////// SAVE SVG //////////////////////////////////////////
34
+
35
+ function inlineStyles(svgElement) {
36
+ const styles = `
37
+ text { font-family: sans-serif; fill: black; font-weight: bold; }
38
+ rect { stroke: none; }
39
+ `;
40
+ const styleElem = document.createElementNS("http://www.w3.org/2000/svg", "style");
41
+ styleElem.textContent = styles;
42
+ svgElement.insertBefore(styleElem, svgElement.firstChild);
43
+ }
44
+
45
+ function saveSVG() {
46
+ const svgElement = document.querySelector("svg");
47
+ inlineStyles(svgElement); // inject style
48
+ const serializer = new XMLSerializer();
49
+ const source = serializer.serializeToString(svgElement);
50
+ const svgBlob = new Blob([source], { type: "image/svg+xml;charset=utf-8" });
51
+ const svgUrl = URL.createObjectURL(svgBlob);
52
+ const downloadLink = document.createElement("a");
53
+ downloadLink.href = svgUrl;
54
+ downloadLink.download = "sankey-diagram.svg";
55
+ document.body.appendChild(downloadLink);
56
+ downloadLink.click();
57
+ document.body.removeChild(downloadLink);
58
+ }
59
+
60
+ window.addEventListener("keydown", function (e) {
61
+ if (e.code === "Space") {
62
+ e.preventDefault(); // prevent page scrolling
63
+ saveSVG();
64
+ }
65
+ });
66
+ ////////////////////////////////////////// SAVE SVG //////////////////////////////////////////
67
+
68
+
69
+
70
+ const width = 680;
71
+ const height = 400;
72
+
73
+ const svg = d3.select("#sankey")
74
+ .attr("width", width)
75
+ .attr("height", height);
76
+
77
+ const sankey = d3.sankey()
78
+ .nodeId(d => d.name)
79
+ .nodeAlign(d3.sankeyLeft)
80
+ .nodeWidth(15)
81
+ .nodePadding(20)
82
+ .extent([[1, 1], [width - 1, height - 6]]);
83
+
84
+ const nodeSet = new Set();
85
+ data.forEach(d => {
86
+ nodeSet.add(d.source);
87
+ nodeSet.add(d.target);
88
+ });
89
+ const nodes = Array.from(nodeSet).map(name => ({ name }));
90
+
91
+ const { nodes: layoutNodes, links: layoutLinks } = sankey({
92
+ nodes: nodes.map(d => Object.assign({}, d)),
93
+ links: data.map(d => Object.assign({}, d))
94
+ });
95
+
96
+
97
+
98
+ const color = name => {
99
+ const map = {
100
+ "Total": "#C0C0C0",
101
+ "Adapters": "#C0C0C0",
102
+ "Checkpoint": "#C0C0C0",
103
+ "Other": "#C0C0C0",
104
+ "Textual Training Data": "#BC8F8F",
105
+ "No Textual Training Data": "#ccc",
106
+ "POI True": "#FFA07A",
107
+ "POI False": "#BC8F8F",
108
+ "explicit": "#DC143C",
109
+ "non-explicit": "#C0C0C0",
110
+ "loli": "#8A2BE2",
111
+ "shota": "#8A2BE2",
112
+ "rape": "#8A2BE2"
113
+ };
114
+ return map[name] || "#ccc";
115
+ };
116
+
117
+ const labelMap = {
118
+ "Textual Training Data": "Textual training data found",
119
+ "No Textual Training Data": "No textual training data",
120
+ "POI True": "POI true",
121
+ "POI False": "POI false",
122
+ "non-explicit": "Non-explicit",
123
+ "explicit": "Explicit",
124
+ "Total": "Total",
125
+ "Adapters": "Adapters",
126
+ "Checkpoint": "Checkpoint",
127
+ "Other": "Other",
128
+ "loli": "Loli",
129
+ "shota": "Shota",
130
+ "rape": "Rape"
131
+ };
132
+
133
+
134
+ const defs = svg.append("defs");
135
+ layoutLinks.forEach((d, i) => {
136
+ const grad = defs.append("linearGradient")
137
+ .attr("id", d.uid = `link-${i}`)
138
+ .attr("gradientUnits", "userSpaceOnUse")
139
+ .attr("x1", d.source.x1)
140
+ .attr("x2", d.target.x0);
141
+
142
+ grad.append("stop").attr("offset", "0%").attr("stop-color", color(d.source.name));
143
+ grad.append("stop").attr("offset", "100%").attr("stop-color", color(d.target.name));
144
+ });
145
+
146
+ svg.append("g")
147
+ .attr("fill", "none")
148
+ .attr("stroke-opacity", 0.5)
149
+ .selectAll("path")
150
+ .data(layoutLinks)
151
+ .join("path")
152
+ .attr("d", d3.sankeyLinkHorizontal())
153
+ .attr("stroke", d => `url(#${d.uid})`)
154
+ .attr("stroke-width", d => Math.max(1, d.width))
155
+ .append("title")
156
+ .text(d => `${d.source.name} → ${d.target.name}\n${d.value}`);
157
+
158
+ svg.append("g")
159
+ .selectAll("rect")
160
+ .data(layoutNodes)
161
+ .join("rect")
162
+ .attr("x", d => d.x0)
163
+ .attr("y", d => d.y0)
164
+ .attr("height", d => d.y1 - d.y0)
165
+ .attr("width", d => d.x1 - d.x0)
166
+ .attr("fill", d => color(d.name))
167
+ .append("title")
168
+ .text(d => `${d.name}\n${d.value}`);
169
+
170
+ svg.append("g")
171
+ .selectAll("text")
172
+ .data(layoutNodes)
173
+ .join("text")
174
+ .attr("x", d => d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
175
+ .attr("y", d => (d.y1 + d.y0) / 2)
176
+ .attr("dy", "0.35em")
177
+ .attr("text-anchor", d => d.x0 < width / 2 ? "start" : "end")
178
+ .style("font-size", "16px")
179
+ .each(function(d) {
180
+ const text = d3.select(this);
181
+ const label = labelMap[d.name] || d.name;
182
+ const lines = label.split(/(?<=\w)\s+(?=\w)/); // split long words
183
+
184
+ lines.forEach((line, i) => {
185
+ text.append("tspan")
186
+ .attr("x", d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
187
+ .attr("dy", i === 0 ? "0.35em" : "1.2em")
188
+ .text(line);
189
+ });
190
+
191
+ /* const base = layoutNodes.find(n => n.name === "Total"); /// Percentages
192
+ if (base && base.value > 0) {
193
+ const pct = ((d.value / base.value) * 100).toFixed(1);
194
+ text.append("tspan")
195
+ .attr("x", d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
196
+ .attr("dy", "1.5em")
197
+ .style("font-size", "14px")
198
+ .style("fill", "#333")
199
+ .text(`${pct}%`); */
200
+
201
+ const format = d3.format(",");
202
+ text.append("tspan")
203
+ .attr("x", d.x0 < width / 2 ? d.x1 + 6 : d.x0 - 6)
204
+ .attr("dy", "1.5em")
205
+ .style("font-size", "14px")
206
+ .style("fill", "#333")
207
+ .text(`${format(d.value)}`);
208
+
209
+
210
+ });
211
+ </script>
212
+ </body>
213
+ </html>
public/csv/sankey_data.csv ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ source,target,value
2
+ Total,Checkpoint,4520
3
+ Total,TextualInversion,1590
4
+ Total,LoRA,30687
5
+ Total,Other,1327
6
+ Total,LOCON,1876
7
+ LoRA,Adapters,30687
8
+ LOCON,Adapters,1876
9
+ TextualInversion,Adapters,1590
10
+ Adapters,has_no_tags,23002
11
+ Adapters,has_tags,11151
12
+ has_tags,has_tags + POI True,2327
13
+ has_tags + POI True,has_tags + POI True + explicit,238
14
+ has_tags + POI True,has_tags + POI True + non-explicit,2089
15
+ has_tags,has_tags + POI False,8824
16
+ has_tags + POI False,has_tags + POI False + explicit,4732
17
+ has_tags + POI False,has_tags + POI False + non-explicit,4092
18
+ has_tags,All Adapters has_tags + explicit,4970
19
+ has_tags,All Adapters has_tags + non-explicit,6181
20
+ explicit,has_tag_rape,189
21
+ explicit,has_tag_loli,558
22
+ explicit,has_tag_shota,69
public/figure_15.html ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <script src="https://d3js.org/d3.v7.min.js"></script>
6
+ <link rel="stylesheet" href="styles.css">
7
+
8
+ </head>
9
+ <body>
10
+ <!-- Header Overlay -->
11
+ <div class="header-overlay">
12
+ <div class="visualization-container">
13
+ <h1>Semantic Network of Textual Training Data</h1>
14
+ <h2 class="caption">Co-occurence of textual captions (tags) extracted from model adapters downloaded from CivitAI</h2>
15
+
16
+ <div class="nav-buttons">
17
+
18
+
19
+
20
+
21
+
22
+
23
+ </div>
24
+ <button class="nav-button" onclick="location.href='index.html'">Home</button>
25
+ <button class="nav-button" onclick="location.href='figure_5.html'">Next: Promotional Tags</button>
26
+
27
+
28
+ <div class="theme-toggle">
29
+ <label><input type="checkbox" id="modeToggle"> Toggle Light Mode</label>
30
+
31
+
32
+ <div class="controls">
33
+ <label>Number of tags shown:
34
+ <input type="range" id="tagSlider" min="0" max="2000" value="100" step="10" />
35
+ <output id="tagCount">500</output>
36
+ </label><br/>
37
+
38
+ <button id="resetBtn">Reset</button>
39
+ <button id="downloadBtn">Download SVG</button>
40
+ </div>
41
+ </div>
42
+
43
+
44
+
45
+ </div>
46
+
47
+ </div>
48
+
49
+ </div>
50
+
51
+ <!-- Visualization SVG -->
52
+ <svg viewBox="0 0 1600 1600" preserveAspectRatio="xMidYMid meet"></svg>
53
+
54
+ <script>
55
+ const width = 1600, height = 1600;
56
+ let fullGraph = null;
57
+ let currentTransform = d3.zoomIdentity;
58
+
59
+ const categoryColors = {
60
+ visual_characteristics: "#708090",
61
+ attire_and_body_accessories: "#DC143C",
62
+ body: "#FF7F50",
63
+ more: "#BC8F8F",
64
+ subject: "black",
65
+ sex: "#8A2BE2",
66
+ default: "#C0C0C0"
67
+ };
68
+
69
+ const categoryLabels = {
70
+ visual_characteristics: "Image Characteristics",
71
+ attire_and_body_accessories: "Attire & Accessories",
72
+ body: "Body & Face",
73
+ more: "Actions & Probs",
74
+ subject: "Subjects",
75
+ sex: "Adult Content",
76
+ default: "Other"
77
+ };
78
+
79
+ const categoryPositions = {};
80
+ const categories = Object.keys(categoryColors);
81
+ categories.forEach((cat, i) => {
82
+ const angle = (i / categories.length) * 2 * Math.PI;
83
+ categoryPositions[cat] = {
84
+ x: width / 2 * Math.cos(angle),
85
+ y: height / 2 * Math.sin(angle)
86
+ };
87
+ });
88
+
89
+ const svg = d3.select("svg");
90
+ const zoomLayer = svg.append("g")
91
+ .attr("class", "zoom-layer")
92
+ .attr("transform", "translate(300, 300) scale(0.45)");
93
+
94
+
95
+ // Create legend outside zoom layer
96
+ const legendWrapper = svg.append("g")
97
+ .attr("class", "legend-wrapper")
98
+ .attr("transform", "translate(10, 400)");
99
+ const legendItemHeight = 24;
100
+
101
+ // Initialize zoom behavior
102
+ const zoom = d3.zoom()
103
+ .scaleExtent([0.01, 8])
104
+ .on("zoom", (event) => {
105
+ currentTransform = event.transform;
106
+ zoomLayer.attr("transform", currentTransform);
107
+ });
108
+
109
+ svg.call(zoom);
110
+
111
+ const simulation = d3.forceSimulation()
112
+ .force("link", d3.forceLink().id(d => d.id).distance(500))
113
+ .force("charge", d3.forceManyBody().strength(-185))
114
+ .force("center", d3.forceCenter(width / 2, (height / 1.5) - 200))
115
+ .force("collide", d3.forceCollide().radius(d => Math.sqrt(d.size) * 0.111))
116
+ .force("category", forceCluster(0.1))
117
+ .alphaDecay(0.15);
118
+
119
+ function forceCluster(strength = 89.05) {
120
+ function force(alpha) {
121
+ for (const d of simulation.nodes()) {
122
+ const centroid = categoryPositions[d.category] || categoryPositions.default;
123
+ d.vx += (centroid.x - d.x) * strength * alpha;
124
+ d.vy += (centroid.y - d.y) * strength * alpha;
125
+ }
126
+ }
127
+ force.initialize = () => {};
128
+ return force;
129
+ }
130
+
131
+ d3.json("json/co_occurrence_network.json").then(graph => {
132
+ fullGraph = graph;
133
+ render(+document.getElementById("tagSlider").value);
134
+ });
135
+
136
+ function initializeZoom() {
137
+ const bounds = zoomLayer.node().getBBox();
138
+ const contentWidth = bounds.width;
139
+ const contentHeight = bounds.height;
140
+
141
+ // Calculate scale to fit content with some padding
142
+ const scale = 0.4 / Math.max(
143
+ contentWidth / width,
144
+ contentHeight / height
145
+ );
146
+
147
+ // Calculate translation to center content
148
+ const translateX = (width - contentWidth * scale) / 3 - bounds.x * scale;
149
+ const translateY = (height - contentHeight * scale) / 2 - bounds.y * scale;
150
+
151
+ // Apply the transform smoothly
152
+ svg.transition()
153
+ .duration(1000)
154
+ .call(
155
+ zoom.transform,
156
+ d3.zoomIdentity
157
+ .translate(translateX, translateY)
158
+ .scale(scale)
159
+ );
160
+ }
161
+
162
+ function render(nodeCount) {
163
+ zoomLayer.selectAll("*").remove();
164
+
165
+ zoomLayer.append("rect")
166
+ .attr("width", width)
167
+ .attr("height", height)
168
+ .attr("fill", getComputedStyle(document.documentElement).getPropertyValue('--svg-fill') || "#fff");
169
+
170
+ const topNodes = fullGraph.nodes.sort((a, b) => b.size - a.size).slice(0, nodeCount);
171
+ const nodeIds = new Set(topNodes.map(d => d.id));
172
+ const nodesWithPos = topNodes.map(d => {
173
+ const centroid = categoryPositions[d.category] || categoryPositions.default;
174
+ return { ...d, x: centroid.x + Math.random() * 100 - 50, y: centroid.y + Math.random() * 100 - 50 };
175
+ });
176
+
177
+ const nodeLookup = new Map(nodesWithPos.map(n => [n.id, n]));
178
+
179
+ const filteredLinks = fullGraph.links
180
+ .filter(l => nodeIds.has(l.source) && nodeIds.has(l.target))
181
+ .map(l => {
182
+ const source = nodeLookup.get(l.source);
183
+ const target = nodeLookup.get(l.target);
184
+ return {
185
+ ...l,
186
+ source,
187
+ target,
188
+ dominant: source.size >= target.size ? source : target
189
+ };
190
+ });
191
+
192
+ const linkValueExtent = d3.extent(filteredLinks, d => d.value);
193
+ const linkStrokeScale = d3.scaleLinear().domain(linkValueExtent).range([0.5, 6]);
194
+
195
+ const link = zoomLayer.append("g").selectAll("line")
196
+ .data(filteredLinks)
197
+ .enter().append("line")
198
+ .attr("stroke", d => categoryColors[d.dominant.category] || categoryColors.default)
199
+ .attr("stroke-width", d => linkStrokeScale(d.value))
200
+ .attr("stroke-opacity", d => Math.min(0.4, d.value / 100));
201
+
202
+ const node = zoomLayer.append("g").selectAll("circle.main")
203
+ .data(nodesWithPos)
204
+ .enter().append("circle")
205
+ .attr("class", "node main")
206
+ .attr("r", d => Math.max(1, Math.sqrt(d.size) * 0.04))
207
+ .attr("fill", d => categoryColors[d.category] || categoryColors.default)
208
+ .attr("fill-opacity", d => Math.max(0.2, Math.min(1, d.size / 200)))
209
+ .call(d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended));
210
+
211
+ const labelGroup = zoomLayer.append("g").selectAll("g.label")
212
+ .data(nodesWithPos)
213
+ .enter().append("g")
214
+ .attr("class", "label");
215
+
216
+ labelGroup.append("text")
217
+ .attr("text-anchor", "start")
218
+ .attr("dy", "0.35em")
219
+ .attr("fill", "black")
220
+ .attr("fill-opacity", 0.7)
221
+ .style("font-size", d => `${Math.max(11, Math.sqrt(d.size) * 0.04)}px`)
222
+ .text(d => d.id);
223
+
224
+ labelGroup.insert("rect", "text")
225
+ .attr("rx", 4)
226
+ .attr("ry", 4)
227
+ .attr("fill", "white")
228
+ .attr("fill-opacity", 0.7);
229
+
230
+ // Update legend
231
+ legendWrapper.selectAll("*").remove();
232
+
233
+ const legend = legendWrapper.selectAll(".legend")
234
+ .data(Object.keys(categoryColors))
235
+ .enter().append("g")
236
+ .attr("class", "legend")
237
+ .attr("transform", (d, i) => `translate(10,${i * legendItemHeight + 10})`);
238
+
239
+ legend.append("rect")
240
+ .attr("width", 12)
241
+ .attr("height", 12)
242
+ .attr("fill", d => categoryColors[d]);
243
+
244
+ legend.append("text")
245
+ .attr("x", 18)
246
+ .attr("y", 6)
247
+ .attr("dy", "0.35em")
248
+ .style("font-size", "14px")
249
+ .attr("fill", "black")
250
+ .text(d => categoryLabels[d] || d);
251
+
252
+ simulation.nodes(nodesWithPos).on("tick", () => {
253
+ link.attr("x1", d => d.source.x).attr("y1", d => d.source.y)
254
+ .attr("x2", d => d.target.x).attr("y2", d => d.target.y);
255
+
256
+ node.attr("cx", d => d.x).attr("cy", d => d.y);
257
+
258
+ labelGroup.each(function(d) {
259
+ const text = d3.select(this).select("text");
260
+ const rect = d3.select(this).select("rect");
261
+ const bbox = text.node().getBBox();
262
+ const padding = 2;
263
+ const offset = Math.max(2, Math.sqrt(d.size) * 0.045 + padding);
264
+
265
+ d3.select(this).attr("transform", `translate(${d.x + offset},${d.y})`);
266
+ rect
267
+ .attr("x", bbox.x - padding)
268
+ .attr("y", bbox.y - padding)
269
+ .attr("width", bbox.width + padding * 2)
270
+ .attr("height", bbox.height + padding * 2);
271
+ });
272
+ });
273
+
274
+ simulation.force("link").links(filteredLinks);
275
+ simulation.alpha(1).restart();
276
+
277
+ // Initialize zoom after simulation stabilizes
278
+ //simulation.on("end", initializeZoom);
279
+ }
280
+
281
+ function dragstarted(event, d) {
282
+ if (!event.active) simulation.alphaTarget(0.3).restart();
283
+ d.fx = d.x;
284
+ d.fy = d.y;
285
+ }
286
+
287
+ function dragged(event, d) {
288
+ d.fx = event.x;
289
+ d.fy = event.y;
290
+ }
291
+
292
+ function dragended(event, d) {
293
+ if (!event.active) simulation.alphaTarget(0);
294
+ d.fx = null;
295
+ d.fy = null;
296
+ }
297
+
298
+ function saveSvg() {
299
+ const svgNode = document.querySelector("svg");
300
+ const serializer = new XMLSerializer();
301
+ const source = serializer.serializeToString(svgNode);
302
+ const blob = new Blob([source], { type: "image/svg+xml;charset=utf-8" });
303
+ const url = URL.createObjectURL(blob);
304
+ const a = document.createElement("a");
305
+ a.href = url;
306
+ a.download = "graph.svg";
307
+ document.body.appendChild(a);
308
+ a.click();
309
+ document.body.removeChild(a);
310
+ URL.revokeObjectURL(url);
311
+ }
312
+
313
+ document.getElementById("tagSlider").addEventListener("input", function () {
314
+ const value = +this.value;
315
+ document.getElementById("tagCount").textContent = value;
316
+ render(value);
317
+ });
318
+
319
+ document.getElementById("modeToggle").addEventListener("change", function () {
320
+ document.documentElement.classList.toggle("light-mode");
321
+ render(+document.getElementById("tagSlider").value);
322
+ });
323
+ </script>
324
+ </body>
325
+ </html>
public/figure_5.html ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <script src="https://d3js.org/d3.v7.min.js"></script>
6
+ <link rel="stylesheet" href="styles.css">
7
+
8
+ </head>
9
+ <body>
10
+ <!-- Header Overlay -->
11
+ <div class="header-overlay">
12
+ <div class="visualization-container">
13
+ <h1>Tag Co-Occurrence Network</h1>
14
+ <h2 class="caption">Thematic clustering of the most frequent promotional tags used across all shared assets on CivitAI</h2>
15
+
16
+ <div class="nav-buttons">
17
+
18
+
19
+
20
+
21
+
22
+
23
+ </div>
24
+ <button class="nav-button" onclick="location.href='index.html'">Home</button>
25
+ <button class="nav-button" onclick="location.href='figure_15.html'">Next: Textual Training Data</button>
26
+
27
+
28
+ <div class="theme-toggle">
29
+ <label><input type="checkbox" id="modeToggle"> Toggle Light Mode</label>
30
+
31
+
32
+ <div class="controls">
33
+ <label>Top N Nodes:
34
+ <input type="range" id="nodeCountInput" value="30" min="1" max="300" />
35
+ <output id="nodeCountOutput">20</output>
36
+ </label><br/>
37
+
38
+ <label>Link Distance Scale:
39
+ <input type="range" id="linkScaleInput" value="1500" min="0" max="10000" />
40
+ <output id="linkScaleOutput">0</output>
41
+ </label><br/>
42
+
43
+ <label>Charge Strength:
44
+ <input type="range" id="chargeInput" value="10" min="0" max="50" />
45
+ <output id="chargeOutput">5</output>
46
+ </label><br/>
47
+
48
+ <label>Collide Strength:
49
+ <input type="range" id="collideInput" value="2" min="0" max="10" step="0.1" />
50
+ <output id="collideOutput">2</output>
51
+ </label><br/>
52
+
53
+ <label>Collide Base Radius:
54
+ <input type="range" id="collideBaseInput" value="22" min="0" max="35" />
55
+ <output id="collideBaseOutput">17</output>
56
+ </label><br/>
57
+
58
+ <label>Node Radius Scale:
59
+ <input type="range" id="radiusInput" value="65" min="5" max="80" />
60
+ <output id="radiusOutput">30</output>
61
+ </label><br/>
62
+
63
+ <label>Font Size Scale:
64
+ <input type="range" id="fontInput" value="25" min="1" max="50" />
65
+ <output id="fontOutput">25</output>
66
+ </label><br/>
67
+
68
+ <label>Link Thickness Base:
69
+ <input type="range" id="LinkStrokeIn" value="30" min="1" max="50" />
70
+ <output id="LinkStrokeOut">5</output>
71
+ </label><br/>
72
+
73
+ <label for="jsonSelector">Choose a JSON file:</label>
74
+ <select id="jsonSelector">
75
+ <option value="json/nodes_all.json">nodes_all.json</option>
76
+ <option value="json/nodes.json">nodes.json</option>
77
+ <option value="json/promo_tags_poi_true.json">promo_tags_poi_true.json</option>
78
+ <option value="json/promo_tags.json">promo_tags.json</option>
79
+ <option value="json/sunburst_data.json">sunburst_data.json</option>
80
+ <option value="json/tags_actor.json">tags_actor.json</option>
81
+ <option value="json/tags_actress.json">tags_actress.json</option>
82
+ <option value="json/tags_all_poi.json">tags_all_poi.json</option>
83
+ <option value="json/tags_american_poi.json">tags_american_poi.json</option>
84
+ <option value="json/tags_american.json">tags_american.json</option>
85
+ <option value="json/tags_asian_poi.json">tags_asian_poi.json</option>
86
+ <option value="json/tags_asian.json">tags_asian.json</option>
87
+ <option value="json/tags_canada.json">tags_canada.json</option>
88
+ <option value="json/tags_celebrity.json">tags_celebrity.json</option>
89
+ <option value="json/tags_character.json">tags_character.json</option>
90
+ <option value="json/tags_china.json">tags_china.json</option>
91
+ <option value="json/tags_chinese_poi.json">tags_chinese_poi.json</option>
92
+ <option value="json/tags_chinese.json">tags_chinese.json</option>
93
+ <option value="json/tags_german_poi.json">tags_german_poi.json</option>
94
+ <option value="json/tags_german.json">tags_german.json</option>
95
+ <option value="json/tags_germany.json">tags_germany.json</option>
96
+ <option value="json/tags_india.json">tags_india.json</option>
97
+ <option value="json/tags_indian_poi.json">tags_indian_poi.json</option>
98
+ <option value="json/tags_indian.json">tags_indian.json</option>
99
+ <option value="json/tags_instagram.json">tags_instagram.json</option>
100
+ <option value="json/tags_japan.json">tags_japan.json</option>
101
+ <option value="json/tags_japanese_poi.json">tags_japanese_poi.json</option>
102
+ <option value="json/tags_japanese.json">tags_japanese.json</option>
103
+ <option value="json/tags_korea.json">tags_korea.json</option>
104
+ <option value="json/tags_korean_poi.json">tags_korean_poi.json</option>
105
+ <option value="json/tags_korean.json">tags_korean.json</option>
106
+ <option value="json/tags_kpop.json">tags_kpop.json</option>
107
+ <option value="json/tags_man_poi.json">tags_man_poi.json</option>
108
+ <option value="json/tags_man.json">tags_man.json</option>
109
+ <option value="json/tags_russia.json">tags_russia.json</option>
110
+ <option value="json/tags_russian_poi.json">tags_russian_poi.json</option>
111
+ <option value="json/tags_russian.json">tags_russian.json</option>
112
+ <option value="json/tags_style_poi.json">tags_style_poi.json</option>
113
+ <option value="json/tags_style.json">tags_style.json</option>
114
+ <option value="json/tags_tiktok.json">tags_tiktok.json</option>
115
+ <option value="json/tags_uk.json">tags_uk.json</option>
116
+ <option value="json/tags_ukrainian.json">tags_ukrainian.json</option>
117
+ <option value="json/tags_woman_poi.json">tags_woman_poi.json</option>
118
+ <option value="json/tags_woman.json">tags_woman.json</option>
119
+ <option value="json/tags_youtuber.json">tags_youtuber.json</option>
120
+ <option value="json/tree_data.json">tree_data.json</option>
121
+ </select>
122
+
123
+ <button id="resetBtn">Reset</button>
124
+ <button id="downloadBtn">Download SVG</button>
125
+ </div>
126
+ </div>
127
+
128
+
129
+
130
+ </div>
131
+
132
+ </div>
133
+
134
+ </div>
135
+
136
+
137
+
138
+ <!-- Main Content -->
139
+
140
+
141
+ <!-- Visualization -->
142
+ <!-- Change the SVG element to this -->
143
+ <svg viewBox="0 0 1600 1200" preserveAspectRatio="xMidYMid meet"></svg>
144
+
145
+ <!-- Theme Toggle -->
146
+
147
+ <!-- Navigation -->
148
+
149
+
150
+ <script>
151
+ const svgElement = document.querySelector("svg");
152
+ const width = 1600;
153
+ const height = 2000;
154
+ const NODE_BASE_RADIUS = 1;
155
+ const FONT_SIZE_BASE = 14;
156
+
157
+ let LINK_DISTANCE_SCALE = -200;
158
+ let CHARGE_STRENGTH = 80;
159
+ let COLLIDE_STRENGTH = 2;
160
+ let COLLIDE_BASE_RADIUS = 17;
161
+ let NODE_RADIUS_SCALE = 30;
162
+ let FONT_SIZE_SCALE = 25;
163
+ let LINK_THICKNESS_BASE = 5;
164
+
165
+ const svg = d3.select("svg");
166
+ const zoomLayer = svg.append("g").attr("class", "zoom-layer");
167
+
168
+ // Add zoom/pan behavior
169
+ svg.call(d3.zoom()
170
+ .scaleExtent([0.1, 8])
171
+ .on("zoom", (event) => {
172
+ zoomLayer.attr("transform", event.transform);
173
+ })
174
+ );
175
+
176
+ // Set initial background
177
+ zoomLayer.append("rect")
178
+ .attr("width", width)
179
+ .attr("height", height)
180
+ .attr("fill", getComputedStyle(document.documentElement).getPropertyValue('--svg-fill').trim());
181
+
182
+ let fullGraph = null;
183
+
184
+ function renderGraph(graph, nodeCount = 1) {
185
+ zoomLayer.selectAll("*").remove();
186
+
187
+ zoomLayer.append("rect")
188
+ .attr("width", width)
189
+ .attr("height", height)
190
+ .attr("fill", getComputedStyle(document.documentElement).getPropertyValue('--svg-fill').trim());
191
+
192
+ const nodeMap = new Map(graph.nodes.map(n => [n.id, n]));
193
+
194
+ graph.nodes.sort((a, b) => b.size - a.size);
195
+ const filteredNodes = graph.nodes.slice(0, nodeCount);
196
+ const topIds = new Set(filteredNodes.map(n => n.id));
197
+ const filteredLinks = graph.links
198
+ .filter(link => topIds.has(link.source) && topIds.has(link.target) && link.value >= 1)
199
+ .map(link => ({
200
+ ...link,
201
+ source: nodeMap.get(link.source),
202
+ target: nodeMap.get(link.target)
203
+ }));
204
+
205
+ const [minNodeSize, maxNodeSize] = d3.extent(filteredNodes, n => n.size);
206
+ filteredNodes.forEach(n => {
207
+ n.normSize = (n.size - minNodeSize) / (maxNodeSize - minNodeSize || 1);
208
+ });
209
+
210
+ const [minLinkVal, maxLinkVal] = d3.extent(filteredLinks, d => d.value);
211
+ filteredLinks.forEach(d => {
212
+ d.normValue = (d.value - minLinkVal) / (maxLinkVal - minLinkVal || 1);
213
+ d.distance = 100 + d.normValue * LINK_DISTANCE_SCALE;
214
+ });
215
+
216
+ const edgeColor = d3.scaleLinear()
217
+ .domain([0, 0.3, 1])
218
+ .interpolate(d3.interpolateRgb)
219
+ .range(["#BC8F8F", "#FF7F50", "#800000"]);
220
+
221
+ const link = zoomLayer.append("g").selectAll("line")
222
+ .data(filteredLinks)
223
+ .enter().append("line")
224
+ .attr("stroke", d => edgeColor(d.normValue))
225
+ .attr("stroke-width", d => Math.max(0.1, d.value / maxLinkVal * LINK_THICKNESS_BASE))
226
+ .attr("stroke-opacity", d => d.normValue + 80)
227
+
228
+ const node = zoomLayer.append("g").selectAll("circle")
229
+ .data(filteredNodes)
230
+ .enter().append("circle")
231
+ .attr("r", d => d.size / maxNodeSize * NODE_RADIUS_SCALE)
232
+ .attr("fill", "#FF7F50")
233
+ .attr("stroke", "#800000")
234
+ .attr("stroke-width", 5)
235
+ .call(d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended));
236
+
237
+ const labels = zoomLayer.append("g")
238
+ .attr("class", "label-group")
239
+ .selectAll("g")
240
+ .data(filteredNodes)
241
+ .enter().append("g")
242
+ .attr("class", "label");
243
+
244
+ labels.append("text")
245
+ .attr("text-anchor", "start")
246
+ .style("font-weight", "bold")
247
+ .style("font-size", d => `${FONT_SIZE_BASE + d.normSize * FONT_SIZE_SCALE}px`)
248
+ .attr("x", d => NODE_BASE_RADIUS + d.normSize * NODE_RADIUS_SCALE + 6)
249
+ .text(d => d.id);
250
+
251
+ const top10 = filteredNodes.slice(0, 3);
252
+ const insideLabels = zoomLayer.append("g")
253
+ .selectAll("text")
254
+ .data(top10)
255
+ .enter().append("text")
256
+ .attr("text-anchor", "middle")
257
+ .attr("dy", "0.35em")
258
+ .style("font-weight", "bold")
259
+ .style("fill", "#800000")
260
+ .style("pointer-events", "none")
261
+ .style("font-size", d => `${FONT_SIZE_BASE + d.normSize * FONT_SIZE_SCALE * 0.2}px`)
262
+ .text(d => d.size);
263
+
264
+ labels.each(function(d) {
265
+ const group = d3.select(this);
266
+ const text = group.select("text");
267
+
268
+ const fontSize = FONT_SIZE_BASE + d.normSize * FONT_SIZE_SCALE;
269
+ const paddingX = 4;
270
+ const paddingY = 2;
271
+
272
+ const labelX = NODE_BASE_RADIUS + d.normSize * NODE_RADIUS_SCALE + 6;
273
+ text
274
+ .attr("x", labelX)
275
+ .attr("y", 0)
276
+ .attr("dy", "0.35em")
277
+ .style("font-size", `${fontSize}px`);
278
+
279
+ const actualWidth = text.node().getComputedTextLength();
280
+ const actualHeight = fontSize;
281
+
282
+ group.insert("rect", "text")
283
+ .attr("x", labelX - paddingX)
284
+ .attr("y", -actualHeight / 2 - paddingY)
285
+ .attr("width", actualWidth + paddingX * 0.2)
286
+ .attr("height", actualHeight + paddingY * 2)
287
+ .attr("rx", 4)
288
+ .attr("ry", 4)
289
+ .attr("fill", "white")
290
+ .attr("fill-opacity", 0.7)
291
+ .attr("stroke", "#800000")
292
+ .attr("stroke-width", 4)
293
+ .attr("stroke-opacity", 1);
294
+ });
295
+ const simulation = d3.forceSimulation(filteredNodes)
296
+ .alphaDecay(0.05)
297
+ .alphaMin(0.001)
298
+ .force("link", d3.forceLink(filteredLinks).id(d => d.id).distance(d => d.distance))
299
+ .force("charge", d3.forceManyBody().strength(CHARGE_STRENGTH))
300
+ .force("center", d3.forceCenter(width / 2 -300, height / 2 - 310)) // Adjusted vertical center
301
+ .force("collide", d3.forceCollide().radius(d =>
302
+ COLLIDE_BASE_RADIUS + d.normSize * NODE_RADIUS_SCALE + (FONT_SIZE_BASE + d.normSize * FONT_SIZE_SCALE) * 0.1
303
+ ).strength(COLLIDE_STRENGTH))
304
+ .on("tick", ticked);
305
+ function ticked() {
306
+ link.attr("x1", d => d.source.x).attr("y1", d => d.source.y)
307
+ .attr("x2", d => d.target.x).attr("y2", d => d.target.y);
308
+ node.attr("cx", d => d.x).attr("cy", d => d.y);
309
+ labels.attr("transform", d => `translate(${d.x}, ${d.y})`);
310
+ insideLabels.attr("x", d => d.x).attr("y", d => d.y);
311
+ }
312
+
313
+ function dragstarted(event, d) {
314
+ if (!event.active) simulation.alphaTarget(0.3).restart();
315
+ d.fx = d.x; d.fy = d.y;
316
+ }
317
+
318
+ function dragged(event, d) {
319
+ d.fx = event.x; d.fy = event.y;
320
+ }
321
+
322
+ function dragended(event, d) {
323
+ if (!event.active) simulation.alphaTarget(0);
324
+ }
325
+ }
326
+
327
+ // Rest of your existing JavaScript for controls, theme toggle, etc.
328
+ const sliders = {
329
+ nodeCount: document.getElementById("nodeCountInput"),
330
+ linkScale: document.getElementById("linkScaleInput"),
331
+ charge: document.getElementById("chargeInput"),
332
+ collide: document.getElementById("collideInput"),
333
+ collideBase: document.getElementById("collideBaseInput"),
334
+ radius: document.getElementById("radiusInput"),
335
+ font: document.getElementById("fontInput"),
336
+ linkThickness: document.getElementById("LinkStrokeIn"),
337
+ };
338
+
339
+ const outputs = {
340
+ nodeCount: document.getElementById("nodeCountOutput"),
341
+ linkScale: document.getElementById("linkScaleOutput"),
342
+ charge: document.getElementById("chargeOutput"),
343
+ collide: document.getElementById("collideOutput"),
344
+ collideBase: document.getElementById("collideBaseOutput"),
345
+ radius: document.getElementById("radiusOutput"),
346
+ font: document.getElementById("fontOutput"),
347
+ linkThickness: document.getElementById("LinkStrokeOut"),
348
+ };
349
+
350
+ function updateAndRender() {
351
+ LINK_DISTANCE_SCALE = +sliders.linkScale.value;
352
+ CHARGE_STRENGTH = +sliders.charge.value;
353
+ COLLIDE_STRENGTH = +sliders.collide.value;
354
+ COLLIDE_BASE_RADIUS = +sliders.collideBase.value;
355
+ NODE_RADIUS_SCALE = +sliders.radius.value;
356
+ FONT_SIZE_SCALE = +sliders.font.value;
357
+ LINK_THICKNESS_BASE = +sliders.linkThickness.value;
358
+
359
+ outputs.linkScale.textContent = LINK_DISTANCE_SCALE;
360
+ outputs.charge.textContent = CHARGE_STRENGTH;
361
+ outputs.collide.textContent = COLLIDE_STRENGTH;
362
+ outputs.collideBase.textContent = COLLIDE_BASE_RADIUS;
363
+ outputs.radius.textContent = NODE_RADIUS_SCALE;
364
+ outputs.font.textContent = FONT_SIZE_SCALE;
365
+ outputs.linkThickness.textContent = LINK_THICKNESS_BASE;
366
+ outputs.nodeCount.textContent = sliders.nodeCount.value;
367
+
368
+ if (fullGraph) renderGraph(fullGraph, +sliders.nodeCount.value);
369
+ }
370
+
371
+ for (const key in sliders) {
372
+ sliders[key].addEventListener("input", updateAndRender);
373
+ }
374
+
375
+ document.getElementById("resetBtn").addEventListener("click", () => {
376
+ sliders.nodeCount.value = 50;
377
+ sliders.linkScale.value = -200;
378
+ sliders.charge.value = 80;
379
+ sliders.collide.value = 2;
380
+ sliders.collideBase.value = 17;
381
+ sliders.radius.value = 30;
382
+ sliders.font.value = 25;
383
+ sliders.linkThickness.value = 5;
384
+ updateAndRender();
385
+ });
386
+
387
+ document.getElementById("jsonSelector").addEventListener("change", function() {
388
+ loadJSON(this.value);
389
+ });
390
+
391
+ function updateSvgThemeStyles() {
392
+ const fill = getComputedStyle(document.documentElement).getPropertyValue('--svg-fill').trim();
393
+ const textColor = getComputedStyle(document.documentElement).getPropertyValue('--text-color').trim();
394
+ const labelBg = getComputedStyle(document.documentElement).getPropertyValue('--label-bg').trim();
395
+
396
+ zoomLayer.select("rect").attr("fill", fill);
397
+ zoomLayer.selectAll("text").attr("fill", textColor);
398
+ zoomLayer.selectAll("g.label rect").attr("fill", labelBg);
399
+ }
400
+
401
+ const toggle = document.getElementById('modeToggle');
402
+ const root = document.documentElement;
403
+
404
+ root.classList.remove('light-mode');
405
+
406
+ toggle.addEventListener('change', () => {
407
+ root.classList.toggle('light-mode');
408
+ setTimeout(updateSvgThemeStyles, 50);
409
+ });
410
+
411
+ function loadJSON(filePath) {
412
+ d3.json(filePath).then(graph => {
413
+ fullGraph = graph;
414
+ updateAndRender();
415
+ });
416
+ }
417
+
418
+ const selector = document.getElementById("jsonSelector");
419
+ loadJSON(selector.value);
420
+
421
+ document.getElementById("downloadBtn").addEventListener("click", () => {
422
+ const svgNode = document.querySelector("svg");
423
+ const serializer = new XMLSerializer();
424
+ const source = serializer.serializeToString(svgNode);
425
+ const blob = new Blob([source], {type: "image/svg+xml;charset=utf-8"});
426
+ const url = URL.createObjectURL(blob);
427
+
428
+ const link = document.createElement("a");
429
+ link.href = url;
430
+ link.download = "graph-export.svg";
431
+ document.body.appendChild(link);
432
+ link.click();
433
+ document.body.removeChild(link);
434
+ URL.revokeObjectURL(url);
435
+ });
436
+
437
+ // Initial render
438
+ updateSvgThemeStyles();
439
+ updateAndRender();
440
+ </script>
441
+ </body>
442
+ </html>
public/img/booru_tags.svg ADDED
public/img/japan.svg ADDED
plots/grid10x120.png → public/img/sankey.png RENAMED
File without changes
public/img/tag_clusters.png ADDED

Git LFS Details

  • SHA256: ffc4621ae3165a9da47acdd829cd6575417bc7bd04c34bf139a2952b76225a33
  • Pointer size: 130 Bytes
  • Size of remote file: 69.6 kB
public/img/training_tags.png ADDED

Git LFS Details

  • SHA256: 990596e72176330a0af620e4ad47399ab68592f16fe5a9698b9a1dddfe15dbe7
  • Pointer size: 132 Bytes
  • Size of remote file: 1.56 MB
public/index.html ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <script src="https://d3js.org/d3.v7.min.js"></script>
6
+ <link rel="stylesheet" href="styles.css">
7
+ <style>
8
+ .node { stroke: none; }
9
+ text { font-family: Arial, sans-serif; fill: black; }
10
+ .legend rect { stroke: black; stroke-width: 0.5px; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <h1>Graph Visualizations</h1>
15
+ <h2 class="caption">Interactive networks and tag structures derived from user-generated content on CivitAI</h2>
16
+ <div class="description">
17
+ <p>
18
+ These interactive visualizations are part of the study <em>"Perpetual Misogyny: How Gendered Tropes Shape Text-To-Image AI Personalization"</em>. The research explores how user-driven content and tagging behavior on the model-sharing hub <strong>CivitAI</strong> has evolved over a two-year span.
19
+ </p>
20
+ <p>
21
+ The first network visualizes tag co-occurrence patterns—how users promote their models by grouping descriptive tags together. The second visualization maps semantic structures in the training data of over 11,000 LoRA models, revealing latent biases and conceptual clusters.
22
+ </p>
23
+ </div>
24
+
25
+ <div class="theme-toggle" style="text-align: center; margin-bottom: 2rem;">
26
+ <label>
27
+ <input type="checkbox" id="modeToggle"> Toggle Light Mode
28
+ </label>
29
+ </div>
30
+ <div class="card-grid">
31
+ <a class="card-link" href="figure_15.html">
32
+ <div class="preview-card">
33
+ <img src="img/booru_tags.svg" alt="Preview 1">
34
+ <div class="card-title">Training Tags Network</div>
35
+ <div class="card-footer">View Graph →</div>
36
+ </div>
37
+ </a>
38
+
39
+ <a class="card-link" href="figure_5.html">
40
+ <div class="preview-card">
41
+ <img src="img/japan.svg" alt="Preview 3">
42
+ <div class="card-title">Tag Groups</div>
43
+ <div class="card-footer">Explore Tags →</div>
44
+ </div>
45
+ </a>
46
+ </div>
47
+
48
+ </div>
49
+ <script>
50
+ const toggle = document.getElementById('modeToggle');
51
+ const root = document.documentElement;
52
+
53
+ // Default to dark mode
54
+ root.classList.remove('light-mode');
55
+
56
+ toggle.addEventListener('change', () => {
57
+ root.classList.toggle('light-mode');
58
+ });
59
+ </script>
60
+
61
+ </body>
62
+ </html>