Ron Au commited on
Commit
79743a3
1 Parent(s): 0483b0c

refactor: V3

Browse files

- refactor(inference): Move operations to notebook
- feat(backend): Remove queuing system
- feat(backend): Add rate-limited card pulling
- feat(endpoints): Merge details and image reqs
- perf(images): Save to JPG instead of PNG
- perf(images): Batch generate images
- refactor(logging): Improve details and order
- refactor(rand_attack): Improve readability
- refactor(rand_type): Rename to rand_energy
- refactor(energy types): Use consistent lowercasing
- refactor(energy types): Order alphabetically
- feat(ui): Remove ETA display
- fix(animation): Fix card showing below booster
- feat(input): Add submit button

.gitattributes CHANGED
@@ -25,3 +25,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
25
  *.zip filter=lfs diff=lfs merge=lfs -text
26
  *.zstandard filter=lfs diff=lfs merge=lfs -text
27
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
25
  *.zip filter=lfs diff=lfs merge=lfs -text
26
  *.zstandard filter=lfs diff=lfs merge=lfs -text
27
  *tfevents* filter=lfs diff=lfs merge=lfs -text
28
+ datasets/pregenerated_pokemon.h5 filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -1,131 +1,43 @@
1
- from time import time
2
- from statistics import mean
3
 
4
- from fastapi import BackgroundTasks, FastAPI
5
  from fastapi.staticfiles import StaticFiles
6
  from fastapi.responses import FileResponse
7
- from pydantic import BaseModel
8
 
9
- from modules.details import rand_details
10
- from modules.inference import generate_image
11
 
12
  app = FastAPI(docs_url=None, redoc_url=None)
13
 
14
  app.mount("/static", StaticFiles(directory="static"), name="static")
15
 
16
- tasks = {}
17
-
18
-
19
- class NewTask(BaseModel):
20
- prompt = "покемон"
21
-
22
-
23
- def get_place_in_queue(task_id):
24
- queued_tasks = list(task for task in tasks.values()
25
- if task["status"] == "queued" or task["status"] == "processing")
26
-
27
- queued_tasks.sort(key=lambda task: task["created_at"])
28
-
29
- queued_task_ids = list(task["task_id"] for task in queued_tasks)
30
-
31
- try:
32
- return queued_task_ids.index(task_id) + 1
33
- except:
34
- return 0
35
-
36
-
37
- def calculate_eta(task_id):
38
- total_durations = list(task["completed_at"] - task["started_at"]
39
- for task in tasks.values() if "completed_at" in task and task["status"] == "completed")
40
-
41
- initial_place_in_queue = tasks[task_id]["initial_place_in_queue"]
42
-
43
- if len(total_durations):
44
- eta = initial_place_in_queue * mean(total_durations)
45
- else:
46
- eta = initial_place_in_queue * 35
47
-
48
- return round(eta, 1)
49
-
50
-
51
- def next_task(task_id):
52
- tasks[task_id]["completed_at"] = time()
53
-
54
- queued_tasks = list(task for task in tasks.values() if task["status"] == "queued")
55
-
56
- if queued_tasks:
57
- print(f"{task_id} {tasks[task_id]['status']}. Task/s remaining: {len(queued_tasks)}")
58
- process_task(queued_tasks[0]["task_id"])
59
-
60
-
61
- def process_task(task_id):
62
- if 'processing' in list(task['status'] for task in tasks.values()):
63
- return
64
-
65
- if tasks[task_id]["last_poll"] and time() - tasks[task_id]["last_poll"] > 30:
66
- tasks[task_id]["status"] = "abandoned"
67
- next_task(task_id)
68
-
69
- tasks[task_id]["status"] = "processing"
70
- tasks[task_id]["started_at"] = time()
71
- print(f"Processing {task_id}")
72
-
73
- try:
74
- tasks[task_id]["value"] = generate_image(tasks[task_id]["prompt"])
75
- except Exception as ex:
76
- tasks[task_id]["status"] = "failed"
77
- tasks[task_id]["error"] = repr(ex)
78
- else:
79
- tasks[task_id]["status"] = "completed"
80
- finally:
81
- next_task(task_id)
82
 
83
 
84
  @app.head('/')
85
  @app.get('/')
86
- def index():
87
  return FileResponse(path="static/index.html", media_type="text/html")
88
 
89
 
90
- @app.get('/details')
91
- def generate_details():
92
- return rand_details()
93
-
94
 
95
- @app.post('/task/create')
96
- def create_task(background_tasks: BackgroundTasks, new_task: NewTask):
97
- created_at = time()
98
 
99
- task_id = f"{str(created_at)}_{new_task.prompt}"
100
-
101
- tasks[task_id] = {
102
- "task_id": task_id,
103
- "status": "queued",
104
- "eta": None,
105
- "created_at": created_at,
106
- "started_at": None,
107
- "completed_at": None,
108
- "last_poll": None,
109
- "poll_count": 0,
110
- "initial_place_in_queue": None,
111
- "place_in_queue": None,
112
- "prompt": new_task.prompt,
113
- "value": None,
114
  }
115
 
116
- tasks[task_id]["initial_place_in_queue"] = get_place_in_queue(task_id)
117
- tasks[task_id]["eta"] = calculate_eta(task_id)
118
-
119
- background_tasks.add_task(process_task, task_id)
120
-
121
- return tasks[task_id]
122
 
 
 
 
123
 
124
- @app.get('/task/poll')
125
- def poll_task(task_id: str):
126
- tasks[task_id]["place_in_queue"] = get_place_in_queue(task_id)
127
- tasks[task_id]["eta"] = calculate_eta(task_id)
128
- tasks[task_id]["last_poll"] = time()
129
- tasks[task_id]["poll_count"] += 1
130
 
131
- return tasks[task_id]
 
 
 
1
+ from typing import Union
2
+ from time import gmtime, strftime
3
 
4
+ from fastapi import FastAPI
5
  from fastapi.staticfiles import StaticFiles
6
  from fastapi.responses import FileResponse
 
7
 
8
+ from modules.details import Details, rand_details
9
+ from modules.dataset import get_image, get_stats
10
 
11
  app = FastAPI(docs_url=None, redoc_url=None)
12
 
13
  app.mount("/static", StaticFiles(directory="static"), name="static")
14
 
15
+ card_logs = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
 
18
  @app.head('/')
19
  @app.get('/')
20
+ def index() -> FileResponse:
21
  return FileResponse(path="static/index.html", media_type="text/html")
22
 
23
 
24
+ @app.get('/new_card')
25
+ def new_card() -> dict[str, Union[Details, str]]:
26
+ card_logs.append(strftime('%Y-%m-%dT%H:%M:%SZ', gmtime()))
 
27
 
28
+ details: Details = rand_details()
 
 
29
 
