ryugaku-app / app.py
lorinta's picture
Upload folder using huggingface_hub
00a5799 verified
Raw
History Blame Contribute Delete
14.4 kB
"""Ryugaku - Backyard AI hackathon Gradio app.
Study assistant for international students in Japan.
Tabbed scenes: Translate, Lecture, Sheet, Review.
"""
from __future__ import annotations
import logging
import os
from typing import Any, Dict, List
import gradio as gr
from src.exam_predictor import predict_exam_topics
from src.keyword_extractor import extract_keywords
from src.ocr import ocr_image
from src.summarizer import summarize, easy_english
from src.stt import transcribe_and_translate
from src.translator import translate
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
CUSTOM_CSS = """
#ryu-app {
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
}
#ryu-app > .form {
display: flex;
flex-direction: column;
height: 100vh;
gap: 0;
}
.ryu-header {
flex: 0 0 auto;
padding: 8px 16px;
}
.ryu-course label,
.ryu-course .label-wrap {
display: none !important;
}
.ryu-tabs {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.ryu-tabs .tab-content {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 12px;
}
.ryu-tabs .tab-content .tabitem > .form {
height: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.ryu-pane-row {
flex: 1 1 auto;
min-height: 0;
display: flex;
gap: 8px;
}
.ryu-pane-row > .column {
flex: 1 1 50%;
min-width: 0;
display: flex;
flex-direction: column;
}
.ryu-pane-row .block {
height: 100%;
}
.ryu-toolbar {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 12px;
}
.ryu-hint {
font-size: 13px;
padding: 0 8px 8px;
}
.ryu-help {
border-radius: 8px;
padding: 12px 16px;
margin: 0 8px;
}
.ryu-status {
flex: 0 0 auto;
padding: 0 16px;
font-size: 12px;
}
.ryu-center {
flex: 0 0 auto;
display: flex;
justify-content: center;
padding: 16px;
}
@media (max-width: 768px) {
.ryu-pane-row {
flex-direction: column;
}
}
"""
def _fmt_items(items: List[Dict[str, Any]], mode: str) -> str:
if not items:
return ""
if mode == "keywords":
return "\n".join(
f"{it.get('word', '')} ({it.get('reading', '')}) — {it.get('meaning', '')}".strip(" —")
for it in items[:20]
)
if mode == "exam":
parts = []
for i, it in enumerate(items[:10], 1):
body = ""
if it.get("topic"):
body += f"Topic: {it['topic']}\n"
if it.get("question"):
body += f"Q: {it['question']}\n"
if it.get("sample_answer"):
body += f"A: {it['sample_answer']}\n"
parts.append(f"[{i}]\n{body.strip()}")
return "\n\n".join(parts)
return ""
def on_translate(source: str) -> str:
if not source.strip():
return ""
out, _ = translate(source)
return out
def on_summarize(source: str) -> str:
if not source.strip():
return ""
out, _ = summarize(source)
return out
def on_easy(source: str) -> str:
if not source.strip():
return ""
out, _ = easy_english(source)
return out
def on_keywords(source: str) -> str:
if not source.strip():
return ""
items, _ = extract_keywords(source)
return _fmt_items(items, "keywords")
def on_exam(source: str) -> str:
if not source.strip():
return ""
items, _ = predict_exam_topics(source)
return _fmt_items(items, "exam")
def on_audio(audio_path: str):
transcript, translation, status = transcribe_and_translate(audio_path)
return transcript, translation, status or ""
def on_image(image):
if image is None:
return "", "", ""
detected, translated = ocr_image(image)
if detected.startswith("OCR error:") or detected == "(no text detected)":
return "", "", detected
return detected, translated, ""
def add_course(name: str, courses: List[Dict[str, Any]]):
if not name or not name.strip():
return courses, None, ""
new_id = str(max([int(c["id"]) for c in courses], default=0) + 1)
courses.append({"id": new_id, "name": name.strip()})
return courses, new_id, f"コース「{name.strip()}」を追加しました。"
def update_dropdown(courses: List[Dict[str, Any]]):
return gr.Dropdown(choices={c["name"]: c["id"] for c in courses}, value=courses[-1]["id"] if courses else None)
def show_help():
return (
"【使い方】\n"
"• 翻訳:日本語を入力すると自動で英語に翻訳します。\n"
"• 音声:マイクボタンを押して話してください。録音停止後に日本語の文字起こしと英訳が表示されます。\n"
"• 画像翻訳:画像をドラッグ&ドロップまたはクリックしてアップロードすると、写っている日本語を読み取って英訳します。\n"
"• 復習:授業のテキストを貼り付けて、下のボタンから要約・かんたん英語・単語帳・試験予測を生成します。\n"
"• コース:ヘッダーの「新しいコース名」に入力して「追加」を押すと、翻訳履歴を分けて管理するコースを作れます。"
)
def toggle_help(visible: bool):
return not visible, gr.update(visible=not visible)
with gr.Blocks(
title="Ryugaku",
elem_id="ryu-app",
css=CUSTOM_CSS,
theme=gr.themes.Default(),
) as demo:
courses_state = gr.State(value=[{"id": "1", "name": "デフォルト"}])
with gr.Row(elem_classes=["ryu-header"]):
gr.Markdown("## Ryugaku")
with gr.Row(elem_classes=["ryu-course"]):
gr.Markdown("コース:")
course_dropdown = gr.Dropdown(
label="",
choices={"デフォルト": "1"},
value="1",
show_label=False,
container=False,
interactive=True,
scale=1,
allow_custom_value=True,
)
new_course_name = gr.Textbox(
label="",
placeholder="新しいコース名",
show_label=False,
container=False,
scale=1,
)
add_course_btn = gr.Button("追加", scale=0)
help_visible = gr.State(False)
help_btn = gr.Button("? 使い方", scale=0)
help_text = gr.Textbox(
label="",
value=show_help(),
interactive=False,
show_label=False,
container=False,
lines=6,
visible=False,
elem_classes=["ryu-help"],
)
with gr.Tabs(elem_classes=["ryu-tabs"]):
with gr.Tab("翻訳 / Translate"):
gr.Markdown(
'<div class="ryu-hint">日本語を入力すると、右側に英語訳が表示されます。</div>'
)
with gr.Row(elem_classes=["ryu-pane-row"]):
translate_source = gr.Textbox(
label="日本語",
placeholder="ここに日本語を入力または貼り付け...",
lines=18,
show_label=True,
container=True,
scale=1,
)
translate_output = gr.Textbox(
label="English",
placeholder="English translation will appear here...",
interactive=False,
lines=18,
show_label=True,
container=True,
scale=1,
)
with gr.Row(elem_classes=["ryu-toolbar"]):
translate_btn = gr.Button("翻訳", variant="primary")
clear_translate_btn = gr.Button("クリア")
with gr.Row(elem_classes=["ryu-status"]):
translate_status = gr.Textbox(
label="",
interactive=False,
show_label=False,
container=False,
value="",
)
with gr.Tab("音声 / Lecture"):
gr.Markdown(
'<div class="ryu-hint">マイクボタンを押して話してください。録音停止後に文字起こしと英訳が表示されます。</div>'
)
with gr.Row(elem_classes=["ryu-center"]):
lecture_audio = gr.Audio(
sources=["microphone"],
type="filepath",
label="マイクで録音",
show_label=True,
)
with gr.Row(elem_classes=["ryu-pane-row"]):
lecture_ja = gr.Textbox(
label="文字起こし(日本語)",
placeholder="ここに日本語の文字起こしが表示されます...",
lines=12,
show_label=True,
container=True,
scale=1,
)
lecture_en = gr.Textbox(
label="翻訳(English)",
placeholder="English translation will appear here...",
interactive=False,
lines=12,
show_label=True,
container=True,
scale=1,
)
with gr.Row(elem_classes=["ryu-status"]):
lecture_status = gr.Textbox(
label="",
interactive=False,
show_label=False,
container=False,
value="",
)
with gr.Tab("画像翻訳 / Sheet"):
gr.Markdown(
'<div class="ryu-hint">画像をドラッグ&ドロップするか、枠内をクリックしてアップロードしてください。写っている日本語を読み取って英訳します。</div>'
)
with gr.Row(elem_classes=["ryu-center"]):
sheet_image = gr.Image(
type="pil",
label="画像をアップロード",
show_label=True,
sources=["upload"],
)
with gr.Row(elem_classes=["ryu-pane-row"]):
sheet_ja = gr.Textbox(
label="読み取った日本語",
placeholder="ここにOCRで読み取った日本語が表示されます...",
lines=12,
show_label=True,
container=True,
scale=1,
)
sheet_en = gr.Textbox(
label="翻訳(English)",
placeholder="English translation will appear here...",
interactive=False,
lines=12,
show_label=True,
container=True,
scale=1,
)
with gr.Row(elem_classes=["ryu-status"]):
sheet_status = gr.Textbox(
label="",
interactive=False,
show_label=False,
container=False,
value="",
)
with gr.Tab("復習 / Review"):
gr.Markdown(
'<div class="ryu-hint">授業のテキストを貼り付けて、下のボタンから復習コンテンツを生成してください。</div>'
)
with gr.Row(elem_classes=["ryu-review-input"]):
review_text = gr.Textbox(
label="授業のテキスト",
placeholder="ここに授業の原稿やノートを貼り付け...",
lines=8,
show_label=True,
container=True,
scale=1,
)
with gr.Row(elem_classes=["ryu-toolbar"]):
summary_btn = gr.Button("要約")
easy_btn = gr.Button("かんたん英語")
keywords_btn = gr.Button("単語帳")
exam_btn = gr.Button("試験予測")
clear_review_btn = gr.Button("クリア")
with gr.Row(elem_classes=["ryu-result"]):
review_output = gr.Textbox(
label="結果",
lines=10,
interactive=False,
show_label=True,
container=True,
placeholder="ここに要約・かんたん英語・単語帳・試験予測が表示されます...",
)
# Events
translate_source.change(fn=on_translate, inputs=translate_source, outputs=translate_output)
translate_btn.click(fn=on_translate, inputs=translate_source, outputs=translate_output)
clear_translate_btn.click(
fn=lambda: ("", ""),
outputs=[translate_source, translate_output],
)
lecture_audio.change(
fn=on_audio,
inputs=lecture_audio,
outputs=[lecture_ja, lecture_en, lecture_status],
)
sheet_image.change(
fn=on_image,
inputs=sheet_image,
outputs=[sheet_ja, sheet_en, sheet_status],
)
summary_btn.click(fn=on_summarize, inputs=review_text, outputs=review_output)
easy_btn.click(fn=on_easy, inputs=review_text, outputs=review_output)
keywords_btn.click(fn=on_keywords, inputs=review_text, outputs=review_output)
exam_btn.click(fn=on_exam, inputs=review_text, outputs=review_output)
clear_review_btn.click(
fn=lambda: ("", ""),
outputs=[review_text, review_output],
)
add_course_btn.click(
fn=add_course,
inputs=[new_course_name, courses_state],
outputs=[courses_state, course_dropdown, translate_status],
).then(
fn=update_dropdown,
inputs=courses_state,
outputs=course_dropdown,
)
help_btn.click(
fn=toggle_help,
inputs=help_visible,
outputs=[help_visible, help_text],
)
if __name__ == "__main__":
port = int(os.environ.get("PORT", "7860"))
demo.launch(server_name="0.0.0.0", server_port=port)