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()