Skip to content

Commit 784f87b

Browse files
pseethprem
and
prem
authored
v0.7.3 (#90)
* Pin wavesurfer to 6.6.4. * Fixing n_samples * Adding traceback * Fixing the mushra script. * fixing linting * test mushra only * Bumping version * adding a license, fixing tests * Fixing linting. --------- Co-authored-by: prem <prem@descript.com>
1 parent 49b8b6b commit 784f87b

File tree

6 files changed

+135
-157
lines changed

6 files changed

+135
-157
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023-Present, Descript
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

audiotools/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.7.2"
1+
__version__ = "0.7.3"
22
from .core import AudioSignal
33
from .core import STFTParams
44
from .core import Meter

audiotools/preference.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import copy
55
import csv
66
import random
7+
import sys
8+
import traceback
79
from collections import defaultdict
810
from pathlib import Path
911
from typing import List
@@ -123,11 +125,11 @@
123125
console.log("Created WaveSurfer object.")
124126
}
125127
126-
load_script('https://unpkg.com/wavesurfer.js')
128+
load_script('https://unpkg.com/wavesurfer.js@6.6.4')
127129
.then(() => {
128-
load_script("https://unpkg.com/wavesurfer.js/dist/plugin/wavesurfer.timeline.min.js")
130+
load_script("https://unpkg.com/wavesurfer.js@6.6.4/dist/plugin/wavesurfer.timeline.min.js")
129131
.then(() => {
130-
load_script('https://unpkg.com/wavesurfer.js/dist/plugin/wavesurfer.regions.min.js')
132+
load_script('https://unpkg.com/wavesurfer.js@6.6.4/dist/plugin/wavesurfer.regions.min.js')
131133
.then(() => {
132134
console.log("Loaded regions");
133135
create_wavesurfer();
@@ -535,7 +537,7 @@ def __init__(self, folder: str, shuffle: bool = True, n_samples: int = None):
535537
if shuffle:
536538
random.shuffle(self.names)
537539

538-
self.n_samples = n_samples
540+
self.n_samples = len(self.names) if n_samples is None else n_samples
539541

540542
def get_updates(self, idx, order):
541543
key = self.names[idx]
@@ -544,7 +546,7 @@ def get_updates(self, idx, order):
544546
def progress(self):
545547
try:
546548
pct = self.current / len(self) * 100
547-
except:
549+
except: # pragma: no cover
548550
pct = 100
549551
text = f"On {self.current} / {len(self)} samples"
550552
pbar = (
@@ -555,7 +557,7 @@ def progress(self):
555557
return gr.update(value=pbar)
556558

557559
def __len__(self):
558-
return len(self.names)
560+
return self.n_samples
559561

560562
def filter_completed(self, user, save_path):
561563
if not self.filtered:
@@ -565,6 +567,7 @@ def filter_completed(self, user, save_path):
565567
reader = csv.DictReader(f)
566568
done = [r["sample"] for r in reader if r["user"] == user]
567569
self.names = [k for k in self.names if k not in done]
570+
self.names = self.names[: self.n_samples]
568571
self.filtered = True # Avoid filtering more than once per session.
569572

570573
def get_next_sample(self, reference, conditions):
@@ -580,6 +583,7 @@ def get_next_sample(self, reference, conditions):
580583
done = gr.update(interactive=True)
581584
pbar = self.progress()
582585
except:
586+
traceback.print_exc()
583587
updates = [gr.update() for _ in range(len(self.order))]
584588
done = gr.update(value="No more samples!", interactive=False)
585589
self.current = len(self)

examples/mushra.py

Lines changed: 90 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,105 @@
1-
import math
21
import string
32
from dataclasses import dataclass
43
from pathlib import Path
4+
from typing import List
55

6+
import argbind
67
import gradio as gr
7-
import numpy as np
8-
import soundfile as sf
98

109
from audiotools import preference as pr
1110

1211

12+
@argbind.bind(without_prefix=True)
1313
@dataclass
1414
class Config:
1515
folder: str = None
1616
save_path: str = "results.csv"
17-
conditions: list = None
17+
conditions: List[str] = None
1818
reference: str = None
1919
seed: int = 0
20-
21-
22-
def random_sine(f):
23-
fs = 44100 # sampling rate, Hz, must be integer
24-
duration = 5.0 # in seconds, may be float
25-
26-
# generate samples, note conversion to float32 array
27-
volume = 0.1
28-
num_samples = int(fs * duration)
29-
samples = volume * np.sin(2 * math.pi * (f / fs) * np.arange(num_samples))
30-
31-
return samples, fs
32-
33-
34-
def create_data(path):
35-
path = Path(path)
36-
hz = [110, 140, 180]
37-
38-
for i in range(6):
39-
name = f"condition_{string.ascii_lowercase[i]}"
40-
for j in range(3):
41-
sample_path = path / name / f"sample_{j}.wav"
42-
sample_path.parent.mkdir(exist_ok=True, parents=True)
43-
audio, sr = random_sine(hz[j] * (2**i))
44-
sf.write(sample_path, audio, sr)
45-
46-
47-
config = Config(
48-
folder="/tmp/pref/audio/",
49-
save_path="/tmp/pref/results.csv",
50-
conditions=["condition_a", "condition_b"],
51-
reference="condition_c",
52-
)
53-
54-
create_data(config.folder)
55-
56-
with gr.Blocks() as app:
57-
save_path = config.save_path
58-
samples = gr.State(pr.Samples(config.folder))
59-
60-
reference = config.reference
61-
conditions = config.conditions
62-
63-
player = pr.Player(app)
64-
player.create()
65-
if reference is not None:
66-
player.add("Play Reference")
67-
68-
user = pr.create_tracker(app)
69-
ratings = []
70-
71-
with gr.Row():
72-
gr.HTML("")
73-
with gr.Column(scale=9):
74-
gr.HTML(pr.slider_mushra)
75-
76-
for i in range(len(conditions)):
77-
with gr.Row().style(equal_height=True):
78-
x = string.ascii_uppercase[i]
79-
player.add(f"Play {x}")
80-
with gr.Column(scale=9):
81-
ratings.append(gr.Slider(value=50, interactive=True))
82-
83-
def build(user, samples, *ratings):
84-
# Filter out samples user has done already, by looking in the CSV.
85-
samples.filter_completed(user, save_path)
86-
87-
# Write results to CSV
88-
if samples.current > 0:
89-
start_idx = 1 if reference is not None else 0
90-
name = samples.names[samples.current - 1]
91-
result = {"sample": name, "user": user}
92-
for k, r in zip(samples.order[start_idx:], ratings):
93-
result[k] = r
94-
pr.save_result(result, save_path)
95-
96-
updates, done, pbar = samples.get_next_sample(reference, conditions)
97-
return updates + [gr.update(value=50) for _ in ratings] + [done, samples, pbar]
98-
99-
progress = gr.HTML()
100-
begin = gr.Button("Submit", elem_id="start-survey")
101-
begin.click(
102-
fn=build,
103-
inputs=[user, samples] + ratings,
104-
outputs=player.to_list() + ratings + [begin, samples, progress],
105-
).then(None, _js=pr.reset_player)
106-
107-
# Comment this back in to actually launch the script.
108-
app.launch()
20+
share: bool = False
21+
n_samples: int = 10
22+
23+
24+
def get_text(wav_file: str):
25+
txt_file = Path(wav_file).with_suffix(".txt")
26+
if Path(txt_file).exists():
27+
with open(txt_file, "r") as f:
28+
txt = f.read()
29+
else:
30+
txt = ""
31+
return f"""<div style="text-align:center;font-size:large;">{txt}</div>"""
32+
33+
34+
def main(config: Config):
35+
with gr.Blocks() as app:
36+
save_path = config.save_path
37+
samples = gr.State(pr.Samples(config.folder, n_samples=config.n_samples))
38+
39+
reference = config.reference
40+
conditions = config.conditions
41+
42+
player = pr.Player(app)
43+
player.create()
44+
if reference is not None:
45+
player.add("Play Reference")
46+
47+
user = pr.create_tracker(app)
48+
ratings = []
49+
50+
with gr.Row():
51+
txt = gr.HTML("")
52+
53+
with gr.Row():
54+
gr.Button("Rate audio quality", interactive=False)
55+
with gr.Column(scale=8):
56+
gr.HTML(pr.slider_mushra)
57+
58+
for i in range(len(conditions)):
59+
with gr.Row().style(equal_height=True):
60+
x = string.ascii_uppercase[i]
61+
player.add(f"Play {x}")
62+
with gr.Column(scale=9):
63+
ratings.append(gr.Slider(value=50, interactive=True))
64+
65+
def build(user, samples, *ratings):
66+
# Filter out samples user has done already, by looking in the CSV.
67+
samples.filter_completed(user, save_path)
68+
69+
# Write results to CSV
70+
if samples.current > 0:
71+
start_idx = 1 if reference is not None else 0
72+
name = samples.names[samples.current - 1]
73+
result = {"sample": name, "user": user}
74+
for k, r in zip(samples.order[start_idx:], ratings):
75+
result[k] = r
76+
pr.save_result(result, save_path)
77+
78+
updates, done, pbar = samples.get_next_sample(reference, conditions)
79+
wav_file = updates[0]["value"]
80+
81+
txt_update = gr.update(value=get_text(wav_file))
82+
83+
return (
84+
updates
85+
+ [gr.update(value=50) for _ in ratings]
86+
+ [done, samples, pbar, txt_update]
87+
)
88+
89+
progress = gr.HTML()
90+
begin = gr.Button("Submit", elem_id="start-survey")
91+
begin.click(
92+
fn=build,
93+
inputs=[user, samples] + ratings,
94+
outputs=player.to_list() + ratings + [begin, samples, progress, txt],
95+
).then(None, _js=pr.reset_player)
96+
97+
# Comment this back in to actually launch the script.
98+
app.launch(share=config.share)
99+
100+
101+
if __name__ == "__main__":
102+
args = argbind.parse_args()
103+
with argbind.scope(args):
104+
config = Config()
105+
main(config)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
setup(
88
name="descript-audiotools",
9-
version="0.7.2",
9+
version="0.7.3",
1010
classifiers=[
1111
"Intended Audience :: Developers",
1212
"Intended Audience :: Education",

tests/test_preference.py

Lines changed: 12 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def build(user, samples, *ratings):
8383
samples.filter_completed(user, save_path)
8484

8585
# Write results to CSV
86-
if samples.current > 0:
86+
if samples.current > 0 and len(samples.names) > 0:
8787
start_idx = 1 if reference is not None else 0
8888
name = samples.names[samples.current - 1]
8989
result = {"sample": name, "user": user}
@@ -107,60 +107,6 @@ def build(user, samples, *ratings):
107107
build("test", samples, 95, 85)
108108

109109

110-
def _test_abx(app, config):
111-
"Launches a preference test"
112-
save_path = config.save_path
113-
samples = gr.State(pr.Samples(config.folder))
114-
115-
reference = None
116-
conditions = config.conditions
117-
assert len(conditions) == 2, "Preference tests take only two conditions!"
118-
119-
player = pr.Player(app)
120-
player.create()
121-
if reference is not None:
122-
player.add("Play Reference")
123-
124-
user = pr.create_tracker(app)
125-
126-
with gr.Row().style(equal_height=True):
127-
for i in range(len(conditions)):
128-
x = string.ascii_uppercase[i]
129-
player.add(f"Play {x}")
130-
131-
rating = gr.Slider(value=50, interactive=True)
132-
gr.HTML(pr.slider_abx)
133-
134-
def build(user, samples, rating):
135-
samples.filter_completed(user, save_path)
136-
137-
# Write results to CSV
138-
if samples.current > 0:
139-
start_idx = 1 if reference is not None else 0
140-
name = samples.names[samples.current - 1]
141-
result = {"sample": name, "user": user}
142-
143-
result[samples.order[start_idx]] = 100 - rating
144-
result[samples.order[start_idx + 1]] = rating
145-
pr.save_result(result, save_path)
146-
147-
updates, done, pbar = samples.get_next_sample(reference, conditions)
148-
return updates + [gr.update(value=50), done, samples, pbar]
149-
150-
progress = gr.HTML()
151-
begin = gr.Button("Submit", elem_id="start-survey")
152-
begin.click(
153-
fn=build,
154-
inputs=[user, samples, rating],
155-
outputs=player.to_list() + [rating, begin, samples, progress],
156-
).then(None, _js=pr.reset_player)
157-
158-
# Call build to simulate a button click
159-
samples = pr.Samples(config.folder)
160-
for i in range(len(samples) + 1):
161-
build("test", samples, 100)
162-
163-
164110
def test_preference():
165111
with tempfile.TemporaryDirectory() as tmpdir:
166112
tmpdir = Path(tmpdir)
@@ -174,6 +120,16 @@ def test_preference():
174120
create_data(config.folder)
175121
with gr.Blocks() as app:
176122
_test_mushra(app, config)
123+
_test_mushra(app, config)
124+
125+
with tempfile.TemporaryDirectory() as tmpdir:
126+
tmpdir = Path(tmpdir)
127+
config = Config(
128+
folder=tmpdir,
129+
save_path=tmpdir / "results.csv",
130+
conditions=["condition_a", "condition_b"],
131+
)
177132

133+
create_data(config.folder)
178134
with gr.Blocks() as app:
179-
_test_abx(app, config)
135+
_test_mushra(app, config)

0 commit comments

Comments
 (0)