import gradio as gr import csv import os import shutil from datetime import datetime from huggingface_hub import HfApi # --- CONFIGURATION --- # 1. SECURITY HF_TOKEN = os.getenv("HF_TOKEN") BACKUP_REPO_NAME = "kinyarwanda-voice-backup" # 2. STORAGE DATA_ROOT = "/data" if os.path.exists("/data") else "local_data" AUDIO_FOLDER = os.path.join(DATA_ROOT, "clips") CSV_FILE = os.path.join(DATA_ROOT, "metadata.csv") TSV_FILE = os.path.join(DATA_ROOT, "validated.tsv") os.makedirs(AUDIO_FOLDER, exist_ok=True) # 3. SCRIPTS (Anonymized & Improved) SCRIPTS = [ "Nagerageje kohereza amafaranga kuri MoMo, ariko rezo yari mbi cyane sinabasha kubona mesaje yemeza ko yagiye.", "Mbere yo kwinjira muri mudasobwa, banza urebe niba sisitemu yakoze update, hanyuma ushyiremo ijambo ry'ibanga kugira ngo ufungure.", "Nta megabayiti zihagije mfite muri telefoni, reka nshakishe wifi hano hafi kugira ngo mbashe gukurura iyo porogaramu vuba.", "Iyi sharijeri ya smartphone iragura ibihumbi bitanu, ariko niba ushaka n'ibirahure bya ecran, turaguha byose ku icumi.", "Nugera kuri banki i Nyarugenge, ubwire ejenti agufashe kubikuza ayo madolari mbere y'uko ukwezi kwa Gicurasi kurangira.", "Wohereze imeli itarimo amakosa kugira ngo ubashe guhindura mot de passe, hanyuma ukande kuri linki iri bube kuri ecran yawe.", "Ugomba gusiba amafoto adakenewe kugira ngo ubone aho kubika izindi gigabayiti ebyiri, cyangwa ukoreshe flash.", "Koresha Mokash wishyure iyo fagitire, kode ni zeru gatatu rimwe, ubundi tujye Gasabo ku wa Gatanu.", "Koresha kibodi wandike ubutumwa, hanyuma uwohereze kuri WhatsApp kuko interineti yo mu Majyaruguru iragenda buhoro.", "Nurangiza gukoresha iyo apurikasiyo, wibuke gukora log out kugira ngo hatagira undi umukiriya ureba amabanga yawe." ] # --- HELPER FUNCTIONS --- def generate_tsv(): """Generates the Standard Mozilla 'validated.tsv' for the backup.""" cv_header = ["client_id", "path", "sentence", "up_votes", "down_votes", "age", "gender", "accent", "locale", "segment"] tsv_content = [] tsv_content.append("\t".join(cv_header)) if os.path.exists(CSV_FILE): with open(CSV_FILE, 'r', encoding='utf-8') as f: reader = csv.reader(f) try: next(reader, None) for row in reader: if len(row) < 5: continue participant_id, age, gender, accent = row[0], row[2], row[3].lower(), row[4] for i in range(len(SCRIPTS)): file_index = 5 + i if file_index < len(row): filename = row[file_index] if filename: tsv_row = [ participant_id, filename, SCRIPTS[i], "1", "0", age, gender, accent, "rw", "" ] tsv_content.append("\t".join(tsv_row)) except StopIteration: pass with open(TSV_FILE, 'w', encoding='utf-8') as f: f.write("\n".join(tsv_content)) def backup_to_dataset_repo(repo_name=BACKUP_REPO_NAME): """Backs up data to Hugging Face Dataset.""" if not HF_TOKEN: print("⚠️ Backup Skipped: HF_TOKEN missing.") return generate_tsv() api = HfApi(token=HF_TOKEN) user = api.whoami()["name"] full_repo_id = f"{user}/{repo_name}" try: api.create_repo(repo_id=full_repo_id, repo_type="dataset", private=True, exist_ok=True) api.upload_folder( folder_path=DATA_ROOT, repo_id=full_repo_id, repo_type="dataset", path_in_repo="data", commit_message=f"Auto-backup {datetime.now().strftime('%Y-%m-%d %H:%M')}" ) print(f"✅ Auto-Backup successful to {full_repo_id}") except Exception as e: print(f"⚠️ Backup Failed: {e}") def save_data(age, gender, region, *audios): """ Saves data locally. Includes specific error checking to tell user WHICH audio is missing. """ # 1. Identify missing recordings missing_indices = [] for i, audio in enumerate(audios): if audio is None: missing_indices.append(str(i+1)) # Store 1-based index if missing_indices: missing_str = ", ".join(missing_indices) error_msg = f"⚠️ Error: You missed Sentence(s): {missing_str}. Please record them." return f"{error_msg}", gr.update(visible=True), gr.update(visible=True) # 2. Generate Filenames & Save timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") participant_id = f"user_{timestamp}" filenames = [] for index, audio_path in enumerate(audios): filename = f"{participant_id}_s{index+1}.wav" save_path = os.path.join(AUDIO_FOLDER, filename) shutil.copy(audio_path, save_path) filenames.append(filename) # 3. Append to CSV file_exists = os.path.exists(CSV_FILE) with open(CSV_FILE, mode='a', newline='', encoding='utf-8') as f: writer = csv.writer(f) if not file_exists: header = ["ID", "Timestamp", "Age_Range", "Gender", "Accent_Region"] + [f"File_{i+1}" for i in range(len(audios))] writer.writerow(header) writer.writerow([participant_id, timestamp, age, gender, region] + filenames) return f"✅ Saved {participant_id}.", gr.update(visible=False), gr.update(visible=True) def transition_to_recorder(age, gender, region): if not age or not gender or not region: return gr.update(visible=True), gr.update(visible=False), "⚠️ Please fill in all fields." return gr.update(visible=False), gr.update(visible=True), "" # --- UI BUILDER --- # CSS: System Sans-Serif Stack (Fastest & Cleanest) system_sans_css = """ body, .gradio-container { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !important; } button, input, textarea, span, div, label, p, h1, h2, h3 { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !important; } """ with gr.Blocks(title="Improve Kinyarwanda AI", css=system_sans_css, theme=gr.themes.Soft()) as demo: gr.Markdown("# 🇷🇼 Improve Kinyarwanda AI") # PAGE 1: METADATA with gr.Column(visible=True) as metadata_page: gr.Markdown("### Intambwe 1: Umwirondoro (Step 1: Profile)") with gr.Group(): age_input = gr.Dropdown( choices=["teens", "twenties", "thirties", "fourties", "fifties"], label="Age Range (Icyiciro cy'imyaka)", value="twenties" ) gender_input = gr.Radio(["Male", "Female"], label="Gender", value="Female") region_input = gr.Dropdown( ["Kigali City", "Musanze", "Rubavu", "Eastern", "Southern", "Diaspora"], label="Accent Origin", value="Kigali City" ) error_msg = gr.Markdown("") next_btn = gr.Button("Komeza (Next) ➡️", variant="primary") # PAGE 2: RECORDING with gr.Column(visible=False) as recording_page: gr.Markdown(f"### Intambwe 2: Fata Amajwi {len(SCRIPTS)} (Step 2: Record {len(SCRIPTS)} Sentences)") audio_inputs = [] for i, text in enumerate(SCRIPTS): gr.Markdown(f"**{i+1}. {text}**") audio = gr.Audio(sources=["microphone"], type="filepath", label=f"Sentence {i+1}", scale=3) audio_inputs.append(audio) submit_btn = gr.Button("Ohereza Byose (Submit All)", variant="primary") status_msg = gr.Label(label="Status") # PAGE 3: SUCCESS with gr.Column(visible=False) as success_page: gr.Markdown("# ✅ Murakoze!") gr.Button("Refresh Page to Start New").click(None, js="window.location.reload()") # --- WIRING --- next_btn.click(fn=transition_to_recorder, inputs=[age_input, gender_input, region_input], outputs=[metadata_page, recording_page, error_msg]) # 1. Save Local save_event = submit_btn.click( fn=save_data, inputs=[age_input, gender_input, region_input] + audio_inputs, outputs=[status_msg, recording_page, success_page] ) # 2. Auto-Backup to Cloud (Background) save_event.then( fn=lambda: backup_to_dataset_repo(), inputs=None, outputs=None ) if __name__ == "__main__": demo.launch()