30
+ return {
31
+ "details": details,
32
+ "image": get_image(details["energy_type"]),
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
 
 
 
 
 
 
 
35
 
36
+ @app.get('/stats')
37
+ def stats() -> dict[str, Union[int, object]]:
38
+ return get_stats() | {"cards_served": len(card_logs)}
39
 
 
 
 
 
 
 
40
 
41
+ @app.get('/logs')
42
+ def logs() -> list[str]:
43
+ return card_logs
datasets/pregenerated_pokemon.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:69bfb8a58317d91df48c385fb8051805ae7fa75cbe116ffab02c74231fb86e5c
3
+ size 412480704
lists/names/{Colorless.json → colorless.json} RENAMED
File without changes
lists/names/{Darkness.json → darkness.json} RENAMED
File without changes
lists/names/{Dragon.json → dragon.json} RENAMED
File without changes
lists/names/{Fairy.json → fairy.json} RENAMED
File without changes
lists/names/{Fighting.json → fighting.json} RENAMED
File without changes
lists/names/{Fire.json → fire.json} RENAMED
File without changes
lists/names/{Grass.json → grass.json} RENAMED
File without changes
lists/names/{Lightning.json → lightning.json} RENAMED
File without changes
lists/names/{Metal.json → metal.json} RENAMED
File without changes
lists/names/{Psychic.json → psychic.json} RENAMED
File without changes
lists/names/{Water.json → water.json} RENAMED
File without changes
modules/dataset.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from random import choices, randint
3
+ from typing import cast, Optional, TypedDict
4
+ import h5py
5
+
6
+
7
+ datasets_dir: str = './datasets'
8
+ datasets_file: str = 'pregenerated_pokemon.h5'
9
+ h5_file: str = os.path.join(datasets_dir, datasets_file)
10
+
11
+
12
+ class Stats(TypedDict):
13
+ size_total: int
14
+ size_mb: float
15
+ size_counts: dict[str, int]
16
+
17
+
18
+ def get_stats(h5_file: str = h5_file) -> Stats:
19
+ with h5py.File(h5_file, 'r') as datasets:
20
+ return {
21
+ "size_total": sum(list(datasets[energy].size.item() for energy in datasets.keys())),
22
+ "size_mb": round(os.path.getsize(h5_file) / 1024**2, 1),
23
+ "size_counts": {key: datasets[key].size.item() for key in datasets.keys()},
24
+ }
25
+
26
+
27
+ energy_types: list[str] = ['colorless', 'darkness', 'dragon', 'fairy', 'fighting',
28
+ 'fire', 'grass', 'lightning', 'metal', 'psychic', 'water']
29
+
30
+
31
+ def get_image(energy: Optional[str] = None, row: Optional[int] = None) -> str:
32
+ if not energy:
33
+ energy = choices(energy_types)[0]
34
+
35
+ with h5py.File(h5_file, 'r') as datasets:
36
+ if not row:
37
+ row = randint(0, datasets[energy].size - 1)
38
+
39
+ return datasets[energy].asstr()[row][0]
modules/details.py CHANGED
@@ -1,8 +1,20 @@
1
  import random
2
  import json
 
3
 
4
 
5
- def load_lists(list_names, base_dir="lists"):
 
 
 
 
 
 
 
 
 
 
 
6
  lists = {}
7
 
8
  for name in list_names:
@@ -12,45 +24,47 @@ def load_lists(list_names, base_dir="lists"):
12
  return lists
13
 
14
 
15
- def rand_hp():
16
  # Weights from https://bulbapedia.bulbagarden.net/wiki/HP_(TCG)
17
 
18
- hp_range = list(range(30, 340 + 1, 10))
19
 
20
- weights = [156, 542, 1264, 1727, 1477, 1232, 1008, 640, 436, 515, 469, 279, 188,
21
- 131, 132, 132, 56, 66, 97, 74, 23, 24, 25, 7, 15, 6, 0, 12, 18, 35, 18, 3]
22
 
23
  return random.choices(hp_range, weights)[0]
24
 
25
 
26
- def rand_type(types=['Grass', 'Fire', 'Water', 'Lightning', 'Fighting',
27
- 'Psychic', 'Colorless', 'Darkness', 'Metal', 'Dragon', 'Fairy'], can_be_none=False):
 
 
28
  if can_be_none:
29
  return random.choices([random.choices(types)[0], None])[0]
30
  else:
31
  return random.choices(types)[0]
32
 
33
 
34
- def rand_name(energy_type=rand_type()):
35
- lists = load_lists([energy_type], 'lists/names')
36
 
37
- return random.choices(lists[energy_type])[0]
38
 
39
 
40
- def rand_species(species):
41
- random_species = random.choices(species)[0]
42
 
43
  return f'{random_species.capitalize()}'
44
 
45
 
46
- def rand_length():
47
  # Weights from https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_height
48
 
49
- feet_ranges = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '16',
50
- '17', '18', '19', '20', '21', '22', '23', '24', '26', '28', '30', '32', '34', '35', '47', '65', '328']
51
 
52
- weights = [30, 220, 230, 176, 130, 109, 63, 27, 17, 17, 5, 5, 6,
53
- 4, 3, 2, 2, 2, 1, 2, 3, 1, 2, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1]
54
 
55
  return {
56
  "feet": random.choices(feet_ranges, weights)[0],
@@ -58,45 +72,65 @@ def rand_length():
58
  }
59
 
60
 
61
- def rand_weight():
62
  # Weights from https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_weight
63
 
64
- weight_ranges = [{"start": 1, "end": 22}, {"start": 22, "end": 44}, {"start": 44, "end": 55}, {"start": 55, "end": 110}, {"start": 110, "end": 132}, {"start": 132, "end": 218}, {"start": 218, "end": 220}, {"start": 221, "end": 226}, {
65
- "start": 226, "end": 331}, {"start": 331, "end": 441}, {"start": 441, "end": 451}, {"start": 452, "end": 661}, {"start": 661, "end": 677}, {"start": 677, "end": 793}, {"start": 794, "end": 903}, {"start": 903, "end": 2204}]
66
-
67
- # 'weights' as in statistical weightings
68
- weights = [271, 145, 53, 204, 57, 122, 1, 11, 57, 28, 7, 34, 4, 17, 5, 31]
69
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  start, end = random.choices(weight_ranges, weights)[0].values()
71
 
72
- random_weight = random.randrange(start, end + 1, 1)
73
 
74
  return f'{random_weight} lbs.'
75
 
76
 
77
- def rand_attack(attacks, name, energy_type=None, colorless_only_allowed=False):
78
- random_attack = random.choices(attacks)[0]
 
 
 
79
 
80
- # There are no attacks in Pokémon TCG that have Dragon energy costs
81
- # so this would loop indefinitely if looking for one
82
 
