Spaces:
Sleeping
Sleeping
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 +1 -0
- app.py +20 -108
- datasets/pregenerated_pokemon.h5 +3 -0
- lists/names/{Colorless.json → colorless.json} +0 -0
- lists/names/{Darkness.json → darkness.json} +0 -0
- lists/names/{Dragon.json → dragon.json} +0 -0
- lists/names/{Fairy.json → fairy.json} +0 -0
- lists/names/{Fighting.json → fighting.json} +0 -0
- lists/names/{Fire.json → fire.json} +0 -0
- lists/names/{Grass.json → grass.json} +0 -0
- lists/names/{Lightning.json → lightning.json} +0 -0
- lists/names/{Metal.json → metal.json} +0 -0
- lists/names/{Psychic.json → psychic.json} +0 -0
- lists/names/{Water.json → water.json} +0 -0
- modules/dataset.py +39 -0
- modules/details.py +100 -48
- modules/inference.py +0 -64
- notebooks/populate_dataset.ipynb +0 -0
- static/index.html +9 -9
- static/js/card-html.js +14 -14
- static/js/dom-manipulation.js +1 -38
- static/js/index.js +17 -38
- static/js/network.js +0 -59
- static/style.css +81 -78
.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
|
2 |
-
from
|
3 |
|
4 |
-
from fastapi import
|
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.
|
11 |
|
12 |
app = FastAPI(docs_url=None, redoc_url=None)
|
13 |
|
14 |
app.mount("/static", StaticFiles(directory="static"), name="static")
|
15 |
|
16 |
-
|
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('/
|
91 |
-
def
|
92 |
-
|
93 |
-
|
94 |
|
95 |
-
|
96 |
-
def create_task(background_tasks: BackgroundTasks, new_task: NewTask):
|
97 |
-
created_at = time()
|
98 |
|
99 |
-
|
100 |
-
|
101 |
-
|
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 |
-
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
22 |
|
23 |
return random.choices(hp_range, weights)[0]
|
24 |
|
25 |
|
26 |
-
def
|
27 |
-
|
|
|
|
|
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=
|
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 = [
|
50 |
-
|
51 |
|
52 |
-
weights = [30, 220, 230, 176, 130, 109, 63, 27, 17, 17, 5, 5, 6,
|
53 |
-
|
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
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
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(
|
78 |
-
|
|
|
|
|
|
|
79 |
|
80 |
-
|
81 |
-
# so this would loop indefinitely if looking for one
|
82 |
|
83 |
-
if energy_type
|
84 |
-
|
85 |
-
random_attack
|
86 |
-
|
87 |
-
|
88 |
-
|
|
|
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
|
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 |
-
|
120 |
-
|
121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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":
|
132 |
-
"resistance":
|
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 |
-
|
32 |
-
<
|
33 |
-
<input name="name" type="text" placeholder="Ash" maxlength="75" />
|
34 |
-
|
35 |
-
|
|
|
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 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
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()}"
|
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 |
-
|
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 {
|
|
|
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 <=
|
50 |
-
|
51 |
}
|
52 |
|
53 |
-
|
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 |
-
|
59 |
-
|
60 |
|
61 |
-
|
62 |
-
setOutput('booster', 'failed');
|
63 |
-
throw new Error(`Task failed: ${completedTask.error}`);
|
64 |
-
}
|
65 |
|
66 |
-
|
67 |
-
|
68 |
-
cardSlot.innerHTML = cardHTML(details);
|
69 |
-
updateCardName(trainerName, pokeName, useTrainerName);
|
70 |
-
document.querySelector('img.picture').src = completedTask.value;
|
71 |
|
72 |
-
|
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.
|
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 |
-
|
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 |
-
|
|
|
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:
|
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-
|
159 |
text-align: center;
|
160 |
font-size: 1.25rem;
|
161 |
-
transition:
|
162 |
-
|
163 |
-
|
|
|
164 |
}
|
165 |
|
166 |
.info input::placeholder {
|
@@ -168,9 +195,14 @@ section {
|
|
168 |
}
|
169 |
|
170 |
input:focus {
|
171 |
-
border-color:
|
172 |
-
|
173 |
-
|
|
|
|
|
|
|
|
|
|
|
174 |
}
|
175 |
|
176 |
.info p {
|
@@ -181,7 +213,8 @@ input:focus {
|
|
181 |
line-height: 1.5rem;
|
182 |
}
|
183 |
|
184 |
-
.info a,
|
|
|
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:
|
196 |
}
|
197 |
|
198 |
.output .actions {
|
@@ -218,13 +251,16 @@ button {
|
|
218 |
font-weight: bold;
|
219 |
color: white;
|
220 |
transform-origin: bottom;
|
221 |
-
|
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:
|
287 |
box-sizing: border-box;
|
288 |
-
margin: 2rem;
|
289 |
perspective: 100rem;
|
290 |
-
transform-origin: center
|
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(
|
588 |
opacity: 1;
|
589 |
}
|
590 |
to {
|
591 |
-
transform: rotateZ(
|
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 {
|
636 |
-
|
|
|
|
|
|
|
|
|
637 |
}
|
638 |
|
639 |
@keyframes fade {
|
640 |
-
from {
|
641 |
-
|
|
|
|
|
|
|
|
|
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,
|
|
|
984 |
width: var(--card-width);
|
985 |
margin-left: -2.5rem;
|
986 |
}
|
987 |
-
.no-damage .attack-text
|
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 |
}
|