"""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( '