83
- if energy_type is not None and energy_type != 'Dragon' and not colorless_only_allowed:
84
- while energy_type not in random_attack["cost"]:
85
- random_attack = random.choices(attacks)[0]
86
- elif energy_type is not None and energy_type != 'Dragon' and colorless_only_allowed:
87
- while energy_type not in random_attack["cost"] and 'Colorless' not in random_attack["cost"]:
88
- random_attack = random.choices(attacks)[0]
 
89
 
90
  random_attack['text'] = random_attack['text'].replace('<name>', name)
91
 
92
  return random_attack
93
 
94
 
95
- def rand_attacks(attacks, name, energy_type=None, n=2):
96
- attack1 = rand_attack(attacks, name, energy_type)
97
 
98
  if n > 1:
99
- attack2 = rand_attack(attacks, name, energy_type, True)
100
  while attack1['text'] == attack2['text']:
101
  attack2 = rand_attack(attacks, name, energy_type, True)
102
  return [attack1, attack2]
@@ -104,32 +138,50 @@ def rand_attacks(attacks, name, energy_type=None, n=2):
104
  return [attack1]
105
 
106
 
107
- def rand_retreat():
108
  return random.randrange(0, 4, 1)
109
 
110
 
111
- def rand_description(descriptions):
112
  return random.choices(descriptions)[0]
113
 
114
 
115
- def rand_rarity():
116
  return random.choices(['●', '◆', '★'], [10, 5, 1])[0]
117
 
118
 
