kathiasi commited on
Commit
842bbce
·
verified ·
1 Parent(s): f2542fa

Initiation

Browse files
.gitattributes CHANGED
@@ -33,3 +33,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ sample-audios/sorsamisk_-_115_01_-_Goevten_voestes_biejjie_015_024_s.wav filter=lfs diff=lfs merge=lfs -text
37
+ sample-audios/sorsamisk_-_7_01_-_Jarkoestidh_028_020.wav filter=lfs diff=lfs merge=lfs -text
38
+ sample-audios/sorsamisk_-_III_01_-_Giesie_eejehtimmiebiejjieh_015_015.wav filter=lfs diff=lfs merge=lfs -text
39
+ sample-audios/sorsamisk_goltelidh_jupmelen_rihjke_lea_gietskesne_cd1_mono_022_009.wav filter=lfs diff=lfs merge=lfs -text
40
+ sample-audios/sorsamisk_goltelidh_jupmelen_rihjke_lea_gietskesne_cd1_mono_030_014.wav filter=lfs diff=lfs merge=lfs -text
41
+ sample-audios/sorsamisk_goltelidh_jupmelen_rihjke_lea_gietskesne_cd2_009_TRN_019_s.wav filter=lfs diff=lfs merge=lfs -text
42
+ sample-audios/sorsamisk_goltelidh_jupmelen_rihjke_lea_gietskesne_cd2_009_TRN_019.wav filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,12 +1,47 @@
1
  ---
