Spaces:
Sleeping
Sleeping
init
Browse files- .gitignore +163 -0
- Dockerfile +22 -0
- README.md +20 -4
- app.py +81 -0
- data/k49-test-imgs.npz +3 -0
- data/k49-test-labels.npz +3 -0
- data/k49-train-imgs.npz +3 -0
- data/k49-train-labels.npz +3 -0
- data/k49_classmap.csv +50 -0
- image.png +0 -0
- main.ipynb +0 -0
- model/k49_model.h5 +3 -0
- static/hiragana-characters.png +0 -0
- static/hiragana.json +50 -0
- static/index.html +25 -0
- static/script.js +166 -0
- static/styles.css +87 -0
.gitignore
ADDED
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
share/python-wheels/
|
24 |
+
*.egg-info/
|
25 |
+
.installed.cfg
|
26 |
+
*.egg
|
27 |
+
MANIFEST
|
28 |
+
|
29 |
+
# PyInstaller
|
30 |
+
# Usually these files are written by a python script from a template
|
31 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
32 |
+
*.manifest
|
33 |
+
*.spec
|
34 |
+
|
35 |
+
# Installer logs
|
36 |
+
pip-log.txt
|
37 |
+
pip-delete-this-directory.txt
|
38 |
+
|
39 |
+
# Unit test / coverage reports
|
40 |
+
htmlcov/
|
41 |
+
.tox/
|
42 |
+
.nox/
|
43 |
+
.coverage
|
44 |
+
.coverage.*
|
45 |
+
.cache
|
46 |
+
nosetests.xml
|
47 |
+
coverage.xml
|
48 |
+
*.cover
|
49 |
+
*.py,cover
|
50 |
+
.hypothesis/
|
51 |
+
.pytest_cache/
|
52 |
+
cover/
|
53 |
+
|
54 |
+
# Translations
|
55 |
+
*.mo
|
56 |
+
*.pot
|
57 |
+
|
58 |
+
# Django stuff:
|
59 |
+
*.log
|
60 |
+
local_settings.py
|
61 |
+
db.sqlite3
|
62 |
+
db.sqlite3-journal
|
63 |
+
|
64 |
+
# Flask stuff:
|
65 |
+
instance/
|
66 |
+
.webassets-cache
|
67 |
+
|
68 |
+
# Scrapy stuff:
|
69 |
+
.scrapy
|
70 |
+
|
71 |
+
# Sphinx documentation
|
72 |
+
docs/_build/
|
73 |
+
|
74 |
+
# PyBuilder
|
75 |
+
.pybuilder/
|
76 |
+
target/
|
77 |
+
|
78 |
+
# Jupyter Notebook
|
79 |
+
.ipynb_checkpoints
|
80 |
+
|
81 |
+
# IPython
|
82 |
+
profile_default/
|
83 |
+
ipython_config.py
|
84 |
+
|
85 |
+
# pyenv
|
86 |
+
# For a library or package, you might want to ignore these files since the code is
|
87 |
+
# intended to run in multiple environments; otherwise, check them in:
|
88 |
+
# .python-version
|
89 |
+
|
90 |
+
# pipenv
|
91 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
92 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
93 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
94 |
+
# install all needed dependencies.
|
95 |
+
#Pipfile.lock
|
96 |
+
|
97 |
+
# poetry
|
98 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
99 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
100 |
+
# commonly ignored for libraries.
|
101 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
102 |
+
#poetry.lock
|
103 |
+
|
104 |
+
# pdm
|
105 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
106 |
+
#pdm.lock
|
107 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
108 |
+
# in version control.
|
109 |
+
# https://pdm.fming.dev/#use-with-ide
|
110 |
+
.pdm.toml
|
111 |
+
|
112 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
113 |
+
__pypackages__/
|
114 |
+
|
115 |
+
# Celery stuff
|
116 |
+
celerybeat-schedule
|
117 |
+
celerybeat.pid
|
118 |
+
|
119 |
+
# SageMath parsed files
|
120 |
+
*.sage.py
|
121 |
+
|
122 |
+
# Environments
|
123 |
+
.env
|
124 |
+
.venv
|
125 |
+
env/
|
126 |
+
venv/
|
127 |
+
ENV/
|
128 |
+
env.bak/
|
129 |
+
venv.bak/
|
130 |
+
|
131 |
+
# Spyder project settings
|
132 |
+
.spyderproject
|
133 |
+
.spyproject
|
134 |
+
|
135 |
+
# Rope project settings
|
136 |
+
.ropeproject
|
137 |
+
|
138 |
+
# mkdocs documentation
|
139 |
+
/site
|
140 |
+
|
141 |
+
# mypy
|
142 |
+
.mypy_cache/
|
143 |
+
.dmypy.json
|
144 |
+
dmypy.json
|
145 |
+
|
146 |
+
# Pyre type checker
|
147 |
+
.pyre/
|
148 |
+
|
149 |
+
# pytype static type analyzer
|
150 |
+
.pytype/
|
151 |
+
|
152 |
+
# Cython debug symbols
|
153 |
+
cython_debug/
|
154 |
+
|
155 |
+
# PyCharm
|
156 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
157 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
158 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
159 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
160 |
+
#.idea/
|
161 |
+
|
162 |
+
Pipfile
|
163 |
+
Pipfile.lock
|
Dockerfile
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.11
|
2 |
+
|
3 |
+
WORKDIR /code
|
4 |
+
|
5 |
+
COPY ./requirements.txt /code/requirements.txt
|
6 |
+
|
7 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
8 |
+
|
9 |
+
COPY . .
|
10 |
+
|
11 |
+
RUN useradd -m -u 1000 user
|
12 |
+
|
13 |
+
USER user
|
14 |
+
|
15 |
+
ENV HOME=/home/user \
|
16 |
+
PATH=/home/user/.local/bin:$PATH
|
17 |
+
|
18 |
+
WORKDIR $HOME/app
|
19 |
+
|
20 |
+
COPY --chown=user . $HOME/app
|
21 |
+
|
22 |
+
CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
@@ -1,10 +1,26 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
---
|
9 |
|
10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: hiragana-handwriting-app
|
3 |
+
emoji: 🏯
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: black
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
---
|
9 |
|
10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
11 |
+
|
12 |
+
# Handwriting Hiragana Quiz
|
13 |
+
|
14 |
+
## Description
|
15 |
+
|
16 |
+
This web application consists of a type of quiz where the user can pick a hiragana character and write it on the screen. The application will then check if the character is correct or not.
|
17 |
+
|
18 |
+
The code was originally placed on GitHub [here](https://github.com/Detopall/handwriting-hiragana). This Hugging Face Spaces repository was created to host the application on Hugging Face Spaces.
|
19 |
+
|
20 |
+
## Data
|
21 |
+
|
22 |
+
The data used for this project is a dataset of hiragana characters. This dataset can be found [here](https://github.com/rois-codh/kmnist). The dataset contains 48 different hiragana characters (with one extra character that is not used in production).
|
23 |
+
|
24 |
+
## Usage
|
25 |
+
|
26 |
+
To use the application you will need to select a hiragana character from one of the boxes, then you will need to write the character on the screen. After you finish writing the character, the application will check if the character is correct or not.
|
app.py
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import tensorflow as tf
|
2 |
+
from PIL import Image
|
3 |
+
import numpy as np
|
4 |
+
from fastapi import FastAPI
|
5 |
+
from fastapi.staticfiles import StaticFiles
|
6 |
+
from fastapi.responses import FileResponse
|
7 |
+
import io
|
8 |
+
import base64
|
9 |
+
import uvicorn
|
10 |
+
from fastapi.middleware.cors import CORSMiddleware
|
11 |
+
import pydantic
|
12 |
+
|
13 |
+
app = FastAPI()
|
14 |
+
|
15 |
+
# Add CORS middleware
|
16 |
+
app.add_middleware(
|
17 |
+
CORSMiddleware,
|
18 |
+
allow_origins=["*"],
|
19 |
+
allow_methods=["*"],
|
20 |
+
allow_headers=["*"]
|
21 |
+
)
|
22 |
+
|
23 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
24 |
+
|
25 |
+
@app.get("/")
|
26 |
+
def read_root():
|
27 |
+
return FileResponse("static/index.html")
|
28 |
+
|
29 |
+
# Load the model
|
30 |
+
model = tf.keras.models.load_model('model/k49_model.h5')
|
31 |
+
|
32 |
+
class ImageRequest(pydantic.BaseModel):
|
33 |
+
image: str
|
34 |
+
label: int
|
35 |
+
|
36 |
+
@app.post("/api/predict")
|
37 |
+
def predict(request: dict):
|
38 |
+
image = request["image"]
|
39 |
+
label = request["label"]
|
40 |
+
|
41 |
+
# Decode base64-encoded image
|
42 |
+
image_bytes = base64.b64decode(image["image"].split(",")[1])
|
43 |
+
|
44 |
+
# Convert image to preprocessed numpy array and save image to server
|
45 |
+
X = preprocess(image_bytes)
|
46 |
+
image = Image.fromarray(X[0].reshape(28, 28) * 255).convert("RGB")
|
47 |
+
|
48 |
+
image.save("image.png")
|
49 |
+
|
50 |
+
# Make prediction using the model
|
51 |
+
prediction = model.predict(X)
|
52 |
+
# Convert prediction to integer
|
53 |
+
prediction = int(np.argmax(prediction))
|
54 |
+
|
55 |
+
print("label: ", label)
|
56 |
+
print("prediction: ", prediction)
|
57 |
+
|
58 |
+
return {'prediction': prediction == int(label)}
|
59 |
+
|
60 |
+
|
61 |
+
def preprocess(image_bytes):
|
62 |
+
# Decode base64-encoded image and convert to PIL Image object
|
63 |
+
img = Image.open(io.BytesIO(image_bytes))
|
64 |
+
# Convert transparent background to white
|
65 |
+
if img.mode == 'RGBA':
|
66 |
+
img.load()
|
67 |
+
background = Image.new('RGB', img.size, (255, 255, 255))
|
68 |
+
background.paste(img, mask=img.split()[3])
|
69 |
+
img = background
|
70 |
+
# Convert to grayscale
|
71 |
+
img = img.convert('L')
|
72 |
+
# Invert colors to black background with white numbers
|
73 |
+
img = np.invert(np.array(img))
|
74 |
+
# Resize image to 28x28 pixels
|
75 |
+
img = Image.fromarray(img).resize((28, 28))
|
76 |
+
# Convert image to numpy array and normalize pixel values
|
77 |
+
X = np.array(img).reshape(1, 28, 28, 1) / 255
|
78 |
+
return X
|
79 |
+
|
80 |
+
if __name__ == '__main__':
|
81 |
+
uvicorn.run('app:app', host='localhost', port=8000, reload=True)
|
data/k49-test-imgs.npz
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:1de2476bb29ed1a12a2424e6724349f7519f6f39d112848aea6aa9c21eeaf594
|
3 |
+
size 10971201
|
data/k49-test-labels.npz
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:299be30ea32c2cc69318f5a5a579ffbffb0a6a1ccff134fb0ca834cd525ad6af
|
3 |
+
size 27450
|
data/k49-train-imgs.npz
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:1c42adc463ed8efe598cf002f6ecafbca6d8c38f0551f7a35e0b23755fca1c8d
|
3 |
+
size 66117696
|
data/k49-train-labels.npz
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:fbfef4750ce9aa70b6072f0bca7daa9e80e2d79b379d5ae923265a63396260e4
|
3 |
+
size 164485
|
data/k49_classmap.csv
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
index,codepoint,char
|
2 |
+
0,U+3042,あ
|
3 |
+
1,U+3044,い
|
4 |
+
2,U+3046,う
|
5 |
+
3,U+3048,え
|
6 |
+
4,U+304A,お
|
7 |
+
5,U+304B,か
|
8 |
+
6,U+304D,き
|
9 |
+
7,U+304F,く
|
10 |
+
8,U+3051,け
|
11 |
+
9,U+3053,こ
|
12 |
+
10,U+3055,さ
|
13 |
+
11,U+3057,し
|
14 |
+
12,U+3059,す
|
15 |
+
13,U+305B,せ
|
16 |
+
14,U+305D,そ
|
17 |
+
15,U+305F,た
|
18 |
+
16,U+3061,ち
|
19 |
+
17,U+3064,つ
|
20 |
+
18,U+3066,て
|
21 |
+
19,U+3068,と
|
22 |
+
20,U+306A,な
|
23 |
+
21,U+306B,に
|
24 |
+
22,U+306C,ぬ
|
25 |
+
23,U+306D,ね
|
26 |
+
24,U+306E,の
|
27 |
+
25,U+306F,は
|
28 |
+
26,U+3072,ひ
|
29 |
+
27,U+3075,ふ
|
30 |
+
28,U+3078,へ
|
31 |
+
29,U+307B,ほ
|
32 |
+
30,U+307E,ま
|
33 |
+
31,U+307F,み
|
34 |
+
32,U+3080,む
|
35 |
+
33,U+3081,め
|
36 |
+
34,U+3082,も
|
37 |
+
35,U+3084,や
|
38 |
+
36,U+3086,ゆ
|
39 |
+
37,U+3088,よ
|
40 |
+
38,U+3089,ら
|
41 |
+
39,U+308A,り
|
42 |
+
40,U+308B,る
|
43 |
+
41,U+308C,れ
|
44 |
+
42,U+308D,ろ
|
45 |
+
43,U+308F,わ
|
46 |
+
44,U+3090,ゐ
|
47 |
+
45,U+3091,ゑ
|
48 |
+
46,U+3092,を
|
49 |
+
47,U+3093,ん
|
50 |
+
48,U+309D,ゝ
|
image.png
ADDED
![]() |
main.ipynb
ADDED
The diff for this file is too large to render.
See raw diff
|
|
model/k49_model.h5
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:fa72f71c814c845a8324d25977e78a99a6d745a1bec3336c9e07ed0e2d18baf7
|
3 |
+
size 5779560
|
static/hiragana-characters.png
ADDED
![]() |
static/hiragana.json
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"A": 0,
|
3 |
+
"I": 1,
|
4 |
+
"U": 2,
|
5 |
+
"E": 3,
|
6 |
+
"O": 4,
|
7 |
+
"KA": 5,
|
8 |
+
"KI": 6,
|
9 |
+
"KU": 7,
|
10 |
+
"KE": 8,
|
11 |
+
"KO": 9,
|
12 |
+
"SA": 10,
|
13 |
+
"SHI": 11,
|
14 |
+
"SU": 12,
|
15 |
+
"SE": 13,
|
16 |
+
"SO": 14,
|
17 |
+
"TA": 15,
|
18 |
+
"CHI": 16,
|
19 |
+
"TSU": 17,
|
20 |
+
"TE": 18,
|
21 |
+
"TO": 19,
|
22 |
+
"NA": 20,
|
23 |
+
"NI": 21,
|
24 |
+
"NU": 22,
|
25 |
+
"NE": 23,
|
26 |
+
"NO": 24,
|
27 |
+
"HA": 25,
|
28 |
+
"HI": 26,
|
29 |
+
"FU": 27,
|
30 |
+
"HE": 28,
|
31 |
+
"HO": 29,
|
32 |
+
"MA": 30,
|
33 |
+
"MI": 31,
|
34 |
+
"MU": 32,
|
35 |
+
"ME": 33,
|
36 |
+
"MO": 34,
|
37 |
+
"YA": 35,
|
38 |
+
"YU": 36,
|
39 |
+
"YO": 37,
|
40 |
+
"RA": 38,
|
41 |
+
"RI": 39,
|
42 |
+
"RU": 40,
|
43 |
+
"RE": 41,
|
44 |
+
"RO": 42,
|
45 |
+
"WA": 43,
|
46 |
+
"WI": 44,
|
47 |
+
"WE": 45,
|
48 |
+
"WO": 46,
|
49 |
+
"N": 47
|
50 |
+
}
|
static/index.html
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
6 |
+
<link rel="stylesheet" href="/static/styles.css" />
|
7 |
+
<script src="/static/script.js" defer></script>
|
8 |
+
<title>Hiragana Prediction</title>
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<h1>Hiragana Prediction</h1>
|
12 |
+
<a href="https://github.com/Detopall/handwriting-hiragana">GitHub</a>
|
13 |
+
<button id="showModalBtn" class="btn">Show Cheat Sheet</button>
|
14 |
+
<dialog id="modal">
|
15 |
+
<img src="/static/hiragana-characters.png" alt="Cheat Sheet" />
|
16 |
+
<button id="closeModalBtn" class="btn">Close</button>
|
17 |
+
</dialog>
|
18 |
+
<div id="hiraganaBoxes"></div>
|
19 |
+
<canvas id="canvas" width="350" height="350"></canvas>
|
20 |
+
<button id="clearCanvasBtn" class="btn">Clear Canvas</button>
|
21 |
+
<button id="checkPredictionBtn" class="btn">Check Prediction</button>
|
22 |
+
<button id="clearSavesBtn" class="btn">Clear Saves</button>
|
23 |
+
<p id="predictionResult"></p>
|
24 |
+
</body>
|
25 |
+
</html>
|
static/script.js
ADDED
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use strict";
|
2 |
+
|
3 |
+
const checkPredictionBtn = document.getElementById("checkPredictionBtn");
|
4 |
+
checkPredictionBtn.addEventListener("click", checkPrediction);
|
5 |
+
const clearCanvasBtn = document.getElementById("clearCanvasBtn");
|
6 |
+
clearCanvasBtn.addEventListener("click", clearCanvas);
|
7 |
+
const showModalBtn = document.getElementById("showModalBtn");
|
8 |
+
const modal = document.getElementById("modal");
|
9 |
+
const closeModalBtn = document.getElementById("closeModalBtn");
|
10 |
+
const clearSavesBtn = document.getElementById("clearSavesBtn");
|
11 |
+
clearSavesBtn.addEventListener("click", () => {
|
12 |
+
localStorage.removeItem("predictions");
|
13 |
+
loadPredictions();
|
14 |
+
// clear the colors of the boxes
|
15 |
+
const hiraganaBoxes = document.querySelectorAll(".hiraganaBox");
|
16 |
+
hiraganaBoxes.forEach(box => {
|
17 |
+
box.style.backgroundColor = "";
|
18 |
+
});
|
19 |
+
});
|
20 |
+
|
21 |
+
showModalBtn.addEventListener("click", () => modal.showModal());
|
22 |
+
closeModalBtn.addEventListener("click", () => modal.close());
|
23 |
+
|
24 |
+
loadHiraganaData();
|
25 |
+
|
26 |
+
// load hiragana data from json file
|
27 |
+
async function loadHiraganaData() {
|
28 |
+
let hiraganaData = {};
|
29 |
+
await fetch("/static/hiragana.json")
|
30 |
+
.then((response) => response.json())
|
31 |
+
.then((data) => {
|
32 |
+
hiraganaData = data;
|
33 |
+
const hiraganaBoxes = document.getElementById("hiraganaBoxes");
|
34 |
+
for (const letter in hiraganaData) {
|
35 |
+
const box = document.createElement("div");
|
36 |
+
box.classList.add("hiraganaBox");
|
37 |
+
box.textContent = letter;
|
38 |
+
box.dataset.label = hiraganaData[letter];
|
39 |
+
hiraganaBoxes.appendChild(box);
|
40 |
+
}
|
41 |
+
})
|
42 |
+
.catch((error) => {
|
43 |
+
console.error("Hiragana data request error:", error);
|
44 |
+
});
|
45 |
+
|
46 |
+
loadPredictions();
|
47 |
+
}
|
48 |
+
|
49 |
+
|
50 |
+
|
51 |
+
async function sendPrediction(label, image) {
|
52 |
+
|
53 |
+
try {
|
54 |
+
const response = await fetch("/api/predict", {
|
55 |
+
method: "POST",
|
56 |
+
headers: { "Content-Type": "application/json" },
|
57 |
+
body: JSON.stringify({
|
58 |
+
image: { image: image },
|
59 |
+
label: label,
|
60 |
+
}),
|
61 |
+
});
|
62 |
+
const data = await response.json();
|
63 |
+
handlePrediction(label, data.prediction);
|
64 |
+
savePrediction(label, data.prediction);
|
65 |
+
} catch (error) {
|
66 |
+
console.error("Prediction request error:", error);
|
67 |
+
}
|
68 |
+
|
69 |
+
loadPredictions();
|
70 |
+
}
|
71 |
+
|
72 |
+
|
73 |
+
function handlePrediction(label, prediction) {
|
74 |
+
const predictionResultElement = document.getElementById("predictionResult");
|
75 |
+
const selectedBox = document.querySelector(`.hiraganaBox[data-label="${label}"]`);
|
76 |
+
if (prediction) {
|
77 |
+
predictionResultElement.textContent = "Correct prediction!";
|
78 |
+
selectedBox.style.backgroundColor = "green";
|
79 |
+
} else {
|
80 |
+
predictionResultElement.textContent = "Incorrect prediction!";
|
81 |
+
selectedBox.style.backgroundColor = "red";
|
82 |
+
}
|
83 |
+
}
|
84 |
+
|
85 |
+
const canvas = document.getElementById("canvas");
|
86 |
+
if (canvas) {
|
87 |
+
canvas.addEventListener("mousedown", function (e) {
|
88 |
+
startDrawing(e);
|
89 |
+
});
|
90 |
+
}
|
91 |
+
|
92 |
+
function startDrawing(e) {
|
93 |
+
canvas.addEventListener("mousemove", handleDrawing);
|
94 |
+
canvas.addEventListener("mouseup", function () {
|
95 |
+
stopDrawing();
|
96 |
+
});
|
97 |
+
}
|
98 |
+
|
99 |
+
function stopDrawing() {
|
100 |
+
canvas.removeEventListener("mousemove", handleDrawing);
|
101 |
+
}
|
102 |
+
|
103 |
+
function handleDrawing(e) {
|
104 |
+
const ctx = canvas.getContext("2d");
|
105 |
+
ctx.lineWidth = 25;
|
106 |
+
ctx.lineCap = "round"
|
107 |
+
ctx.lineJoin = "round"
|
108 |
+
ctx.strokeStyle = "#000"
|
109 |
+
const rect = canvas.getBoundingClientRect();
|
110 |
+
const x = e.clientX - rect.left;
|
111 |
+
const y = e.clientY - rect.top;
|
112 |
+
|
113 |
+
ctx.fillStyle = "#000";
|
114 |
+
ctx.beginPath();
|
115 |
+
ctx.arc(x, y, 5, 0, Math.PI * 2);
|
116 |
+
ctx.fill();
|
117 |
+
}
|
118 |
+
|
119 |
+
function clearCanvas() {
|
120 |
+
const ctx = canvas.getContext("2d");
|
121 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
122 |
+
}
|
123 |
+
|
124 |
+
function checkPrediction() {
|
125 |
+
const canvas = document.getElementById("canvas");
|
126 |
+
if (!canvas) return;
|
127 |
+
const image = canvas.toDataURL("image/png");
|
128 |
+
const selectedCharacter = document.querySelector(".hiraganaBox.selected").dataset.label;
|
129 |
+
|
130 |
+
sendPrediction(selectedCharacter, image);
|
131 |
+
}
|
132 |
+
|
133 |
+
// Event delegation for selecting hiragana boxes
|
134 |
+
document.addEventListener("click", (e) => {
|
135 |
+
const hiraganaBoxes = document.querySelectorAll(".hiraganaBox");
|
136 |
+
hiraganaBoxes.forEach(box => {
|
137 |
+
box.classList.remove("selected");
|
138 |
+
});
|
139 |
+
if (e.target.classList.contains("hiraganaBox")) {
|
140 |
+
e.target.classList.add("selected");
|
141 |
+
}
|
142 |
+
});
|
143 |
+
|
144 |
+
function savePrediction(label, prediction) {
|
145 |
+
const predictions = JSON.parse(localStorage.getItem("predictions")) || [];
|
146 |
+
// remove the old prediction if it exists
|
147 |
+
const updatedPredictions = predictions.filter(p => p.label !== label);
|
148 |
+
updatedPredictions.push({ label, prediction });
|
149 |
+
localStorage.setItem("predictions", JSON.stringify(updatedPredictions));
|
150 |
+
}
|
151 |
+
|
152 |
+
function loadPredictions() {
|
153 |
+
const predictions = JSON.parse(localStorage.getItem("predictions")) || [];
|
154 |
+
// color the boxes based on the predictions
|
155 |
+
const hiraganaBoxes = document.querySelectorAll(".hiraganaBox");
|
156 |
+
hiraganaBoxes.forEach(box => {
|
157 |
+
const prediction = predictions.find(p => p.label === box.dataset.label);
|
158 |
+
if (prediction) {
|
159 |
+
if (prediction.prediction) {
|
160 |
+
box.style.backgroundColor = "green";
|
161 |
+
} else {
|
162 |
+
box.style.backgroundColor = "red";
|
163 |
+
}
|
164 |
+
}
|
165 |
+
});
|
166 |
+
}
|
static/styles.css
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
font-family: Arial, sans-serif;
|
3 |
+
display: flex;
|
4 |
+
justify-content: center;
|
5 |
+
align-items: center;
|
6 |
+
flex-direction: column;
|
7 |
+
}
|
8 |
+
|
9 |
+
h1 {
|
10 |
+
margin-bottom: 20px;
|
11 |
+
}
|
12 |
+
|
13 |
+
a {
|
14 |
+
color: #007bff;
|
15 |
+
}
|
16 |
+
|
17 |
+
#hiraganaBoxes {
|
18 |
+
display: grid;
|
19 |
+
grid-template-columns: repeat(10, 1fr);
|
20 |
+
grid-gap: 10px;
|
21 |
+
margin: 20px 0;
|
22 |
+
}
|
23 |
+
|
24 |
+
.hiraganaBox {
|
25 |
+
border: 1px solid #333;
|
26 |
+
padding: 10px;
|
27 |
+
margin: 0 10px;
|
28 |
+
cursor: pointer;
|
29 |
+
border-radius: 0.5rem;
|
30 |
+
font-weight: bold;
|
31 |
+
width: 2rem;
|
32 |
+
text-align: center;
|
33 |
+
}
|
34 |
+
|
35 |
+
.hiraganaBox:hover {
|
36 |
+
background-color: lightblue;
|
37 |
+
}
|
38 |
+
|
39 |
+
.hiraganaBox.selected {
|
40 |
+
background-color: rgb(106, 96, 250);
|
41 |
+
}
|
42 |
+
|
43 |
+
#canvas {
|
44 |
+
border: 1px solid #333;
|
45 |
+
display: block;
|
46 |
+
margin: 0 auto 20px;
|
47 |
+
border-radius: 0.5rem;
|
48 |
+
}
|
49 |
+
|
50 |
+
.btn {
|
51 |
+
background-color: #007bff;
|
52 |
+
border: none;
|
53 |
+
color: #fff;
|
54 |
+
padding: 10px 20px;
|
55 |
+
font-size: 16px;
|
56 |
+
cursor: pointer;
|
57 |
+
margin: 0.5rem 0;
|
58 |
+
border-radius: 5px;
|
59 |
+
}
|
60 |
+
|
61 |
+
.btn:hover {
|
62 |
+
background-color: #0056b3;
|
63 |
+
}
|
64 |
+
|
65 |
+
#predictionResult {
|
66 |
+
margin-top: 20px;
|
67 |
+
font-size: 18px;
|
68 |
+
}
|
69 |
+
|
70 |
+
dialog {
|
71 |
+
position: fixed;
|
72 |
+
top: 50%;
|
73 |
+
left: 50%;
|
74 |
+
transform: translate(-50%, -50%);
|
75 |
+
background-color: white;
|
76 |
+
border: 1px solid #333;
|
77 |
+
padding: 20px;
|
78 |
+
}
|
79 |
+
|
80 |
+
dialog img {
|
81 |
+
max-width: 100%;
|
82 |
+
height: auto;
|
83 |
+
}
|
84 |
+
|
85 |
+
dialog button {
|
86 |
+
margin-top: 10px;
|
87 |
+
}
|