Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import json | |
| import os | |
| import csv | |
| import io | |
| from datetime import datetime | |
| from huggingface_hub import HfApi, hf_hub_download | |
| import tempfile | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") | |
| DATASET_REPO = os.environ.get("DATASET_REPO", "") | |
| RESULTS_FILE = "results.csv" | |
| AUDIO_ITEMS = [ | |
| { | |
| "id": "sample_01", | |
| "audio_path": "audios/sample1.wav", | |
| "transcript": "She opened the door and walked inside.", | |
| "stress_words": ["opened", "walked"], | |
| }, | |
| { | |
| "id": "sample_02", | |
| "audio_path": "audios/sample2.wav", | |
| "transcript": "The storm destroyed every single house on the street.", | |
| "stress_words": ["destroyed", "every", "street"], | |
| }, | |
| { | |
| "id": "sample_03", | |
| "audio_path": "audios/sample3.wav", | |
| "transcript": "I never said she stole the money.", | |
| "stress_words": ["never", "stole"], | |
| }, | |
| ] | |
| TOTAL = len(AUDIO_ITEMS) | |
| _theme = gr.themes.Default( | |
| primary_hue="gray", | |
| neutral_hue="slate", | |
| font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "sans-serif"], | |
| ) | |
| _css = """ | |
| body, .gradio-container { | |
| font-family: 'Inter', sans-serif !important; | |
| } | |
| .gradio-container { | |
| max-width: 820px !important; | |
| margin: 0 auto; | |
| } | |
| #card, #done-card { | |
| background: #ffffff; | |
| border: 1px solid #e5e7eb; | |
| border-radius: 12px; | |
| padding: 24px; | |
| } | |
| #next-btn { | |
| background: #ffffff !important; | |
| color: #111827 !important; | |
| border: 1px solid #d1d5db !important; | |
| border-radius: 8px !important; | |
| font-weight: 600 !important; | |
| } | |
| #next-btn:hover { | |
| background: #f9fafb !important; | |
| } | |
| #result-box { | |
| border-radius: 10px; | |
| } | |
| """ | |
| def highlight_transcript(transcript: str, stress_words: list) -> str: | |
| words = transcript.split() | |
| sw_lower = [w.lower().strip(".,!?;:") for w in stress_words] | |
| highlighted = [] | |
| for w in words: | |
| clean = w.lower().strip(".,!?;:") | |
| if clean in sw_lower: | |
| highlighted.append( | |
| f'<span style="color:#dc2626;font-weight:700;">{w}</span>' | |
| ) | |
| else: | |
| highlighted.append(w) | |
| return " ".join(highlighted) | |
| def build_transcript_html(item: dict) -> str: | |
| stress_list = ", ".join( | |
| [f'<span style="color:#dc2626;font-weight:700;">{w}</span>' for w in item["stress_words"]] | |
| ) | |
| body = highlight_transcript(item["transcript"], item["stress_words"]) | |
| return ( | |
| '<div style="background:#ffffff;border:1px solid #e5e7eb;border-radius:10px;' | |
| 'padding:18px 20px;margin-bottom:8px;">' | |
| '<p style="margin:0 0 8px;font-size:0.82rem;color:#6b7280;' | |
| 'text-transform:uppercase;letter-spacing:.05em;font-weight:600;">Transcript</p>' | |
| f'<p style="margin:0 0 14px;font-size:1.02rem;line-height:1.7;color:#111827;">{body}</p>' | |
| '<p style="margin:0;font-size:0.82rem;color:#6b7280;text-transform:uppercase;' | |
| 'letter-spacing:.05em;font-weight:600;">Stress words</p>' | |
| f'<p style="margin:4px 0 0;font-size:0.95rem;color:#111827;">{stress_list}</p>' | |
| '</div>' | |
| ) | |
| def progress_html(idx: int) -> str: | |
| pct = int(idx / TOTAL * 100) | |
| return ( | |
| '<div>' | |
| '<div style="display:flex;justify-content:space-between;margin-bottom:6px;">' | |
| f'<span style="font-size:.85rem;color:#6b7280;">Audio {idx} of {TOTAL}</span>' | |
| f'<span style="font-size:.85rem;color:#111827;font-weight:600;">{pct}%</span>' | |
| '</div>' | |
| '<div style="background:#e5e7eb;border-radius:999px;height:6px;">' | |
| f'<div style="background:#111827;width:{pct}%;height:6px;border-radius:999px;"></div>' | |
| '</div></div>' | |
| ) | |
| def push_results_to_hub(rows: list) -> str: | |
| if not HF_TOKEN or not DATASET_REPO: | |
| return "Results were not saved because HF_TOKEN or DATASET_REPO is missing." | |
| api = HfApi(token=HF_TOKEN) | |
| fieldnames = [ | |
| "timestamp", "evaluator_id", "audio_id", "transcript", | |
| "stress_words", "naturalness", "stress_quality", "comment", | |
| ] | |
| existing_rows = [] | |
| try: | |
| path = hf_hub_download( | |
| repo_id=DATASET_REPO, | |
| filename=RESULTS_FILE, | |
| repo_type="dataset", | |
| token=HF_TOKEN, | |
| ) | |
| with open(path, newline="", encoding="utf-8") as f: | |
| existing_rows = list(csv.DictReader(f)) | |
| except Exception: | |
| pass | |
| all_rows = existing_rows + rows | |
| buf = io.StringIO() | |
| writer = csv.DictWriter(buf, fieldnames=fieldnames) | |
| writer.writeheader() | |
| writer.writerows(all_rows) | |
| with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False, encoding="utf-8") as tmp: | |
| tmp.write(buf.getvalue()) | |
| tmp_path = tmp.name | |
| api.upload_file( | |
| path_or_fileobj=tmp_path, | |
| path_in_repo=RESULTS_FILE, | |
| repo_id=DATASET_REPO, | |
| repo_type="dataset", | |
| commit_message=f"Add {len(rows)} evaluation(s) — {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')}", | |
| ) | |
| os.unlink(tmp_path) | |
| return f"Saved {len(rows)} responses to {DATASET_REPO}/{RESULTS_FILE}" | |
| def make_app(): | |
| with gr.Blocks(title="TTS Evaluation Survey") as demo: | |
| idx_state = gr.State(0) | |
| answers_state = gr.State([]) | |
| gr.HTML(""" | |
| <div style="text-align:center;padding:12px 0 8px;"> | |
| <h1 style="margin:0;font-size:1.5rem;font-weight:700;color:#1d4ed8;">TTS Evaluation Survey</h1> | |
| <p style="margin:6px 0 0;color:#6b7280;font-size:.95rem;"> | |
| Listen to each sample and rate naturalness and stress quality from 1 to 5. | |
| </p> | |
| </div> | |
| """) | |
| evaluator_id = gr.Textbox( | |
| label="Your name / evaluator ID", | |
| placeholder="e.g. listener_01", | |
| max_lines=1, | |
| ) | |
| progress_bar = gr.HTML(value=progress_html(0)) | |
| with gr.Group(elem_id="card", visible=True) as eval_card: | |
| audio_label = gr.HTML( | |
| value=( | |
| f'<p style="font-size:.82rem;font-weight:600;color:#6b7280;' | |
| f'text-transform:uppercase;letter-spacing:.05em;margin:0 0 8px;">' | |
| f'Audio sample 1 / {TOTAL}</p>' | |
| ) | |
| ) | |
| transcript_box = gr.HTML(value=build_transcript_html(AUDIO_ITEMS[0])) | |
| audio_player = gr.Audio( | |
| value=AUDIO_ITEMS[0]["audio_path"], | |
| label="Listen", | |
| interactive=False, | |
| ) | |
| naturalness = gr.Radio( | |
| choices=["1", "2", "3", "4", "5"], | |
| label="Naturalness (1 = poor, 5 = excellent)", | |
| ) | |
| stress_quality = gr.Radio( | |
| choices=["1", "2", "3", "4", "5"], | |
| label="Stress quality (1 = poor, 5 = excellent)", | |
| ) | |
| comment = gr.Textbox( | |
| label="Optional comment", | |
| placeholder="Any remarks about this sample", | |
| lines=2, | |
| ) | |
| next_btn = gr.Button("Save and Next", elem_id="next-btn") | |
| with gr.Group(elem_id="done-card", visible=False) as done_card: | |
| done_html = gr.HTML(value="") | |
| def next_audio(idx, answers, eval_id, nat, stress, cmt): | |
| if nat is None or stress is None: | |
| gr.Warning("Please rate both Naturalness and Stress Quality.") | |
| return ( | |
| idx, answers, gr.update(), gr.update(), gr.update(), gr.update(), | |
| nat, stress, cmt, | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(value=""), | |
| ) | |
| item = AUDIO_ITEMS[idx] | |
| record = { | |
| "timestamp": datetime.utcnow().isoformat(), | |
| "evaluator_id": eval_id.strip() if eval_id else "anonymous", | |
| "audio_id": item["id"], | |
| "transcript": item["transcript"], | |
| "stress_words": "|".join(item["stress_words"]), | |
| "naturalness": nat, | |
| "stress_quality": stress, | |
| "comment": cmt.strip() if cmt else "", | |
| } | |
| new_answers = answers + [record] | |
| new_idx = idx + 1 | |
| if new_idx >= TOTAL: | |
| status = push_results_to_hub(new_answers) | |
| completion_html = ( | |
| '<div style="text-align:center; background:#f8fafc; border:1px solid #dbeafe; ' | |
| 'border-radius:12px; padding:20px 16px;">' | |
| '<h2 style="margin:0 0 8px; color:#1e3a8a; font-size:1.2rem; font-weight:700;">' | |
| 'Evaluation completed</h2>' | |
| '<p style="margin:0 0 10px; color:#334155; font-size:0.98rem;">' | |
| 'All responses have been recorded. Please close the session.</p>' | |
| f'<p style="margin:0; color:#0f172a; font-size:.95rem; font-weight:500;"></p>' | |
| '</div>' | |
| ) | |
| print(json.dumps(new_answers, ensure_ascii=False, indent=2)) | |
| return ( | |
| new_idx, new_answers, progress_html(new_idx), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(value=None), | |
| None, None, "", | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(value=completion_html), | |
| ) | |
| next_item = AUDIO_ITEMS[new_idx] | |
| next_label = ( | |
| f'<p style="font-size:.82rem;font-weight:600;color:#6b7280;' | |
| f'text-transform:uppercase;letter-spacing:.05em;margin:0 0 8px;">' | |
| f'Audio sample {new_idx + 1} / {TOTAL}</p>' | |
| ) | |
| return ( | |
| new_idx, new_answers, progress_html(new_idx), next_label, | |
| build_transcript_html(next_item), next_item["audio_path"], | |
| None, None, "", | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(value=""), | |
| ) | |
| next_btn.click( | |
| fn=next_audio, | |
| inputs=[idx_state, answers_state, evaluator_id, naturalness, stress_quality, comment], | |
| outputs=[ | |
| idx_state, answers_state, progress_bar, | |
| audio_label, transcript_box, audio_player, | |
| naturalness, stress_quality, comment, | |
| eval_card, done_card, done_html, | |
| ], | |
| ) | |
| return demo | |
| demo = make_app() | |
| if __name__ == "__main__": | |
| demo.launch(theme=_theme, css=_css) | |