はじめに
Streamlitの勉強も兼ねて、子ども向けのアプリを作ってみました。
コンセプト
とにかく子どもは飽きやすいので以下のことを意識しました。
- いろんなミニアプリを作って切り替えられるようにする
- 内容も差し替えや追加を簡単にできるようにする
- イラストや効果音、アニメーションを多用する
一方で子ども向けなので、手の込んだ内容にする必要はなく、以下のような簡単な機能でとりあえずスタートさせてみます。
- 写真が撮れてアルバムのように見れる
- 音声を録音して再生できる
- お絵描きができる
- ちょっとしたクイズ
画像や音声ファイルの配置
- 方法1
GoogleDriveに配置して共有リンクを指定(アドレスを修正する必要あり)
- 方法2
Streamlit Cloudに直接配置して指定(今回はこちらにしました。)
以下のように配置して相対パス(./assets/image.jpgなど)で指定可能です。
.
|_assets
| |_image.jpg
| |_audio.mp3
|_app.py
写真を撮ってアルバムのように表示
カメラ機能はst.camera_input
で実装可能です。
import streamlit as st
picture = st.camera_input("Take a picture")
if picture:
st.image(picture)
アルバム表示については当初、st.columnsを利用してページ下部にグリッド配置するようにしていましたが、サイドバーにすぐ出てくるのがウケたので変更しました)
def photo():
# 各種メニューの非表示設定
hide_menu_style = """
<style>
#MainMenu {visibility: hidden; }
footer {visibility: hidden;}
</style>
"""
st.markdown(hide_menu_style, unsafe_allow_html=True)
# 初期化
if "pictures" not in st.session_state:
st.session_state.pictures = []
picture = False
picture = st.camera_input(" ")
if picture:
st.session_state.pictures.append(picture)
with st.sidebar:
st.write("# 📸撮った写真")
if st.session_state.pictures:
for picture in st.session_state.pictures:
st.image(picture)
音声を録音して再生
audio_recorder_streamlit
を利用します。
pip install audio_recorder_streamlit
import streamlit as st
from audio_recorder_streamlit import audio_recorder
def audio_recorder():
# 初期化
if "audio_list" not in st.session_state:
st.session_state.audio_list = []
audio_bytes = False
audio_bytes = audio_recorder()
if audio_bytes:
st.session_state.audio_list.append(audio_bytes)
with st.sidebar:
st.write("# 🎙️録った音")
if st.session_state.audio_list:
for audio_bytes in st.session_state.audio_list:
st.audio(audio_bytes, format="audio/wav")
一覧の表示は写真と同じにしています。
お絵描き
streamlit-drawable-canvas
を使います。
pip install streamlit-drawable-canvas
def paint():
def save_paint():
st.session_state.paint_list.append(canvas_result.image_data)
# 各種メニューの非表示設定
hide_menu_style = """
<style>
#MainMenu {visibility: hidden; }
footer {visibility: hidden;}
</style>
"""
st.markdown(hide_menu_style, unsafe_allow_html=True)
# 初期化
if "paint_list" not in st.session_state:
st.session_state.paint_list = []
# Specify canvas parameters in application
drawing_mode = st.sidebar.selectbox("Drawing tool:", ("freedraw", "transform"))
stroke_width = st.sidebar.slider("Stroke width: ", 1, 25, 3)
if drawing_mode == "point":
point_display_radius = st.sidebar.slider("Point display radius: ", 1, 25, 3)
stroke_color = st.sidebar.color_picker("Stroke color hex: ")
bg_color = st.sidebar.color_picker("Background color hex: ", "#eee")
bg_image = st.sidebar.file_uploader("Background image:", type=["png", "jpg"])
realtime_update = st.sidebar.checkbox("Update in realtime", True)
canvas_result = st_canvas(
fill_color="rgba(255, 165, 0, 0.3)", # Fixed fill color with some opacity
stroke_width=stroke_width,
stroke_color=stroke_color,
background_color=bg_color,
background_image=Image.open(bg_image) if bg_image else None,
update_streamlit=realtime_update,
height=400,
drawing_mode=drawing_mode,
point_display_radius=point_display_radius if drawing_mode == "point" else 0,
key="canvas",
)
st.button("保存", on_click=save_paint)
with st.sidebar:
st.write("# 🎨描いた絵")
for paint in st.session_state.paint_list:
st.image(paint)
ちょっとしたクイズ
2人子どもがいるのですが、お互いの小さい頃(1ヶ月〜6ヶ月ぐらい)の写真を表示してどちらが自分(姉)かを選択するものにしました。
※サンプルではどっちがにゃんこでしょう。に差し替えています。
import base64
import glob
import random
import time
import streamlit as st
from st_clickable_images import clickable_images
def quiz():
container = st.empty()
def judge(clicked, correct, page_id):
if clicked == correct:
time.sleep(0.5)
st.markdown(st.session_state.correct_sound_html, unsafe_allow_html=True)
st.balloons()
st.session_state.correct_num += 1
else:
time.sleep(0.5)
st.markdown(st.session_state.failed_sound_html, unsafe_allow_html=True)
st.session_state.page_id = page_id
st.session_state.first = True
def quest(no):
st.markdown(
f"<h1 style='text-align: center;'>第{no}問</h1>",
unsafe_allow_html=True,
)
if st.session_state.first:
st.session_state.questions = [0] * 2
st.session_state.correct, st.session_state.failed = random.sample([0, 1], 2)
st.session_state.questions[st.session_state.correct] = random.choice(
st.session_state.siz_images
)
st.session_state.questions[st.session_state.failed] = random.choice(
st.session_state.ao_images
)
st.session_state.first = False
clicked = clickable_images(
st.session_state.questions,
titles=[f"Image#{str(i)}" for i in range(2)],
div_style={
"display": "flex",
"justify-content": "center",
"flex-wrap": "wrap",
},
img_style={"margin": "5px", "height": "400px"},
key=no,
)
if clicked > -1:
if no != st.session_state.quest_num:
judge(clicked, st.session_state.correct, f"page{no+1}")
else:
judge(clicked, st.session_state.correct, "final")
else:
st.stop()
if "page_id" not in st.session_state:
siz_files = glob.glob("./assets/image/siz/*")
ao_files = glob.glob("./assets/image/ao/*")
start_image = "./assets/image/start.png"
again_image = "./assets/image/again.jpg"
st.session_state.sounds = {
"correct": "./assets/audio/correct.mp3",
"failed": "./assets/audio/failed.mp3",
}
with open(st.session_state.sounds["correct"], "rb") as file1:
audio_str = "data:audio/ogg;base64,%s" % (
base64.b64encode(file1.read()).decode()
)
st.session_state.correct_sound_html = (
"""
<audio autoplay=True>
<source src="%s" type="audio/ogg" autoplay=True>
Your browser does not support the audio element.
</audio>
"""
% audio_str
)
with open(st.session_state.sounds["failed"], "rb") as file2:
audio_str = "data:audio/ogg;base64,%s" % (
base64.b64encode(file2.read()).decode()
)
st.session_state.failed_sound_html = (
"""
<audio autoplay=True>
<source src="%s" type="audio/ogg" autoplay=True>
Your browser does not support the audio element.
</audio>
"""
% audio_str
)
st.session_state.siz_images = []
for file1 in siz_files:
with open(file1, "rb") as image1:
encoded = base64.b64encode(image1.read()).decode()
st.session_state.siz_images.append(f"data:image/jpeg;base64,{encoded}")
st.session_state.ao_images = []
for file2 in ao_files:
with open(file2, "rb") as image2:
encoded = base64.b64encode(image2.read()).decode()
st.session_state.ao_images.append(f"data:image/jpeg;base64,{encoded}")
with open(start_image, "rb") as image3:
encoded = base64.b64encode(image3.read()).decode()
st.session_state.start_logo = f"data:image/jpeg;base64,{encoded}"
with open(again_image, "rb") as image4:
encoded = base64.b64encode(image4.read()).decode()
st.session_state.again_logo = f"data:image/jpeg;base64,{encoded}"
st.session_state.correct_num = 0
st.session_state.first = True
st.session_state.page_id = "main"
if st.session_state.page_id == "main":
with container.container():
st.markdown(
"<h1 style='text-align: center;'>どっちがにゃんこでしょう??</h1>",
unsafe_allow_html=True,
)
st.markdown(
"<h3 style='text-align: center;'>問題数</h3>",
unsafe_allow_html=True,
)
_, _, center, _, _ = st.columns(5)
with center:
st.session_state.quest_num = int(
st.radio(" ", ("3", "5", "10"), horizontal=True)
)
clicked = clickable_images(
[st.session_state.start_logo],
titles=[f"Image#{str(i)}" for i in range(1)],
div_style={
"display": "flex",
"justify-content": "center",
"flex-wrap": "wrap",
},
img_style={"margin": "5px", "height": "200px"},
key="top",
)
if clicked == 0:
st.session_state.page_id = "page1"
st.experimental_rerun()
for num in range(st.session_state.quest_num):
if st.session_state.page_id == f"page{num+1}":
with container.container():
quest(num + 1)
if st.session_state.page_id == "final":
with container.container():
st.markdown(
"<h1 style='text-align: center;'>結果</h1>",
unsafe_allow_html=True,
)
time.sleep(1)
st.markdown(
f"<h1 style='text-align: center;'>{st.session_state.correct_num}問正解!!</h1>",
unsafe_allow_html=True,
)
clicked = clickable_images(
[st.session_state.again_logo],
titles=[f"Image#{str(i)}" for i in range(1)],
div_style={
"display": "flex",
"justify-content": "center",
"flex-wrap": "wrap",
},
img_style={"margin": "5px", "height": "200px"},
key="final",
)
if clicked == 0:
st.session_state.pop("page_id")
st.experimental_rerun()
メニューバーをつける
サイドバーよりページ上部にメニューがあるほうが選びやすいと思ったので、streamlit-option-menu
を使います。
pip install streamlit-option-menu
画像のボタンを作る
標準のボタンだと味気ないので画像をボタンにします。
st-clickable-images
を使います。
pip install st-clickable-images
app.py
各アプリをメニューバーをもちいて切り替えるメインの画面です。
import streamlit as st
from audio import audio
from paint import paint
from photo import photo
from quiz import quiz
from streamlit_option_menu import option_menu
st.set_page_config(
page_title="KidsApps",
layout="wide",
initial_sidebar_state="expanded",
)
# 各種メニューの非表示設定
hide_menu_style = """
<style>
#MainMenu {visibility: hidden; }
footer {visibility: hidden;}
</style>
"""
st.markdown(hide_menu_style, unsafe_allow_html=True)
selected = option_menu(
None,
["写真", "録音", "お絵描き", "クイズ"],
icons=["camera", "mic", "palette", "question-circle"],
menu_icon="cast",
default_index=0,
orientation="horizontal",
)
if selected == "写真":
photo()
if selected == "録音":
audio()
if selected == "お絵描き":
paint()
if selected == "クイズ":
quiz()
Streamlit Cloudへ公開する
おおまかな流れは以下記事と同じなので省略します。
requirements.txtの作成
追加でインストールしたライブラリ等はrequirements.txtにまとめておくことでインストールしてくれます。
pip freeze > requirements.txt
タブレットからアクセスする
普段Fire HD8 Plusでしま〇じろうを嗜んでおられるので、ホーム画面にアプリのブックマークを配置したかったのですが、どうやらできない模様。。
しかたなく、シルクブラウザのお気に入りに登録して初期画面に表示するという方法をとりました。
感想
StreamlitはAIや機械学習を活用したWebアプリ作成で人気があると思いますが、簡易なフロントエンド作成にとても便利なのでオススメです。