2
- title: Nb Tts Rubric
3
- emoji: 📉
4
- colorFrom: pink
5
- colorTo: pink
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
 
 
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Tts Online Rubric
3
+ emoji: 🌍
4
+ colorFrom: indigo
5
+ colorTo: red
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
+ license: cc-by-4.0
11
+ short_description: A rubric for choosing a good TTS voice
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
15
+
16
+ ## Saving responses to a private Hugging Face dataset
17
+
18
+ This project can optionally push saved responses to a private dataset on the Hugging Face Hub. The feature is disabled by default and only active when the following environment variables are set:
19
+
20
+ - `HF_TOKEN` — a Hugging Face access token with dataset write permissions.
21
+ - `HF_DATASET_ID` — repo id like `your-username/my-eval-responses`.
22
+
23
+ Setup:
24
+
25
+ 1. Create a token on https://huggingface.co/settings/tokens and grant it write permissions for datasets/repos.
26
+ 2. Export the variables locally before running the app:
27
+
28
+ ```bash
29
+ export HF_TOKEN="hf_...your_token..."
30
+ export HF_DATASET_ID="your-username/my-eval-responses"
31
+ ```
32
+
33
+ 3. Install dependencies and run the app:
34
+
35
+ ```bash
36
+ pip install -r requirements.txt
37
+ python app.py
38
+ # open http://localhost:7860 and submit ratings via the UI
39
+ ```
40
+
41
+ Notes:
42
+
43
+ - If the `datasets` or `huggingface_hub` packages are not installed, HF pushing is skipped gracefully and responses are still written to `responses.csv`.
44
+ - Pushing audio files will use Git LFS on the Hub; monitor your account storage and LFS quota.
45
+ - Each UI save triggers a small push (single-record commit). For heavy usage consider batching pushes instead.
46
+
47
+ There is also a small test script included to programmatically verify pushing a single sample row to your dataset. See `hf_push_test.py`.
README_INSTRUCTIONS.md ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TTS Online Rubric (Gradio)
2
+
3
+ This is a minimal Gradio-based UI for running TTS rubric evaluations. It's designed to be deployed on Hugging Face Spaces or run locally.
4
+
5
+ Features:
6
+ - Browse audio files from `sample-audios/`.
7
+ - Play reference audio and upload system outputs to compare.
8
+ - Rate outputs on three sliders (nativeness, naturalness, overall quality) and add comments.
9
+ - Responses are appended to `responses.csv`.
10
+
11
+ How to run locally:
12
+
13
+ 1. Create a Python environment and install requirements:
14
+
15
+ ```bash
16
+ python -m venv .venv
17
+ source .venv/bin/activate
18
+ pip install -r requirements.txt
19
+ ```
20
+
21
+ 2. Add your reference audio files under `sample-audios/`.
22
+
23
+ 3. Run the app:
24
+
25
+ ```bash
26
+ python app.py
27
+ ```
28
+
29
+ 4. Open the URL shown in the terminal (default http://127.0.0.1:7860).
30
+
31
+ Deploying to Hugging Face Spaces:
32
+
33
+ - Create a new Space using the Gradio SDK (python) and push this repo. Ensure `requirements.txt` is present. The app will run automatically.
34
+
35
+ Notes and next steps:
36
+ - The current UI accepts system outputs via file upload. You may prefer a fixed set of system files stored in a directory for side-by-side playback.
37
+ - Consider adding authentication or locking to avoid duplicate/corrupt CSV writes under heavy load.
Rubric for choosing a TTS voice.docx ADDED
Binary file (9.11 kB). View file
 
app.py ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ import gradio as gr
4
+ import os
5
+ import csv
6
+ import fcntl
7
+ from datetime import datetime
8
+ import uuid
9
+ import yaml # You need to install this: pip install pyyaml
10
+ import glob
11
+ import random
12
+ import json
13
+ import pandas as pd
14
+ import io
15
+
16
+ # --- Hugging Face Functionality Notes ---
17
+ # To save results to a private Hugging Face dataset, you must:
18
+ # 1. Install the required libraries: pip install huggingface_hub datasets
19
+ # 2. Set the following environment variables before running the script:
20
+ # - HF_TOKEN: Your Hugging Face access token with write permissions.
21
+ # - HF_DATASET_ID: The ID of the private dataset repo (e.g., "username/my-dataset").
22
+ # If these are not set, saving to HF Hub will be skipped.
23
+
24
+ # --- Start of Local Mode Implementation ---
25
+ IS_LOCAL_MODE = os.environ.get("GRADIO_LOCAL_MODE", "false").lower() in ["true", "1"]
26
+
27
+ if IS_LOCAL_MODE:
28
+ print("Running in LOCAL mode. Hugging Face functionalities are disabled.")
29
+ HfApi = None
30
+ else:
31
+ try:
32
+ from huggingface_hub import HfApi, hf_hub_download
33
+ print("Hugging Face libraries found. HF push functionality is available.")
34
+ except ImportError:
35
+ print("Hugging Face libraries not found. HF push functionality will be disabled.")
36
+ HfApi = None
37
+ # --- End of Local Mode Implementation ---
38
+
39
+ # --- Configuration Loading ---
40
+ def load_config(config_path='config.yaml'):
41
+ """Loads the UI and criteria configuration from a YAML file."""
42
+ try:
43
+ with open(config_path, 'r', encoding='utf-8') as f:
44
+ config = yaml.safe_load(f)
45
+ if 'criteria' not in config or not isinstance(config['criteria'], list):
46
+ raise ValueError("Config must contain a list of 'criteria'.")
47
+ return config
48
+ except FileNotFoundError:
49
+ return None
50
+ except Exception as e:
51
+ print(f"ERROR: Could not parse {config_path}: {e}")
52
+ return None
53
+
54
+ def find_config_files():
55
+ """Finds all .yaml and .yml files in the root directory."""
56
+ return glob.glob("*.yaml") + glob.glob("*.yml")
57
+
58
+ # --- Static & File I/O Functions ---
59
+ OUTPUT_CSV = "responses.csv"
60
+ MAX_CRITERIA = 15 # Maximum number of sliders to support
61
+
62
+ def list_samples(samples_dir):
63
+ """Lists audio files from a specified directory."""
64
+ if not os.path.isdir(samples_dir):
65
+ print(f"WARNING: Samples directory '{samples_dir}' not found.")
66
+ return []
67
+ files = [f for f in os.listdir(samples_dir) if f.lower().endswith(('.wav', '.mp3', '.ogg', '.flac'))]
68
+ files.sort()
69
+ return files
70
+
71
+ def save_responses_to_hf(rows, repo_id: str | None = None, token: str | None = None):
72
+ """
73
+ Append new rows to a CSV file in a private Hugging Face dataset.
74
+
75
+ - Reads the existing CSV (if present).
76
+ - Appends new rows.
77
+ - Uploads the updated file back to the repo.
78
+
79
+ Each 'row' should be a dict with consistent keys.
80
+
81
+ NOTE:
82
+ - Replaces the entire CSV on each update (no true append on the server side).
83
+ - Use small/medium datasets; large ones should use the `datasets` library instead.
84
+ """
85
+ if HfApi is None:
86
+ return {"status": "hf_unavailable", "reason": "missing_packages"}
87
+
88
+ token = token or os.environ.get("HF_TOKEN")
89
+ repo_id = repo_id or os.environ.get("HF_DATASET_ID")
90
+ if not token or not repo_id:
91
+ return {"status": "hf_skipped", "reason": "missing_token_or_repo_env"}
92
+
93
+ api = HfApi(token=token)
94
+ path_in_repo = "data/responses.csv" # fixed CSV location in repo
95
+ repo_err = None
96
+
97
+ # Ensure dataset exists
98
+ try:
99
+ api.create_repo(repo_id=repo_id, repo_type="dataset", private=True, exist_ok=True)
100
+ except Exception as e:
101
+ repo_err = str(e)
102
+
103
+ # Try downloading existing CSV
104
+ existing_df = pd.DataFrame()
105
+ try:
106
+ local_path = hf_hub_download(
107
+ repo_id=repo_id,
108
+ filename=path_in_repo,
109
+ repo_type="dataset",
110
+ token=token,
111
+ )
112
+ existing_df = pd.read_csv(local_path)
113
+ except Exception as e:
114
+ print("file", path_in_repo, "couldn't be found / read", str(e))
115
+ # File doesn't exist or is unreadable — start fresh
116
+ pass
117
+
118
+ # Convert new rows to DataFrame and append
119
+ new_df = pd.DataFrame(rows)
120
+ combined_df = pd.concat([existing_df, new_df], ignore_index=True)
121
+ print(combined_df)
122
+ # Save to memory as CSV
123
+ csv_buffer = io.StringIO()
124
+ combined_df.to_csv(csv_buffer, index=False)
125
+ csv_bytes = csv_buffer.getvalue().encode("utf-8")
126
+
127
+ # Upload the updated CSV
128
+ try:
129
+ api.upload_file(
130
+ path_or_fileobj=csv_bytes,
131
+ path_in_repo=path_in_repo,
132
+ repo_id=repo_id,
133
+ repo_type="dataset",
134
+ )
135
+ except Exception as e:
136
+ print(str(e))
137
+ return {"status": "hf_push_error", "error": str(e), "repo_error": repo_err}
138
+
139
+ return {"status": "hf_pushed", "rows_added": len(rows), "repo": repo_id, "repo_error": repo_err}
140
+
141
+ def _save_responses_to_hf(rows, repo_id: str | None = None, token: str | None = None):
142
+ """
143
+ Push a list of dict rows to a private HF dataset, one JSON file per row.
144
+
145
+ NOTE: This approach saves each response as an individual file. While this
146
+ prevents data loss from overwriting a single file, be aware of the following:
147
+ - Performance: Uploading many small files can be slower than a single large one.
148
+ - Scalability: A very large number of files (e.g., millions) can make the
149
+ dataset repository unwieldy to browse or clone.
150
+ - Loading Data: To load this data back into a `datasets.Dataset` object, you
151
+ will need to point to the specific files, for example:
152
+ `load_dataset('json', data_files='path/to/your/repo/data/*.json')`
153
+ """
154
+ if HfApi is None:
155
+ return {"status": "hf_unavailable", "reason": "missing_packages"}
156
+
157
+ token = token or os.environ.get("HF_TOKEN")
158
+ repo_id = repo_id or os.environ.get("HF_DATASET_ID")
159
+ if not token or not repo_id:
160
+ return {"status": "hf_skipped", "reason": "missing_token_or_repo_env"}
161
+
162
+ api = HfApi(token=token)
163
+ repo_err = None
164
+ try:
165
+ api.create_repo(repo_id=repo_id, repo_type="dataset", private=True, exist_ok=True)
166
+ except Exception as e:
167
+ repo_err = str(e)
168
+
169
+ # Process each row, uploading it as a separate JSON file
170
+ num_pushed = 0
171
+ errors = []
172
+ for row_dict in rows:
173
+ try:
174
+ # Create a unique filename. Using a UUID is the most robust method.
175
+ filename = f"{uuid.uuid4()}.json"
176
+ # Place files in a 'data' subdirectory to keep the repo root clean.
177
+ path_in_repo = f"data/{filename}"
178
+
179
+ # Convert the dictionary to JSON bytes for uploading
180
+ json_bytes = json.dumps(row_dict, indent=2).encode("utf-8")
181
+
182
+ api.upload_file(
183
+ path_or_obj=json_bytes,
184
+ path_in_repo=path_in_repo,
185
+ repo_id=repo_id,
186
+ repo_type="dataset",
187
+ )
188
+ num_pushed += 1
189
+ except Exception as e:
190
+ errors.append(str(e))
191
+
192
+ if errors:
193
+ print("json errors", errors, "repo errors", repo_err)
194
+ return {"status": "hf_push_error", "pushed": num_pushed, "total": len(rows), "errors": errors, "repo_error": repo_err}
195
+
196
+ return {"status": "hf_pushed", "rows": len(rows), "repo": repo_id, "repo_error": repo_err}
197
+
198
+
199
+ def save_response(sample, audio_path, annotator, session_id, user_email, comment, scores, config):
200
+ """Saves a response row locally and attempts to push to Hugging Face Hub."""
201
+ os.makedirs(os.path.dirname(OUTPUT_CSV) or '.', exist_ok=True)
202
+
203
+ criteria_labels = [c['label'] for c in config['criteria']]
204
+ header = ["timestamp", "sample", "audio_path", "annotator", "session_id", "user_email"] + criteria_labels + ["comment"]
205
+
206
+ active_scores = list(scores)[:len(criteria_labels)]
207
+ row = [datetime.utcnow().isoformat(), sample, audio_path, annotator, session_id, user_email] + active_scores + [comment]
208
+
209
+ write_header = not os.path.exists(OUTPUT_CSV)
210
+ with open(OUTPUT_CSV, "a", newline='', encoding='utf-8') as f:
211
+ try: fcntl.flock(f.fileno(), fcntl.LOCK_EX)
212
+ except Exception: pass
213
+
214
+ writer = csv.writer(f)
215
+ if write_header: writer.writerow(header)
216
+ writer.writerow(row)
217
+
218
+ try: fcntl.flock(f.fileno(), fcntl.LOCK_UN)
219
+ except Exception: pass
220
+
221
+ # --- Hugging Face Push Logic ---
222
+ hf_result = None
223
+ if not IS_LOCAL_MODE:
224
+ try:
225
+ hf_record = dict(zip(header, row))
226
+ hf_result = save_responses_to_hf([hf_record])
227
+ except Exception as e:
228
+ print(e)
229
+ hf_result = {"status": "hf_error", "error": str(e)}
230
+
231
+ return {"status": "saved", "sample": sample, "hf": hf_result}
232
+
233
+
234
+ # --- Gradio UI Definition ---
235
+ def make_ui():
236
+
237
+ def make_explainer_fn(criterion_index):
238
+ def explainer(value, config):
239
+ if not config or criterion_index >= len(config.get('criteria', [])): return ""
240
+ criterion = config['criteria'][criterion_index]
241
+ try: iv = int(value)
242
+ except (ValueError, TypeError): iv = value
243
+ text = criterion['explanations'].get(iv, "No description for this score.")
244
+ return f"**{criterion['label']} ({iv}/{criterion['max']}):** {text}"
245
+ return explainer
246
+
247
+ #with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo:
248
+ with gr.Blocks() as demo:
249
+ # --- STATE MANAGEMENT ---
250
+ samples_list = gr.State()
251
+ current_index = gr.State(0)
252
+ config_state = gr.State()
253
+ session_id_global = gr.State()
254
+
255
+ # --- SETUP UI (Visible at start) ---
256
+ with gr.Group() as setup_group:
257
+ gr.Markdown("# Evaluation Setup")
258
+ gr.Markdown("Please provide your details and select the evaluation setup to begin.")
259
+ #config_dropdown = gr.Dropdown(choices=find_config_files(), label="Select Evaluation", value=find_config_files()[0] if find_config_files() else "")
260
+ config_dropdown = gr.Dropdown(choices=find_config_files(), label="Select Evaluation", value=None) # if find_config_files() else "")
261
+
262
+ instructions_md = gr.Markdown(visible=False, elem_classes="instructions")
263
+
264
+ with gr.Accordion("Annotator Info", open=True):
265
+ annotator_global = gr.Textbox(label="Annotator ID", lines=1)
266
+ user_email_global = gr.Textbox(label="User email (optional)", lines=1)
267
+ start_button = gr.Button("Start Evaluation", variant="primary")
268
+ config_error_md = gr.Markdown("", visible=False)
269
+
270
+ # --- MAIN EVALUATION UI (Initially hidden) ---
271
+ with gr.Group(visible=False) as main_group:
272
+ title_md = gr.Markdown("# Evaluation UI")
273
+ header_md = gr.Markdown("")
274
+ progress_md = gr.Markdown("Sample 1 of X")
275
+
276
+ with gr.Row():
277
+ with gr.Column(scale=1, variant='panel'):
278
+ sample_name_md = gr.Markdown("### Audio File")
279
+ gr.Markdown("---")
280
+ evaluation_audio = gr.Audio(label="Audio for Evaluation")
281
+ gr.Markdown("---")
282
+ submit_btn = gr.Button("Save & Next", variant="primary", interactive=False)
283
+ status = gr.Textbox(label="Status", interactive=False)
284
+
285
+ with gr.Column(scale=2, variant='panel'):
286
+ gr.Markdown("### Scoring Criteria")
287
+ slider_explanation_md = gr.Markdown("_Move a slider to see the description for each score._")
288
+ gr.Markdown("---")
289
+ sliders = [gr.Slider(visible=False, interactive=True) for _ in range(MAX_CRITERIA)]
290
+ gr.Markdown("---")
291
+ comment = gr.Textbox(label="Comments (optional)", lines=4, placeholder="Enter any additional feedback here...")
292
+
293
+ # --- UI ELEMENT LISTS ---
294
+ main_ui_elements = [
295
+ title_md, header_md, progress_md, sample_name_md, evaluation_audio,
296
+ slider_explanation_md, comment, submit_btn, status, *sliders
297
+ ]
298
+
299
+ # --- LOGIC & EVENTS ---
300
+ def load_sample(samples, index, config):
301
+ total_samples = len(samples)
302
+ updates = {}
303
+ if index >= total_samples:
304
+ completion_msg = f"**All {total_samples} samples completed! Thank you!**"
305
+ for el in main_ui_elements: updates[el] = gr.update(visible=False)
306
+ updates[progress_md] = gr.update(value=completion_msg, visible=True)
307
+ updates[status] = gr.update(value="Finished.", visible=True)
308
+ return updates
309
+
310
+ sample = samples[index]
311
+ samples_dir = config.get('samples_directory', 'sample-audios')
312
+ sample_path = os.path.join(samples_dir, sample)
313
+ sample_exists = os.path.exists(sample_path)
314
+
315
+ updates = {
316
+ progress_md: gr.update(value=f"Sample **{index + 1}** of **{total_samples}**", visible=True),
317
+ sample_name_md: gr.update(value=f"### File: `{sample}`", visible=True),
318
+ evaluation_audio: gr.update(value=sample_path if sample_exists else None, visible=sample_exists),
319
+ slider_explanation_md: gr.update(value="_Move a slider to see the description for each score._", visible=True),
320
+ comment: gr.update(value="", visible=True),
321
+ submit_btn: gr.update(value="Play audio to enable", interactive=False, visible=True),
322
+ status: gr.update(value="Ready.", visible=True)
323
+ }
324
+ num_criteria = len(config['criteria'])
325
+ for i in range(MAX_CRITERIA):
326
+ if i < num_criteria:
327
+ criterion = config['criteria'][i]
328
+ updates[sliders[i]] = gr.update(
329
+ label=criterion['label'], minimum=criterion['min'], maximum=criterion['max'],
330
+ step=criterion['step'], value=criterion['default'], visible=True
331
+ )
332
+ else:
333
+ updates[sliders[i]] = gr.update(visible=False, value=0)
334
+ return updates
335
+
336
+ def enable_submit_button():
337
+ return gr.update(value="Save & Next", interactive=True)
338
+
339
+ def update_instructions(config_path):
340
+ if not config_path: return gr.update(value="", visible=False)
341
+ config = load_config(config_path)
342
+ if config and 'instructions_markdown' in config:
343
+ return gr.update(value=config['instructions_markdown'], visible=True)
344
+ return gr.update(value="", visible=False)
345
+
346
+ def start_session(config_path):
347
+ if not config_path or not os.path.exists(config_path):
348
+ return {config_error_md: gr.update(value="**Error:** Please select a valid configuration file.", visible=True)}
349
+
350
+ config = load_config(config_path)
351
+ if config is None:
352
+ return {config_error_md: gr.update(value=f"**Error:** Could not load or parse `{config_path}`. Check console for details.", visible=True)}
353
+
354
+ samples_dir = config.get('samples_directory', 'sample-audios')
355
+ should_randomize = config.get('randomize_samples', False)
356
+
357
+ s_list = list_samples(samples_dir)
358
+ if not s_list:
359
+ return {config_error_md: gr.update(value=f"**Error:** No audio files found in directory: `{samples_dir}`", visible=True)}
360
+
361
+ if should_randomize: random.shuffle(s_list)
362
+
363
+ session_id = str(uuid.uuid4())
364
+ index = 0
365
+
366
+ updates = {
367
+ setup_group: gr.update(visible=False),
368
+ main_group: gr.update(visible=True),
369
+ config_error_md: gr.update(visible=False),
370
+ title_md: gr.update(value=f"# {config.get('title', 'Evaluation UI')}"),
371
+ header_md: gr.update(value=config.get('header_markdown', '')),
372
+ config_state: config,
373
+ session_id_global: session_id,
374
+ samples_list: s_list,
375
+ current_index: index,
376
+ }
377
+ sample_updates = load_sample(s_list, index, config)
378
+ updates.update(sample_updates)
379
+ return updates
380
+
381
+ def save_and_next(index, samples, annotator, sid, email, comment, config, *scores):
382
+ sample = samples[index]
383
+ samples_dir = config.get('samples_directory', 'sample-audios')
384
+ sample_path = os.path.join(samples_dir, sample)
385
+ save_status = save_response(sample, sample_path, annotator, sid, email, comment, scores, config)
386
+
387
+ next_index = index + 1
388
+ updates_dict = load_sample(samples, next_index, config)
389
+ # Provide more detailed status, including HF info if available
390
+ status_message = f"Saved {sample} locally."
391
+ if save_status.get('hf'):
392
+ hf_stat = save_status['hf'].get('status', 'hf_unknown')
393
+ status_message += f" HF status: {hf_stat}."
394
+ updates_dict[status] = gr.update(value=status_message)
395
+
396
+ ordered_updates = [updates_dict.get(el) for el in main_ui_elements]
397
+ return [next_index] + ordered_updates
398
+
399
+ # --- Event Wiring ---
400
+ config_dropdown.change(
401
+ update_instructions, inputs=[config_dropdown], outputs=[instructions_md]
402
+ ).then(None, None, None, js="() => { document.getElementById('component-0').scrollIntoView(); }")
403
+
404
+ start_button.click(
405
+ start_session,
406
+ inputs=[config_dropdown],
407
+ outputs=[
408
+ setup_group, main_group, config_error_md, *main_ui_elements,
409
+ config_state, session_id_global, samples_list, current_index
410
+ ]
411
+ )
412
+
413
+ submit_btn.click(
414
+ save_and_next,
415
+ inputs=[current_index, samples_list, annotator_global, session_id_global, user_email_global, comment, config_state, *sliders],
416
+ outputs=[current_index, *main_ui_elements]
417
+ )
418
+
419
+ for i, slider in enumerate(sliders):
420
+ slider.change(make_explainer_fn(i), inputs=[slider, config_state], outputs=[slider_explanation_md])
421
+
422
+ evaluation_audio.play(fn=enable_submit_button, inputs=None, outputs=[submit_btn])
423
+
424
+ demo.load(update_instructions, inputs=config_dropdown, outputs=instructions_md)
425
+
426
+ return demo
427
+
428
+ if __name__ == "__main__":
429
+ app = make_ui()
430
+ app.launch(server_name="0.0.0.0", server_port=7860)
config_mos.yaml ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configuration for a standard Mean Opinion Score (MOS) test.
2
+ title: "MOS Test - Audio Quality Evaluation"
3
+ header_markdown: "Listen to the audio sample and rate its overall quality on a scale of 1 to 5."
4
+
5
+ instructions_markdown: |
6
+ **Welcome, Annotator!**
7
+
8
+ Instructions for MOS test:
9
+
10
+ Please follow these steps carefully:
11
+ 1. Enter your unique **Annotator ID** before you begin.
12
+ 2. Listen to each audio clip from start to finish.
13
+ 3. Rate the clip using the sliders provided based on the scoring guide.
14
+ 4. Provide any extra details in the comments box.
15
+ 5. Click 'Save & Next' to submit your rating and load the next clip.
16
+
17
+ # The directory where your audio files are stored.
18
+ samples_directory: "sample-audios"
19
+
20
+ # Set to 'true' to shuffle the audio files, 'false' for alphabetical order.
21
+ randomize_samples: true
22
+
23
+
24
+ # MOS tests typically use a single criterion for overall quality.
25
+ criteria:
26
+ - label: "Overall Quality"
27
+ min: 1
28
+ max: 5
29
+ step: 1
30
+ default: 3
31
+ # These are standard definitions for the 5-point Absolute Category Rating (ACR) scale.
32
+ explanations:
33
+ 1: "Bad - The quality is very distracting and unpleasant."
34
+ 2: "Poor - The quality is distracting and annoying."
35
+ 3: "Fair - The quality is slightly distracting, but acceptable."
36
+ 4: "Good - The quality is not distracting, it is fine."
37
+ 5: "Excellent - The quality is flawless and natural."
config_original.yaml ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # General UI Configuration
2
+ title: "TTS Rubric — Dynamic Evaluation"
3
+
4
+ instructions_markdown: |
5
+ **Welcome annotator!**
6
+
7
+ Instructions for multiple aspect test
8
+
9
+ Please follow these steps carefully:
10
+ 1. Enter your unique **Annotator ID** before you begin.
11
+ 2. Listen to each audio clip from start to finish.
12
+ 3. Rate the clip using the sliders provided based on the scoring guide.
13
+ 4. Provide any extra details in the comments box.
14
+ 5. Click 'Save & Next' to submit your rating and load the next clip.
15
+
16
+ # The directory where your audio files are stored.
17
+ samples_directory: "sample-audios"
18
+
19
+ # Set to 'true' to shuffle the audio files, 'false' for alphabetical order.
20
+ randomize_samples: true
21
+ # Define the evaluation criteria. The UI will be built from this list.
22
+ criteria:
23
+ - label: "Clarity & Intelligibility"
24
+ min: 1
25
+ max: 5
26
+ step: 1
27
+ default: 3
28
+ explanations:
29
+ 1: "Unacceptable."
30
+ 2: "Often unclear or distorted; difficult to follow."
31
+ 3: "Understandable but requires effort; some words unclear."
32
+ 4: "Mostly clear, minor issues (with fast/slow playback)."
33
+ 5: "Speech is clear, easy to understand (at all speeds)."
34
+
35
+ - label: "Accent & Pronunciation"
36
+ min: 1
37
+ max: 5
38
+ step: 1
39
+ default: 3
40
+ explanations:
41
+ 1: "Severe pronunciation problems; largely unintelligible."
42
+ 2: "Frequent pronunciation issues that impede understanding."
43
+ 3: "Some mispronunciations that require effort to interpret."
44
+ 4: "Minor pronunciation quirks but overall fine."
45
+ 5: "Pronunciation is natural and appropriate for the target dialect."
46
+
47
+ - label: "Tone & Suitability"
48
+ min: 1
49
+ max: 5
50
+ step: 1
51
+ default: 3
52
+ explanations:
53
+ 1: "Tone is inappropriate or harmful for the content."
54
+ 2: "Tone often feels off or distracting from the content."
55
+ 3: "Tone is acceptable but occasionally inappropriate."
56
+ 4: "Generally appropriate tone with small mismatches."
57
+ 5: "Tone fits the content and use-case perfectly."
58
+
59
+ - label: "Voice quality"
60
+ min: 1
61
+ max: 5
62
+ step: 1
63
+ default: 3
64
+ explanations:
65
+ 1: "Unusable voice quality."
66
+ 2: "Poor quality with frequent artifacts."
67
+ 3: "Noticeable quality issues but still usable."
68
+ 4: "Minor artifacts but overall high quality."
69
+ 5: "Natural, pleasant voice with no artifacts."
70
+
71
+ - label: "Customization & Flexibility"
72
+ min: 1
73
+ max: 5
74
+ step: 1
75
+ default: 3
76
+ explanations:
77
+ 1: "No useful customization; inflexible."
78
+ 2: "Very limited or brittle customization options."
79
+ 3: "Limited customization; acceptable for simple use-cases."
80
+ 4: "Some customization available; works well for most cases."
81
+ 5: "Highly flexible and customizable for different styles."
82
+
83
+ - label: "Listening comfort"
84
+ min: 1
85
+ max: 5
86
+ step: 1
87
+ default: 3
88
+ explanations:
89
+ 1: "Uncomfortable or painful to listen to."
90
+ 2: "Often fatiguing or distracting to listen to."
91
+ 3: "Some listening fatigue; tolerable for short durations."
92
+ 4: "Mostly comfortable with occasional sharpness or fatigue."
93
+ 5: "Comfortable to listen to for extended periods."
download_dataset.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Download and merge all data files from a Hugging Face dataset repo.
2
+
3
+ Usage:
4
+ HF_TOKEN must be exported in your environment (or pass --token).
5
+ HF_DATASET_ID may be exported or passed via --repo.
6
+
7
+ Example:
8
+ export HF_TOKEN="hf_..."
9
+ python download_dataset.py --repo kathiasi/tts-rubric-responses --outdir out
10
+
11
+ This script downloads any files under `data/` (parquet or arrow/ipc), reads them,
12
+ concatenates into a single table, and writes `combined.parquet` and `combined.csv` in
13
+ `outdir`.
14
+ """
15
+ import os
16
+ import argparse
17
+ import json
18
+ from huggingface_hub import HfApi, hf_hub_download
19
+ import pyarrow.parquet as pq
20
+ import pyarrow.ipc as ipc
21
+ import pandas as pd
22
+
23
+
24
+ def read_parquet(path):
25
+ try:
26
+ tbl = pq.read_table(path)
27
+ return tbl.to_pandas()
28
+ except Exception as e:
29
+ raise RuntimeError(f"Failed to read parquet {path}: {e}")
30
+
31
+
32
+ def read_arrow(path):
33
+ try:
34
+ with open(path, 'rb') as f:
35
+ reader = ipc.open_file(f)
36
+ tbl = reader.read_all()
37
+ return tbl.to_pandas()
38
+ except Exception as e:
39
+ raise RuntimeError(f"Failed to read arrow/ipc {path}: {e}")
40
+
41
+
42
+ def download_and_merge(repo_id, outdir, token=None):
43
+ api = HfApi()
44
+ token = token or os.environ.get('HF_TOKEN')
45
+ if not token:
46
+ raise RuntimeError('HF_TOKEN not provided; export HF_TOKEN or pass --token')
47
+
48
+ files = api.list_repo_files(repo_id=repo_id, repo_type='dataset', token=token)
49
+ data_files = [f for f in files if f.startswith('data/')]
50
+ if not data_files:
51
+ print('No data/ files found in dataset repo. Files found:')
52
+ print(json.dumps(files, indent=2))
53
+ return
54
+
55
+ os.makedirs(outdir, exist_ok=True)
56
+ dfs = []
57
+ for fname in sorted(data_files):
58
+ print('Processing', fname)
59
+ local_path = hf_hub_download(repo_id=repo_id, repo_type='dataset', filename=fname, token=token)
60
+ if fname.endswith('.parquet'):
61
+ df = read_parquet(local_path)
62
+ elif fname.endswith('.arrow') or fname.endswith('.ipc'):
63
+ df = read_arrow(local_path)
64
+ else:
65
+ print('Skipping unsupported data file:', fname)
66
+ continue
67
+ dfs.append(df)
68
+
69
+ if not dfs:
70
+ print('No supported data files were read.')
71
+ return
72
+
73
+ combined = pd.concat(dfs, ignore_index=True)
74
+ out_parquet = os.path.join(outdir, 'combined.parquet')
75
+ out_csv = os.path.join(outdir, 'combined.csv')
76
+ print(f'Writing {len(combined)} rows to', out_parquet)
77
+ combined.to_parquet(out_parquet, index=False)
78
+ print('Also writing CSV to', out_csv)
79
+ combined.to_csv(out_csv, index=False)
80
+ print('Done.')
81
+
82
+
83
+ if __name__ == '__main__':
84
+ p = argparse.ArgumentParser()
85
+ p.add_argument('--repo', help='Dataset repo id (user/name)', default=os.environ.get('HF_DATASET_ID'))
86
+ p.add_argument('--outdir', help='Output directory', default='hf_dataset')
87
+ p.add_argument('--token', help='Hugging Face token (optional)', default=None)
88
+ args = p.parse_args()
89
+
90
+ if not args.repo:
91
+ print('Dataset repo id is required via --repo or HF_DATASET_ID env var')
92
+ raise SystemExit(1)
93
+
94
+ download_and_merge(args.repo, args.outdir, token=args.token)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio==5.15
2
+ numpy
3
+ python-docx
4
+ PyYAML
5
+ huggingface_hub>=0.28.1
6
+ pandas
7
+
8
+
sample-audios/sorsamisk_-_115_01_-_Goevten_voestes_biejjie_015_024_s.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d08e73cfdf0d3c6aaf1eef5b4a028b96f6dac88a0f673ed654faabdabb0b82cd
3
+ size 827436
sample-audios/sorsamisk_-_7_01_-_Jarkoestidh_028_020.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3292ee4d5f741001bff3a8ac7e2c5d18daee9b227f7a36cf43632205b6983b91
3
+ size 209532
sample-audios/sorsamisk_-_III_01_-_Giesie_eejehtimmiebiejjieh_015_015.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:98c6fc87df1f69d24878c4f04fce7f7713c574a1bd531001a65f66b096e0ad40
3
+ size 295016
sample-audios/sorsamisk_goltelidh_jupmelen_rihjke_lea_gietskesne_cd1_mono_022_009.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f3bbcaea90f2bdb4bf798c8e4214ddf2381eedaf3ffd974509ca54d02dc5504b
3
+ size 468702
sample-audios/sorsamisk_goltelidh_jupmelen_rihjke_lea_gietskesne_cd1_mono_030_014.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:90bc9504d119bc8a95ab02c16299c054725c7b32fa55e9b5b2dbe847fb297d83
3
+ size 458130
sample-audios/sorsamisk_goltelidh_jupmelen_rihjke_lea_gietskesne_cd2_009_TRN_019.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:71ccd5595e3ed565eba27cd1ce48a07cd954717c196a9ea3a672bbe9cf043290
3
+ size 147504
sample-audios/sorsamisk_goltelidh_jupmelen_rihjke_lea_gietskesne_cd2_009_TRN_019_s.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:86554d671d506fef1863a3b026ccbfb77e6fd6e2112b4380b9ccab18acd3f9a1
3
+ size 143916