Spaces:
Running
Running
| 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() | |