german_exercise / app.py
gursul's picture
Upload app.py with huggingface_hub
4c10fc7 verified
import random
import json
import os
import gradio as gr
import shutil
from huggingface_hub import upload_file, hf_hub_download, login
DATASET_REPO = "gursul/german_exercise_dataset"
#ENGLISH_WORDS_FILE = "/content/sample_data/en_de.json" # maps to en_de.json
#GERMAN_WORDS_FILE = "/content/sample_data/de_en.json" # maps to de_en.json
ENGLISH_WORDS_FILE = "en_de.json"
GERMAN_WORDS_FILE = "de_en.json"
PASSWORD = os.getenv("ADMIN_PASSWORD")
# Authenticate once at startup
if "HF_TOKEN" in os.environ:
login(token=os.environ["HF_TOKEN"])
def ensure_json_file(path):
if not os.path.exists(path):
with open(path, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False, indent=2) # dict, not list
def load_json(path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_json(path, data: dict):
# keep keys sorted for readability
with open(path, "w", encoding="utf-8") as f:
json.dump(dict(sorted(data.items(), key=lambda kv: kv[0].lower())), f, ensure_ascii=False, indent=2)
def load_english():
return load_json(ENGLISH_WORDS_FILE)
def load_german():
return load_json(GERMAN_WORDS_FILE)
def save_english(d: dict):
save_json(ENGLISH_WORDS_FILE, d)
upload_file(
path_or_fileobj=ENGLISH_WORDS_FILE,
path_in_repo="en_de.json",
repo_id=DATASET_REPO,
repo_type="dataset",
token=os.environ.get("HF_TOKEN")
)
def save_german(d: dict):
save_json(GERMAN_WORDS_FILE, d)
upload_file(
path_or_fileobj=GERMAN_WORDS_FILE,
path_in_repo="de_en.json",
repo_id=DATASET_REPO,
repo_type="dataset",
token=os.environ.get("HF_TOKEN")
)
def new_word():
eng_data = load_english()
if not eng_data:
# keep inputs disabled; show "-" in artikel
return "No words in list!", "", "-", "", gr.update(interactive=False), gr.update(interactive=False)
eng_word = random.choice(list(eng_data.keys()))
meta = eng_data[eng_word]
wtype = meta.get("type", "")
# artikel box: enable for nouns (value ""), disable otherwise (value "-")
artikel_value = "" if wtype == "noun" else "-"
artikel_update = gr.update(interactive=(wtype == "noun"))
# translation box becomes active for the round
german_update = gr.update(interactive=True)
return (
eng_word, # English word shown
wtype, # hidden type
artikel_value, # artikel textbox value
"", # clear translation input
artikel_update, # artikel interactivity
german_update # translation interactivity
)
def check_answer(english, wtype, article_in, german_in):
eng_data = load_english()
ger_data = load_german()
english = (english or "").strip()
german_in = (german_in or "").strip()
article_in = (article_in or "").strip()
meta = eng_data.get(english)
if not meta:
return "⚠ Word not found."
translations = [t.strip() for t in meta.get("translations", [])]
# Case-insensitive compare for membership
translations_ci = {t.lower(): t for t in translations} # map lower->original for nice feedback
target_key = german_in.lower()
if wtype == "noun":
# Must match one of the German translations AND the article must match the German word's article
if target_key in translations_ci:
german_canonical = translations_ci[target_key]
g_entry = ger_data.get(german_canonical, {})
expected_article = (g_entry.get("artikel") or "").strip()
if expected_article and article_in.lower() == expected_article.lower():
return "✅ Correct!"
else:
if expected_article:
return f"❌ Almost. Correct article for **{german_canonical}** is **{expected_article}**."
else:
return f"❌ I couldn't verify the article for **{german_canonical}**. Check your article or add it to the dictionary."
else:
# show all acceptable answers with their articles
options = []
for g in translations:
art = (ger_data.get(g, {}).get("artikel") or "").strip()
options.append(f"{(art + ' ') if art else ''}{g}")
return "❌ Wrong. Acceptable answers: " + ", ".join(options)
else:
# Non-noun: only need the German word to be one of the translations
if target_key in translations_ci:
return "✅ Correct!"
else:
return "❌ Wrong. Acceptable answers: " + ", ".join(translations)
def new_word_de_en():
ger_data = load_german()
if not ger_data:
# keep inputs disabled; show "-" in artikel
return "No words in list!", "", "-", "", gr.update(interactive=False), gr.update(interactive=False)
ger_word = random.choice(list(ger_data.keys()))
meta = ger_data[ger_word]
wtype = meta.get("type", "")
# artikel box: enable for nouns (value ""), disable otherwise (value "-")
artikel_value = "" if wtype == "noun" else "-"
artikel_update = gr.update(interactive=(wtype == "noun"))
# translation box becomes active for the round
english_update = gr.update(interactive=True)
return (
ger_word, # German word shown
wtype, # hidden type
artikel_value, # artikel textbox value
"", # clear translation input
artikel_update, # artikel interactivity
english_update # translation interactivity
)
def check_answer_de_en(german, wtype, artikel_in, english_in):
ger_data = load_german()
german = (german or "").strip()
english_in = (english_in or "").strip()
artikel_in = (artikel_in or "").strip()
meta = ger_data.get(german)
if not meta:
return "⚠ Word not found."
translations = [t.strip() for t in meta.get("translations", [])]
translations_ci = {t.lower(): t for t in translations} # map lower->original
target_key = english_in.lower()
if wtype == "noun":
expected_article = (meta.get("artikel") or "").strip()
if target_key in translations_ci and expected_article and artikel_in.lower() == expected_article.lower():
return "✅ Correct!"
elif target_key in translations_ci and not expected_article:
# We don’t have the article stored; accept translation only.
return "✅ Translation correct (no article recorded in dictionary)."
else:
# Build feedback
if expected_article:
return f"❌ Wrong. Expected **{expected_article}** + one of: {', '.join(translations)}"
else:
return f"❌ Wrong. Acceptable answers: {', '.join(translations)}"
else:
if target_key in translations_ci:
return "✅ Correct!"
else:
return "❌ Wrong. Acceptable answers: " + ", ".join(translations)
def add_word(english, wtype, article, german):
# Normalize inputs (trim spaces)
english = (english or "").strip()
german = (german or "").strip()
article = (article or "").strip()
wtype = (wtype or "").strip()
# Validate
if not english or not german or not wtype:
return "❌ Please fill English, German and Word Type.", get_words_table_en_de(), get_words_table_de_en()
if wtype == "noun" and not article:
return "❌ Please provide the article for nouns.", get_words_table_en_de(), get_words_table_de_en()
eng_data = load_english() # { english_word: {type, translations:[german,...]} }
ger_data = load_german() # { german_word: {artikel, type, translations:[english,...]} }
# --- Update ENGLISH side ---
if english not in eng_data:
eng_data[english] = {
#"artikel": (article if wtype == "noun" else ""),
"type": wtype,
"translations": [german]
}
else:
# keep existing artikel/type (don’t override silently)
if german not in eng_data[english].get("translations", []):
eng_data[english]["translations"].append(german)
if not eng_data[english].get("type"):
eng_data[english]["type"] = wtype
# --- Update GERMAN side ---
if german not in ger_data:
ger_data[german] = {
"artikel": (article if wtype == "noun" else ""),
"type": wtype,
"translations": [english]
}
else:
if english not in ger_data[german].get("translations", []):
ger_data[german]["translations"].append(english)
if wtype == "noun" and not (ger_data[german].get("artikel") or "").strip():
ger_data[german]["artikel"] = article
if not ger_data[german].get("type"):
ger_data[german]["type"] = wtype
save_english(eng_data)
save_german(ger_data)
return "✅ Word(s) added!", get_words_table_en_de(), get_words_table_de_en()
def delete_word(word, lang):
"""
Delete a word from dictionaries.
lang = "en" means the word is an English word.
lang = "de" means the word is a German word.
"""
eng_data = load_english()
ger_data = load_german()
word = (word or "").strip()
if not word:
return "❌ Please provide a word to delete.", get_words_table_en_de(), get_words_table_de_en()
if lang == "English":
if word not in eng_data:
return f"⚠ English word '{word}' not found.", get_words_table_en_de(), get_words_table_de_en()
# remove links from german side
for g in eng_data[word].get("translations", []):
if g in ger_data and word in ger_data[g].get("translations", []):
ger_data[g]["translations"].remove(word)
if not ger_data[g]["translations"]:
del ger_data[g] # remove orphan
del eng_data[word]
elif lang == "Deutsch":
if word not in ger_data:
return f"⚠ German word '{word}' not found.", get_words_table_en_de(), get_words_table_de_en()
# remove links from english side
for e in ger_data[word].get("translations", []):
if e in eng_data and word in eng_data[e].get("translations", []):
eng_data[e]["translations"].remove(word)
if not eng_data[e]["translations"]:
del eng_data[e] # remove orphan
del ger_data[word]
else:
return "❌ Invalid language parameter. Use 'en' or 'de'.", get_words_table_en_de(), get_words_table_de_en()
save_english(eng_data)
save_german(ger_data)
return f"✅ '{word}' deleted.", get_words_table_en_de(), get_words_table_de_en()
def toggle_article_input(word_type):
return gr.update(interactive=(word_type == "noun"))
def get_words_table_en_de(search_query=""):
eng_data = load_english() # English→German (no artikel here)
ger_data = load_german() # German→English (artikel lives here)
filtered = {}
for eng_word, meta in sorted(eng_data.items(), key=lambda kv: kv[0].lower()):
translations = meta.get("translations", [])
wtype = meta.get("type", "")
# check if search matches the word or any translation
if search_query.lower() in eng_word.lower() or any(search_query.lower() in t.lower() for t in translations):
artikel_list = []
for g in translations:
art = (ger_data.get(g, {}).get("artikel") or "").strip()
if art: # only add non-empty
artikel_list.append(art)
# 🔑 only join if there are real articles
artikel_text = ", ".join(artikel_list) if artikel_list else ""
translations_text = ", ".join(translations)
filtered[eng_word] = [eng_word, artikel_text, translations_text, wtype]
return list(filtered.values())
def get_words_table_de_en(search_query=""):
ger_data = load_german() # Load fresh
filtered = {}
for ger_word, meta in sorted(ger_data.items(), key=lambda kv: kv[0].lower()):
translations = meta.get("translations", [])
# check if search matches the word or any translation
if search_query.lower() in ger_word.lower() or any(search_query.lower() in t.lower() for t in translations):
artikel = meta.get("artikel", "") or "" # ✅ Bug 1: no null
translations_text = ", ".join(translations) # ✅ Bug 2: join translations
filtered[ger_word] = [ger_word, artikel, translations_text, meta.get("type", "")]
return list(filtered.values())
def reset_filter():
return "", get_words_table_en_de(), get_words_table_de_en()
def sync_from_hf():
en_de_cache_path = hf_hub_download(repo_id=DATASET_REPO, filename="en_de.json", repo_type="dataset")
de_en_cache_path = hf_hub_download(repo_id=DATASET_REPO, filename="de_en.json", repo_type="dataset")
#os.rename("en_de.json", ENGLISH_WORDS_FILE)
#os.rename("de_en.json", GERMAN_WORDS_FILE)
# Copy to local Space root so the app can access them
shutil.copy(en_de_cache_path, ENGLISH_WORDS_FILE)
shutil.copy(de_en_cache_path, GERMAN_WORDS_FILE)
sync_from_hf()
ensure_json_file(ENGLISH_WORDS_FILE)
ensure_json_file(GERMAN_WORDS_FILE)
eng_data = load_english()
ger_data = load_german()
MEME_PATH = hf_hub_download(
repo_id=DATASET_REPO,
filename="gradio_meme.jpg",
repo_type="dataset"
)
# Password unlock
def unlock_section(pwd, current_auth):
if current_auth:
return (
current_auth,
"",
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
"✅ Access Already Granted!"
)
if pwd == PASSWORD:
return (
True,
"",
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
gr.update(interactive=True),
"✅ Access Granted! You can now add/delete words."
)
else:
return (
False,
pwd,
gr.update(), gr.update(), gr.update(),
gr.update(), gr.update(),
gr.update(), gr.update(), gr.update(),
"❌ Wrong Password! Try again."
)
# Gradio UI
with gr.Blocks() as demo:
authenticated = gr.State(False) # Session-based state
with gr.Tab("Word Translation"):
with gr.Row():
# ========= EN → DE =========
with gr.Column():
gr.Markdown("### English → German")
word_display = gr.Textbox(label="English Word", interactive=False)
word_type = gr.Textbox(visible=False)
# always visible; start disabled; show "-" until a noun appears
article_box = gr.Textbox(label="Artikel (der/die/das)", value="-", visible=True, interactive=False)
# start disabled until New Word
german_box = gr.Textbox(label="German Translation", visible=True, interactive=False)
# initial message
result = gr.Markdown("Let's play!")
new_btn = gr.Button("New Word (EN→DE)")
submit_btn = gr.Button("Check Answer (EN→DE)")
# ========= DE → EN =========
with gr.Column():
gr.Markdown("### German → English")
de_word_display = gr.Textbox(label="German Word", interactive=False)
de_word_type = gr.Textbox(visible=False)
# always visible; start disabled; show "-" until a noun appears
de_artikel_box = gr.Textbox(label="Artikel (der/die/das)", value="-", visible=True, interactive=False)
# start disabled until New Word
de_english_box = gr.Textbox(label="English Translation", visible=True, interactive=False)
# initial message
de_result = gr.Markdown("Let's play!")
de_new_btn = gr.Button("New Word (DE→EN)")
de_submit_btn = gr.Button("Check Answer (DE→EN)")
with gr.Tab("Words Dataset"):
with gr.Row(equal_height=True):
login_password = gr.Textbox(label="Enter Password", type="password", scale=5)
login_btn = gr.Button("Unlock Add/Delete Section", scale=1)
login_status = gr.Markdown("Are you the one who holds the key?")
with gr.Row():
with gr.Column():
english_in = gr.Textbox(label="English Word", interactive=False)
type_in = gr.Dropdown(label="Word Type", choices=["noun", "verb", "other"], value="noun", interactive=False)
article_in = gr.Textbox(label="Artikel (der/die/das) — only for nouns", interactive=False)
german_in = gr.Textbox(label="German Translation", interactive=False)
add_btn = gr.Button("Add Word", interactive=False)
add_result = gr.Markdown("Let's add some words!!!")
with gr.Column():
meme_img = gr.Image(
value=MEME_PATH,
label="Fun Meme",
show_download_button=False,
show_label=False,
show_share_button=False,
show_fullscreen_button=False,
height=159, #320x215
width=320*(159/215)-1
)
delete_word_in = gr.Textbox(label="Word to Delete", interactive=False)
delete_lang_in = gr.Dropdown(label="Language", choices=["English", "Deutsch"], value="English", interactive=False)
delete_btn = gr.Button("Delete Word", interactive=False)
delete_result = gr.Markdown("So you're deleting me...")
with gr.Row(equal_height=True):
search_box = gr.Textbox(label="Search words", placeholder="Type to filter...", scale=5)
reset_btn = gr.Button("Reset Filter", scale=1)
with gr.Row():
# English → German table
word_table_en_de = gr.DataFrame(
headers=["English", "Article", "German", "Type"],
#value=get_words_table_en_de(),
value=[],
interactive=False,
wrap=True,
max_height=300
)
# German → English table
word_table_de_en = gr.DataFrame(
headers=["German", "Article", "English", "Type"],
#value=get_words_table_de_en(),
value=[],
interactive=False,
wrap=True,
max_height=300
)
demo.load(
fn=lambda: (get_words_table_en_de(), get_words_table_de_en()),
inputs=None,
outputs=[word_table_en_de, word_table_de_en]
)
new_btn.click(
fn=new_word,
outputs=[word_display, word_type, article_box, german_box,
article_box, german_box]
)
submit_btn.click(
fn=check_answer,
inputs=[word_display, word_type, article_box, german_box],
outputs=result
)
de_new_btn.click(
fn=new_word_de_en,
outputs=[de_word_display, de_word_type, de_artikel_box, de_english_box, de_artikel_box, de_english_box]
)
de_submit_btn.click(
fn=check_answer_de_en,
inputs=[de_word_display, de_word_type, de_artikel_box, de_english_box],
outputs=de_result
)
add_btn.click(
fn=add_word,
inputs=[english_in, type_in, article_in, german_in],
outputs=[add_result, word_table_en_de, word_table_de_en]
)
delete_btn.click(
fn=delete_word,
inputs=[delete_word_in, delete_lang_in],
outputs=[delete_result, word_table_en_de, word_table_de_en]
)
type_in.change(
fn=toggle_article_input,
inputs=type_in,
outputs=article_in
)
search_box.change(
fn=lambda q: (get_words_table_en_de(q), get_words_table_de_en(q)),
inputs=search_box,
outputs=[word_table_en_de, word_table_de_en]
)
reset_btn.click(
fn=reset_filter,
outputs=[search_box, word_table_en_de, word_table_de_en]
)
login_btn.click(
unlock_section,
inputs=[login_password, authenticated],
outputs=[
authenticated,
login_password,
english_in, type_in, article_in, german_in, add_btn,
delete_word_in, delete_lang_in, delete_btn,
login_status
]
)
demo.launch()