119
- def rand_details(lists=load_lists(['attacks', 'descriptions', 'species'])):
120
- energy_type = rand_type()
121
- name = rand_name(energy_type)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  return {
124
  "name": name,
125
  "hp": rand_hp(),
126
  "energy_type": energy_type,
127
- "species": rand_species(lists["species"]),
128
  "length": rand_length(),
129
  "weight": rand_weight(),
130
- "attacks": rand_attacks(lists["attacks"], name, energy_type=energy_type),
131
- "weakness": rand_type(can_be_none=True),
132
- "resistance": rand_type(can_be_none=True),
133
  "retreat": rand_retreat(),
134
  "description": rand_description(lists["descriptions"]),
135
  "rarity": rand_rarity(),
 
1
  import random
2
  import json
3
+ from typing import cast, Optional, TypedDict, Union
4
 
5
 
6
+ class Attack(TypedDict):
7
+ name: str
8
+ cost: list[str]
9
+ convertedEnergyCost: int
10
+ damage: str
11
+ text: str
12
+
13
+
14
+ ListCollection = dict[str, Union[list[str], list[Attack]]]
15
+
16
+
17
+ def load_lists(list_names: list[str], base_dir: str = "lists") -> ListCollection:
18
  lists = {}
19
 
20
  for name in list_names:
 
24
  return lists
25
 
26
 
27
+ def rand_hp() -> int:
28
  # Weights from https://bulbapedia.bulbagarden.net/wiki/HP_(TCG)
29
 
30
+ hp_range: list[int] = list(range(30, 340 + 1, 10))
31
 
32
+ weights: list[int] = [156, 542, 1264, 1727, 1477, 1232, 1008, 640, 436, 515, 469, 279, 188,
33
+ 131, 132, 132, 56, 66, 97, 74, 23, 24, 25, 7, 15, 6, 0, 12, 18, 35, 18, 3]
34
 
35
  return random.choices(hp_range, weights)[0]
36
 
37
 
38
+ def rand_energy(can_be_none: bool = False) -> Union[str, None]:
39
+ types: list[str] = ['colorless', 'darkness', 'dragon', 'fairy', 'fighting',
40
+ 'fire', 'grass', 'lightning', 'metal', 'psychic', 'water']
41
+
42
  if can_be_none:
43
  return random.choices([random.choices(types)[0], None])[0]
44
  else:
45
  return random.choices(types)[0]
46
 
47
 
48
+ def rand_name(energy_type: str = cast(str, rand_energy())) -> str:
49
+ lists: ListCollection = load_lists([energy_type], 'lists/names')
50
 
51
+ return cast(str, random.choices(lists[energy_type])[0])
52
 
53
 
54
+ def rand_species(species: list[str]) -> str:
55
+ random_species: str = random.choices(species)[0]
56
 
57
  return f'{random_species.capitalize()}'
58
 
59
 
60
+ def rand_length() -> dict[str, int]:
61
  # Weights from https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_height
62
 
63
+ feet_ranges: list[int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16,
64
+ 17, 18, 19, 20, 21, 22, 23, 24, 26, 28, 30, 32, 34, 35, 47, 65, 328]
65
 
66
+ weights: list[int] = [30, 220, 230, 176, 130, 109, 63, 27, 17, 17, 5, 5, 6,
67
+ 4, 3, 2, 2, 2, 1, 2, 3, 1, 2, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1]
68
 
69
  return {
70
  "feet": random.choices(feet_ranges, weights)[0],
 
72
  }
73
 
74
 
75
+ def rand_weight() -> str:
76
  # Weights from https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_weight
77
 
78
+ weight_ranges: list[dict[str, int]] = [
79
+ {"start": 1, "end": 22},
80
+ {"start": 22, "end": 44},
81
+ {"start": 44, "end": 55},
82
+ {"start": 55, "end": 110},
83
+ {"start": 110, "end": 132},
84
+ {"start": 132, "end": 218},
85
+ {"start": 218, "end": 220},
86
+ {"start": 221, "end": 226},
87
+ {"start": 226, "end": 331},
88
+ {"start": 331, "end": 441},
89
+ {"start": 441, "end": 451},
90
+ {"start": 452, "end": 661},
91
+ {"start": 661, "end": 677},
92
+ {"start": 677, "end": 793},
93
+ {"start": 794, "end": 903},
94
+ {"start": 903, "end": 2204}]
95
+
96
+ # 'weights' as in statistical weightings, not physical mass
97
+ weights: list[int] = [271, 145, 53, 204, 57, 122, 1, 11, 57, 28, 7, 34, 4, 17, 5, 31]
98
+
99
+ start: int
100
+ end: int
101
  start, end = random.choices(weight_ranges, weights)[0].values()
102
 
103
+ random_weight: int = random.randrange(start, end + 1, 1)
104
 
105
  return f'{random_weight} lbs.'
106
 
107
 
108
+ def rand_attack(
109
+ attacks: list[Attack],
110
+ name: str, energy_type: Optional[str],
111
+ colorless_only_allowed: bool = False) -> Attack:
112
+ random_attack: Attack = random.choices(attacks)[0]
113
 
114
+ energy_type = energy_type.capitalize() if energy_type else None # Energy is capitalised in the JSON lists
 
115
 
116
+ if energy_type and energy_type != 'Dragon': # No attacks use Dragon energy so this would otherwise infinitely loop
117
+ if colorless_only_allowed:
118
+ while energy_type not in random_attack["cost"] and 'colorless' not in random_attack["cost"]:
119
+ random_attack = random.choices(attacks)[0]
120
+ else:
121
+ while energy_type not in random_attack["cost"]:
122
+ random_attack = random.choices(attacks)[0]
123
 
124
  random_attack['text'] = random_attack['text'].replace('<name>', name)
125
 
126
  return random_attack
127
 
128
 
129
+ def rand_attacks(attacks: list[Attack], name: str, energy_type: Optional[str], n: int = 2) -> list[Attack]:
130
+ attack1: Attack = rand_attack(attacks, name, energy_type)
131
 
132
  if n > 1:
133
+ attack2: Attack = rand_attack(attacks, name, energy_type, True)
134
  while attack1['text'] == attack2['text']:
135
  attack2 = rand_attack(attacks, name, energy_type, True)
136
  return [attack1, attack2]
 
138
  return [attack1]
139
 
140
 
141
+ def rand_retreat() -> int:
142
  return random.randrange(0, 4, 1)
143
 
144
 
145
+ def rand_description(descriptions) -> str:
146
  return random.choices(descriptions)[0]
147
 
148
 
149
+ def rand_rarity() -> str:
150
  return random.choices(['●', '◆', '★'], [10, 5, 1])[0]
151
 
152
 
153
+ lists: ListCollection = load_lists(['attacks', 'descriptions', 'species'])
154
+
155
+
156
+ class Details(TypedDict):
157
+ name: str
158
+ hp: int
159
+ energy_type: str
160
+ species: str
161
+ length: dict[str, int]
162
+ weight: str
163
+ attacks: list[Attack]
164
+ weakness: Union[str, None]
165
+ resistance: Union[str, None]
166
+ retreat: int
167
+ description: str
168
+ rarity: str
169
+
170
+
171
+ def rand_details() -> Details:
172
+ energy_type: str = cast(str, rand_energy())
173
+ name: str = rand_name(energy_type)
174
 
175
  return {
176
  "name": name,
177
  "hp": rand_hp(),
178
  "energy_type": energy_type,
179
+ "species": rand_species(cast(list[str], lists["species"])),
180
  "length": rand_length(),
181
  "weight": rand_weight(),
182
+ "attacks": cast(list[Attack], rand_attacks(cast(list[Attack], lists["attacks"]), name, energy_type=energy_type)),
183
+ "weakness": rand_energy(can_be_none=True),
184
+ "resistance": rand_energy(can_be_none=True),
185
  "retreat": rand_retreat(),
186
  "description": rand_description(lists["descriptions"]),
187
  "rarity": rand_rarity(),
modules/inference.py DELETED
@@ -1,64 +0,0 @@
1
- from time import gmtime, strftime
2
-
3
- print(f'{strftime("%Y-%m-%d %H:%M:%S", gmtime())} Preparing for inference...') # noqa
4
-
5
- from rudalle.pipelines import generate_images
6
- from rudalle import get_rudalle_model, get_tokenizer, get_vae
7
- from huggingface_hub import hf_hub_url, cached_download
8
- import torch
9
- from io import BytesIO
10
- import base64
11
-
12
- print(f"GPUs available: {torch.cuda.device_count()}")
13
- print(f"GPU[0] memory: {int(torch.cuda.get_device_properties(0).total_memory / 1048576)}Mib")
14
- print(f"GPU[0] memory reserved: {int(torch.cuda.memory_reserved(0) / 1048576)}Mib")
15
- print(f"GPU[0] memory allocated: {int(torch.cuda.memory_allocated(0) / 1048576)}Mib")
16
-
17
- device = "cuda" if torch.cuda.is_available() else "cpu"
18
- fp16 = torch.cuda.is_available()
19
-
20
- file_dir = "./models"
21
- file_name = "pytorch_model.bin"
22
- config_file_url = hf_hub_url(repo_id="minimaxir/ai-generated-pokemon-rudalle", filename=file_name)
23
- cached_download(config_file_url, cache_dir=file_dir, force_filename=file_name)
24
-
25
- model = get_rudalle_model('Malevich', pretrained=False, fp16=fp16, device=device)
26
- model.load_state_dict(torch.load(f"{file_dir}/{file_name}", map_location=f"{'cuda:0' if torch.cuda.is_available() else 'cpu'}"))
27
-
28
- vae = get_vae().to(device)
29
- tokenizer = get_tokenizer()
30
-
31
- print(f'{strftime("%Y-%m-%d %H:%M:%S", gmtime())} Ready for inference')
32
-
33
-
34
- def english_to_russian(english_string):
35
- word_map = {
36
- "grass": "Покемон трава",
37
- "fire": "Покемон огня",
38
- "water": "Покемон в воду",
39
- "lightning": "Покемон электрического типа",
40
- "fighting": "Покемон боевого типа",
41
- "psychic": "Покемон психического типа",
42
- "colorless": "Покемон нормального типа",
43
- "darkness": "Покемон темного типа",
44
- "metal": "Покемон из стали типа",
45
- "dragon": "Покемон типа дракона",
46
- "fairy": "Покемон фея"
47
- }
48
-
49
- return word_map[english_string.lower()]
50
-
51
-
52
- def generate_image(prompt):
53
- if prompt.lower() in ['grass', 'fire', 'water', 'lightning', 'fighting', 'psychic', 'colorless', 'darkness',
54
- 'metal', 'dragon', 'fairy']:
55
- prompt = english_to_russian(prompt)
56
-
57
- result, _ = generate_images(prompt, tokenizer, model, vae, top_k=2048, images_num=1, top_p=0.995)
58
-
59
- buffer = BytesIO()
60
- result[0].save(buffer, format="PNG")
61
- base64_bytes = base64.b64encode(buffer.getvalue())
62
- base64_string = base64_bytes.decode("UTF-8")
63
-
64
- return "data:image/png;base64," + base64_string
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
notebooks/populate_dataset.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
static/index.html CHANGED
@@ -27,14 +27,15 @@
27
  <img src="static/bluey.png" alt="AI generated creature" width="80" height="80">
28
  </div>
29
  <h1>This Pokémon<br />Does Not Exist</h1>
30
- <label>
31
- Enter your trainer name
32
- <form class="trainer-name" action="">
33
- <input name="name" type="text" placeholder="Ash" maxlength="75" />
34
- </form>
35
- </label>
 
36
  <p>
37
- Each illustration is <strong>generated with AI</strong> using a <a href="https://rudalle.ru/en/" target="_blank">ruDALL-E</a>
38
  model <a href="https://huggingface.co/minimaxir/ai-generated-pokemon-rudalle" target="_blank">fine-tuned by Max Woolf.</a> Over
39
  <a href="https://huggingface.co/models" target="_blank">30,000 such models</a> are hosted on Hugging Face for immediate use.</a
40
  >
@@ -47,10 +48,9 @@
47
  <button class="toggle-name" data-include tabindex="-1">Trainer Name</button>
48
  <button class="generate-new" tabindex="-1">New Pokémon</button>
49
  </div>
50
- <div class="duration"><span class="elapsed">0.0</span>s (ETA: <span class="eta">35</span>s)</div>
51
  </div>
52
  <div class="scene">
53
- <div class="booster">
54
  <div class="foil triangle top left"></div>
55
  <div class="foil triangle top right"></div>
56
  <div class="foil top flat"></div>
 
27
  <img src="static/bluey.png" alt="AI generated creature" width="80" height="80">
28
  </div>
29
  <h1>This Pokémon<br />Does Not Exist</h1>
30
+ <label for="name-input">Enter your trainer name</label>
31
+ <form class="name-form" action="">
32
+ <!-- <div class="name-interactive"> -->
33
+ <input id="name-input" name="name" type="text" placeholder="Ash" maxlength="75" />
34
+ <button type="submit">Submit</button>
35
+ <!-- </div> -->
36
+ </form>
37
  <p>
38
+ Each illustration is <strong>generated with AI</strong> using a <a href="https://rudalle.ru/en/" rel="noopener" target="_blank">ruDALL-E</a>
39
  model <a href="https://huggingface.co/minimaxir/ai-generated-pokemon-rudalle" target="_blank">fine-tuned by Max Woolf.</a> Over
40
  <a href="https://huggingface.co/models" target="_blank">30,000 such models</a> are hosted on Hugging Face for immediate use.</a
41
  >
 
48
  <button class="toggle-name" data-include tabindex="-1">Trainer Name</button>
49
  <button class="generate-new" tabindex="-1">New Pokémon</button>
50
  </div>
 
51
  </div>
52
  <div class="scene">
53
+ <div class="booster" title="Open booster pack for new card">
54
  <div class="foil triangle top left"></div>
55
  <div class="foil triangle top right"></div>
56
  <div class="foil top flat"></div>
static/js/card-html.js CHANGED
@@ -1,19 +1,19 @@
1
  const TYPES = {
2
- Grass: '🍃',
3
- Fire: '🔥',
4
- Water: '💧',
5
- Lightning: '',
6
- Fighting: '✊',
7
- Psychic: '👁️',
8
- Colorless: '',
9
- Darkness: '🌑',
10
- Metal: '⚙️',
11
- Dragon: '🐲',
12
- Fairy: '🧚',
13
  };
14
 
15
  const energyHTML = (type, types = TYPES) => {
16
- return `<span title="${type} energy" class="energy ${type.toLowerCase()}">${types[type]}</span>`;
17
  };
18
 
19
  const attackCostHTML = (cost) => {
@@ -78,7 +78,7 @@ export const cardHTML = (details) => {
78
  const poke_name = details.name; // `name` would be reserved JS word
79
 
80
  return `
81
- <div class="pokecard ${energy_type.toLowerCase()}" data-displayed="true">
82
  <p class="evolves">Basic Pokémon</p>
83
  <header>
84
  <h1 class="name">${poke_name}</h1>
@@ -122,5 +122,5 @@ export const cardHTML = (details) => {
122
  <span title="Rarity">${rarity}</span>
123
  </div>
124
  </div>
125
- </div>`;
126
  };
 
1
  const TYPES = {
2
+ colorless: '',
3
+ darkness: '🌑',
4
+ dragon: '🐲',
5
+ fairy: '🧚',
6
+ fighting: '✊',
7
+ fire: '🔥',
8
+ grass: '🍃',
9
+ lightning: '',
10
+ metal: '⚙️',
11
+ psychic: '👁️',
12
+ water: '💧',
13
  };
14
 
15
  const energyHTML = (type, types = TYPES) => {
16
+ return `<span title="${type} energy" class="energy ${type.toLowerCase()}">${types[type.toLowerCase()]}</span>`;
17
  };
18
 
19
  const attackCostHTML = (cost) => {
 
78
  const poke_name = details.name; // `name` would be reserved JS word
79
 
80
  return `
81
+ <div class="pokecard ${energy_type.toLowerCase()}">
82
  <p class="evolves">Basic Pokémon</p>
83
  <header>
84
  <h1 class="name">${poke_name}</h1>
 
122
  <span title="Rarity">${rarity}</span>
123
  </div>
124
  </div>
125
+ </div>`;
126
  };
static/js/dom-manipulation.js CHANGED
@@ -1,42 +1,5 @@
1
  import { toPng } from 'https://cdn.skypack.dev/html-to-image';
2
 
3
- const durationTimer = () => {
4
- const elapsedDisplay = document.querySelector('.elapsed');
5
- let duration = 0.0;
6
-
7
- return () => {
8
- const startTime = performance.now();
9
-
10
- const incrementSeconds = setInterval(() => {
11
- duration += 0.1;
12
- elapsedDisplay.textContent = duration.toFixed(1);
13
- }, 100);
14
-
15
- const updateDuration = (task) => {
16
- if (task?.status == 'completed') {
17
- duration = Date.now() / 1_000 - task.created_at;
18
- return;
19
- }
20
-
21
- duration = Number(((performance.now() - startTime) / 1_000).toFixed(1));
22
- };
23
-
24
- window.addEventListener('focus', updateDuration);
25
-
26
- return {
27
- cleanup: (completedTask) => {
28
- if (completedTask) {
29
- updateDuration(completedTask);
30
- }
31
-
32
- clearInterval(incrementSeconds);
33
- window.removeEventListener('focus', updateDuration);
34
- elapsedDisplay.textContent = duration.toFixed(1);
35
- },
36
- };
37
- };
38
- };
39
-
40
  const updateCardName = (trainerName, pokeName, useTrainerName) => {
41
  const cardName = document.querySelector('.pokecard .name');
42
 
@@ -127,4 +90,4 @@ const screenshotCard = async () => {
127
  return imageUrl;
128
  };
129
 
130
- export { durationTimer, updateCardName, initialiseCardRotation, setOutput, screenshotCard };
 
1
  import { toPng } from 'https://cdn.skypack.dev/html-to-image';
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  const updateCardName = (trainerName, pokeName, useTrainerName) => {
4
  const cardName = document.querySelector('.pokecard .name');
5
 
 
90
  return imageUrl;
91
  };
92
 
93
+ export { updateCardName, initialiseCardRotation, setOutput, screenshotCard };
static/js/index.js CHANGED
@@ -1,12 +1,5 @@
1
- import { generateDetails, createTask, longPollTask } from './network.js';
2
  import { cardHTML } from './card-html.js';
3
- import {
4
- durationTimer,
5
- updateCardName,
6
- initialiseCardRotation,
7
- setOutput,
8
- screenshotCard,
9
- } from './dom-manipulation.js';
10
 
11
  const nameInput = document.querySelector('input[name="name"');
12
  const nameToggle = document.querySelector('button.toggle-name');
@@ -25,60 +18,46 @@ const generate = async () => {
25
  const scene = document.querySelector('.scene');
26
  const cardSlot = scene.querySelector('.card-slot');
27
  const actions = document.querySelector('.actions');
28
- const durationDisplay = actions.querySelector('.duration');
29
- const etaDisplay = durationDisplay.querySelector('.eta');
30
- const timer = durationTimer(durationDisplay);
31
- const timerCleanup = timer().cleanup;
32
 
33
  scene.removeEventListener('mousemove', mousemoveHandlerForPreviousCard, true);
34
  cardSlot.innerHTML = '';
35
  generating = true;
 
36
  setOutput('booster', 'generating');
37
 
38
  try {
39
- const details = await generateDetails();
40
- pokeName = details.name;
41
- const task = await createTask(details.energy_type);
42
-
43
  actions.style.opacity = '1';
44
  actions.setAttribute('aria-hidden', 'false');
45
  actions.querySelectorAll('button').forEach((button) => button.setAttribute('tabindex', '0'));
46
- etaDisplay.textContent = Math.round(task.eta);
47
- durationDisplay.classList.add('displayed');
48
 
49
- if (window.innerWidth <= 500) {
50
- durationDisplay.scrollIntoView({ behavior: 'smooth' });
51
  }
52
 
53
- // Theoretically, keep client waiting only an extra half interval once task is complete
54
- const interval = 5_000;
55
- await new Promise((resolve) => setTimeout(resolve, task.eta * 1_000 - interval / 2));
56
- const completedTask = await longPollTask(task, interval);
57
 
58
- generating = false;
59
- timerCleanup(completedTask);
60
 
61
- if (completedTask.status === 'failed') {
62
- setOutput('booster', 'failed');
63
- throw new Error(`Task failed: ${completedTask.error}`);
64
- }
65
 
66
- setOutput('booster', 'completed');
67
-
68
- cardSlot.innerHTML = cardHTML(details);
69
- updateCardName(trainerName, pokeName, useTrainerName);
70
- document.querySelector('img.picture').src = completedTask.value;
71
 
72
- mousemoveHandlerForPreviousCard = initialiseCardRotation(scene);
73
 
74
  await new Promise((resolve) =>
75
  setTimeout(resolve, window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 1_500 : 1_000)
76
  );
77
 
 
 
 
 
 
 
78
  setOutput('card', 'completed');
79
  } catch (err) {
80
  generating = false;
81
- timerCleanup();
82
  setOutput('booster', 'failed');
83
  console.error(err);
84
  }
@@ -92,7 +71,7 @@ nameInput.addEventListener('input', (e) => {
92
  updateCardName(trainerName, pokeName, useTrainerName);
93
  });
94
 
95
- document.querySelector('form.trainer-name').addEventListener('submit', (e) => {
96
  e.preventDefault();
97
 
98
  if (document.querySelector('.output').dataset.state === 'completed') {
 
 
1
  import { cardHTML } from './card-html.js';
2
+ import { updateCardName, initialiseCardRotation, setOutput, screenshotCard } from './dom-manipulation.js';
 
 
 
 
 
 
3
 
4
  const nameInput = document.querySelector('input[name="name"');
5
  const nameToggle = document.querySelector('button.toggle-name');
 
18
  const scene = document.querySelector('.scene');
19
  const cardSlot = scene.querySelector('.card-slot');
20
  const actions = document.querySelector('.actions');
 
 
 
 
21
 
22
  scene.removeEventListener('mousemove', mousemoveHandlerForPreviousCard, true);
23
  cardSlot.innerHTML = '';
24
  generating = true;
25
+ document.querySelector('.scene .booster').removeAttribute('title');
26
  setOutput('booster', 'generating');
27
 
28
  try {
 
 
 
 
29
  actions.style.opacity = '1';
30
  actions.setAttribute('aria-hidden', 'false');
31
  actions.querySelectorAll('button').forEach((button) => button.setAttribute('tabindex', '0'));
 
 
32
 
33
+ if (window.innerWidth <= 920) {
34
+ scene.scrollIntoView({ behavior: 'smooth', block: 'center' });
35
  }
36
 
37
+ await new Promise((resolve) => setTimeout(resolve, 5_000));
 
 
 
38
 
39
+ const cardResponse = await fetch('/new_card');
40
+ const card = await cardResponse.json();
41
 
42
+ pokeName = card.details.name;
 
 
 
43
 
44
+ generating = false;
 
 
 
 
45
 
46
+ setOutput('booster', 'completed');
47
 
48
  await new Promise((resolve) =>
49
  setTimeout(resolve, window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 1_500 : 1_000)
50
  );
51
 
52
+ cardSlot.innerHTML = cardHTML(card.details);
53
+ updateCardName(trainerName, pokeName, useTrainerName);
54
+ document.querySelector('img.picture').src = card.image;
55
+
56
+ mousemoveHandlerForPreviousCard = initialiseCardRotation(scene);
57
+
58
  setOutput('card', 'completed');
59
  } catch (err) {
60
  generating = false;
 
61
  setOutput('booster', 'failed');
62
  console.error(err);
63
  }
 
71
  updateCardName(trainerName, pokeName, useTrainerName);
72
  });
73
 
74
+ document.querySelector('form.name-form').addEventListener('submit', (e) => {
75
  e.preventDefault();
76
 
77
  if (document.querySelector('.output').dataset.state === 'completed') {
static/js/network.js DELETED
@@ -1,59 +0,0 @@
1
- /**
2
- * Reconcile paths for hf.space resources fetched from hf.co iframe
3
- */
4
-
5
- const pathFor = (path) => {
6
- const basePath = document.location.origin + document.location.pathname;
7
- return new URL(path, basePath).href;
8
- };
9
-
10
- const generateDetails = async () => {
11
- const details = await fetch(pathFor('details'));
12
- return await details.json();
13
- };
14
-
15
- const createTask = async (prompt) => {
16
- const taskResponse = await fetch(pathFor('task/create'), {
17
- method: 'POST',
18
- headers: {
19
- 'Content-Type': 'application/json',
20
- },
21
- body: JSON.stringify({ prompt }),
22
- });
23
-
24
- if (!taskResponse.ok || !taskResponse.headers.get('content-type')?.includes('application/json')) {
25
- throw new Error(await taskResponse.text());
26
- }
27
-
28
- const task = await taskResponse.json();
29
-
30
- return task;
31
- };
32
-
33
- const pollTask = async (task) => {
34
- const taskResponse = await fetch(pathFor(`task/poll?task_id=${task.task_id}`));
35
-
36
- if (!taskResponse.ok || !taskResponse.headers.get('content-type')?.includes('application/json')) {
37
- throw new Error(await taskResponse.text());
38
- }
39
-
40
- return await taskResponse.json();
41
- };
42
-
43
- const longPollTask = async (task, interval = 5_000, max) => {
44
- const etaDisplay = document.querySelector('.eta');
45
-
46
- task = await pollTask(task);
47
-
48
- if (task.status === 'completed' || task.status === 'failed' || (max && task.poll_count > max)) {
49
- return task;
50
- }
51
-
52
- etaDisplay.textContent = Math.round(task.eta);
53
-
54
- await new Promise((resolve) => setTimeout(resolve, interval));
55
-
56
- return await longPollTask(task, interval, max);
57
- };
58
-
59
- export { generateDetails, createTask, longPollTask };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/style.css CHANGED
@@ -9,11 +9,6 @@
9
  --theme-error-border: hsl(355 85% 55%);
10
  }
11
 
12
- html,
13
- body {
14
- overflow: scroll;
15
- }
16
-
17
  * {
18
  transition: outline-offset 0.25s ease-out;
19
  outline-style: none;
@@ -36,11 +31,19 @@ body {
36
  background-color: gold;
37
  }
38
 
 
 
 
 
 
 
 
39
  body {
40
- height: 100vh;
41
  background-color: whitesmoke;
42
  background-image: linear-gradient(300deg, var(--theme-highlight), white);
43
  font-family: 'Gill Sans', 'Gill Sans Mt', 'sans-serif';
 
44
  }
45
 
46
  main {
@@ -49,7 +52,8 @@ main {
49
  grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
50
  gap: 1.5rem 0;
51
  max-width: 80rem;
52
- padding: 3rem;
 
53
  margin: 0 auto;
54
  }
55
 
@@ -69,7 +73,18 @@ main {
69
  .scene .card-slot {
70
  margin-top: 1rem;
71
  }
 
72
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
 
75
  @media (max-width: 1280px) {
@@ -148,19 +163,31 @@ section {
148
  font-weight: 700;
149
  }
150
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  .info input {
152
  display: block;
153
- width: 80%;
154
  box-sizing: border-box;
155
- padding: 0.5rem 1rem;
156
- margin: 0.5rem auto;
157
  border: 0.2rem solid hsl(0 0% 70%);
158
- border-radius: 1rem;
159
  text-align: center;
160
  font-size: 1.25rem;
161
- transition: border-color 0.25s ease-in;
162
- box-shadow: 0 0 0.25rem hsl(165.1 64.7% 48.8% / 60%);
163
- outline-color: white;
 
164
  }
165
 
166
  .info input::placeholder {
@@ -168,9 +195,14 @@ section {
168
  }
169
 
170
  input:focus {
171
- border-color: transparent;
172
- outline-style: solid;
173
- outline-color: var(--theme-primary);
 
 
 
 
 
174
  }
175
 
176
  .info p {
@@ -181,7 +213,8 @@ input:focus {
181
  line-height: 1.5rem;
182
  }
183
 
184
- .info a, info a:is(:hover, :focus, :active, :visited) {
 
185
  color: var(--theme-subtext);
186
  cursor: pointer;
187
  }
@@ -192,7 +225,7 @@ input:focus {
192
  display: flex;
193
  flex-direction: column;
194
  justify-content: space-around;
195
- height: max-content;
196
  }
197
 
198
  .output .actions {
@@ -218,13 +251,16 @@ button {
218
  font-weight: bold;
219
  color: white;
220
  transform-origin: bottom;
221
- transform: translateY(-25%);
222
  transition: transform 0.5s ease, box-shadow 0.1s, outline-offset 0.25s ease-out, filter 0.25s ease-out, opacity 0.25s;
223
- transition: transform 0.5s, opacity 0.5s;
224
  whitespace: nowrap;
225
- box-shadow: 0 0.2rem 0.375rem hsl(158 100% 33% / 60%);
226
  filter: saturate(1);
227
  cursor: pointer;
 
 
 
 
 
228
  user-select: none;
229
  pointer-events: none;
230
  opacity: 0;
@@ -245,49 +281,12 @@ button.toggle-name.off {
245
  filter: saturate(0.15);
246
  }
247
 
248
- .duration {
249
- visibility: hidden;
250
- width: max-content;
251
- padding: 0.35rem 1rem;
252
- border: 0.1rem solid hsl(90 100% 50% / 25%);
253
- border-radius: 1rem;
254
- background-color: var(--theme-highlight);
255
- text-align: right;
256
- color: var(--theme-subtext);
257
- transform: translateY(-25%);
258
- transition: transform 0.5s, opacity 0.5s;
259
- opacity: 0;
260
- }
261
-
262
- .duration.displayed {
263
- visibility: visible;
264
- transform: translateY(0);
265
- opacity: 1;
266
- }
267
-
268
- [data-state="failed"] .duration {
269
- border-color: var(--theme-error-border);
270
- background-color: var(--theme-error-bg);
271
- color: transparent;
272
- }
273
-
274
- [data-state="failed"] .duration.displayed::after {
275
- content: 'Try again';
276
- position: absolute;
277
- top: 20%;
278
- left: 0;
279
- width: 100%;
280
- text-align: center;
281
- color: white;
282
- }
283
-
284
  .scene {
285
  --scale: 0.9;
286
- height: 40rem;
287
  box-sizing: border-box;
288
- margin: 2rem;
289
  perspective: 100rem;
290
- transform-origin: center 40%;
291
  transform: scale(var(--scale));
292
  transition: transform 0.5s ease-out;
293
  }
@@ -584,11 +583,11 @@ img.hf-logo {
584
 
585
  @keyframes shrink {
586
  from {
587
- transform: rotateZ(-45deg) scale(var(--booster-scale));
588
  opacity: 1;
589
  }
590
  to {
591
- transform: rotateZ(-270deg) scale(0);
592
  opacity: 0;
593
  }
594
  }
@@ -616,6 +615,7 @@ img.hf-logo {
616
 
617
  [data-mode='booster'][data-state='completed'] .card-slot {
618
  transform: scale(0);
 
619
  }
620
 
621
  [data-mode='booster'][data-state='completed'] .back {
@@ -628,17 +628,26 @@ img.hf-logo {
628
 
629
  [data-mode='card'][data-state='completed'] .card-slot {
630
  transform: scale(1);
 
631
  }
632
 
633
  @media (prefers-reduced-motion) {
634
  @keyframes pulse {
635
- from { opacity: 1; }
636
- to { opacity: 0.6; }
 
 
 
 
637
  }
638
 
639
  @keyframes fade {
640
- from { opacity: 1; }
641
- to { opacity: 0; }
 
 
 
 
642
  }
643
 
644
  .card-slot .pokecard {
@@ -671,12 +680,12 @@ img.hf-logo {
671
  }
672
  }
673
 
674
-
675
  /* Pokémon Card */
676
 
677
  .card-slot {
 
678
  perspective: 100rem;
679
- transition: transform 0.5s ease-out;
680
  }
681
 
682
  .grass {
@@ -825,13 +834,6 @@ img.hf-logo {
825
  box-shadow: 0 0.75rem 1.25rem 0 hsl(0 0% 50% / 40%);
826
  }
827
 
828
- .pokecard[data-displayed='true'] {
829
- display: flex;
830
- }
831
- .pokecard[data-displayed='false'] {
832
- display: none;
833
- }
834
-
835
  .pokecard .lower-half {
836
  display: flex;
837
  flex-direction: column;
@@ -980,11 +982,12 @@ header .energy {
980
  text-align: center;
981
  }
982
 
983
- .no-cost .attack-text > span:only-child, .no-cost.no-damage .attack-text > span:only-child {
 
984
  width: var(--card-width);
985
  margin-left: -2.5rem;
986
  }
987
- .no-damage .attack-text > span:only-child {
988
  width: var(--card-width);
989
  margin-left: -5.5rem;
990
  }
 
9
  --theme-error-border: hsl(355 85% 55%);
10
  }
11
 
 
 
 
 
 
12
  * {
13
  transition: outline-offset 0.25s ease-out;
14
  outline-style: none;
 
31
  background-color: gold;
32
  }
33
 
34
+ html {
35
+ display: flex;
36
+ display: grid;
37
+ align-items: center;
38
+ height: 100%;
39
+ }
40
+
41
  body {
42
+ margin: 0;
43
  background-color: whitesmoke;
44
  background-image: linear-gradient(300deg, var(--theme-highlight), white);
45
  font-family: 'Gill Sans', 'Gill Sans Mt', 'sans-serif';
46
+ overflow-x: hidden;
47
  }
48
 
49
  main {
 
52
  grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
53
  gap: 1.5rem 0;
54
  max-width: 80rem;
55
+ height: 100%;
56
+ padding: 0 3rem;
57
  margin: 0 auto;
58
  }
59
 
 
73
  .scene .card-slot {
74
  margin-top: 1rem;
75
  }
76
+ }
77
 
78
+ @media (max-width: 895px) {
79
+ html {
80
+ height: auto;
81
+ }
82
+ }
83
+
84
+ @media (max-width: 1024px) {
85
+ .output .booster {
86
+ --booster-scale: 0.6;
87
+ }
88
  }
89
 
90
  @media (max-width: 1280px) {
 
163
  font-weight: 700;
164
  }
165
 
166
+ .info form {
167
+ display: flex;
168
+ flex-direction: row;
169
+ width: 80%;
170
+ margin: 0.5rem auto;
171
+ }
172
+
173
+ .info .name-interactive {
174
+ display: flex;
175
+ flex-direction: row;
176
+ }
177
+
178
  .info input {
179
  display: block;
180
+ width: 100%;
181
  box-sizing: border-box;
182
+ padding: 0.5rem 1rem 0.5rem 5rem;
 
183
  border: 0.2rem solid hsl(0 0% 70%);
184
+ border-right: none;
185
  text-align: center;
186
  font-size: 1.25rem;
187
+ transition: box-shadow 0.5s ease-out;
188
+ border-top-left-radius: 1rem;
189
+ border-bottom-left-radius: 1rem;
190
+ box-shadow: none;
191
  }
192
 
193
  .info input::placeholder {
 
195
  }
196
 
197
  input:focus {
198
+ border-color: var(--theme-secondary);
199
+ box-shadow: 0 0 0.5rem hsl(165 67% 48% / 60%);
200
+ }
201
+
202
+ form button {
203
+ height: 2.8125rem;
204
+ border-top-left-radius: 0;
205
+ border-bottom-left-radius: 0;
206
  }
207
 
208
  .info p {
 
213
  line-height: 1.5rem;
214
  }
215
 
216
+ .info a,
217
+ info a:is(:hover, :focus, :active, :visited) {
218
  color: var(--theme-subtext);
219
  cursor: pointer;
220
  }
 
225
  display: flex;
226
  flex-direction: column;
227
  justify-content: space-around;
228
+ height: min-content;
229
  }
230
 
231
  .output .actions {
 
251
  font-weight: bold;
252
  color: white;
253
  transform-origin: bottom;
254
+
255
  transition: transform 0.5s ease, box-shadow 0.1s, outline-offset 0.25s ease-out, filter 0.25s ease-out, opacity 0.25s;
 
256
  whitespace: nowrap;
 
257
  filter: saturate(1);
258
  cursor: pointer;
259
+ }
260
+
261
+ .actions button {
262
+ transform: translateY(-25%);
263
+ box-shadow: 0 0.2rem 0.375rem hsl(158 100% 33% / 60%);
264
  user-select: none;
265
  pointer-events: none;
266
  opacity: 0;
 
281
  filter: saturate(0.15);
282
  }
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  .scene {
285
  --scale: 0.9;
286
+ height: min-content;
287
  box-sizing: border-box;
 
288
  perspective: 100rem;
289
+ transform-origin: center;
290
  transform: scale(var(--scale));
291
  transition: transform 0.5s ease-out;
292
  }
 
583
 
584
  @keyframes shrink {
585
  from {
586
+ transform: rotateZ(45deg) scale(var(--booster-scale));
587
  opacity: 1;
588
  }
589
  to {
590
+ transform: rotateZ(270deg) scale(0);
591
  opacity: 0;
592
  }
593
  }
 
615
 
616
  [data-mode='booster'][data-state='completed'] .card-slot {
617
  transform: scale(0);
618
+ opacity: 0;
619
  }
620
 
621
  [data-mode='booster'][data-state='completed'] .back {
 
628
 
629
  [data-mode='card'][data-state='completed'] .card-slot {
630
  transform: scale(1);
631
+ opacity: 1;
632
  }
633
 
634
  @media (prefers-reduced-motion) {
635
  @keyframes pulse {
636
+ from {
637
+ opacity: 1;
638
+ }
639
+ to {
640
+ opacity: 0.6;
641
+ }
642
  }
643
 
644
  @keyframes fade {
645
+ from {
646
+ opacity: 1;
647
+ }
648
+ to {
649
+ opacity: 0;
650
+ }
651
  }
652
 
653
  .card-slot .pokecard {
 
680
  }
681
  }
682
 
 
683
  /* Pokémon Card */
684
 
685
  .card-slot {
686
+ height: 100%;
687
  perspective: 100rem;
688
+ transition: transform 0.5s ease-out, opacity 0.5s ease-in;
689
  }
690
 
691
  .grass {
 
834
  box-shadow: 0 0.75rem 1.25rem 0 hsl(0 0% 50% / 40%);
835
  }
836
 
 
 
 
 
 
 
 
837
  .pokecard .lower-half {
838
  display: flex;
839
  flex-direction: column;
 
982
  text-align: center;
983
  }
984
 
985
+ .no-cost .attack-text > span:only-child,
986
+ .no-cost.no-damage .attack-text > span:only-child {
987
  width: var(--card-width);
988
  margin-left: -2.5rem;
989
  }
990
+ .no-damage .attack-text > span:only-child {
991
  width: var(--card-width);
992
  margin-left: -5.5rem;
993
  }