Taino commited on
Commit
e5867bc
·
1 Parent(s): 44c76a2

Create app-streamlit.py

Browse files
Files changed (1) hide show
  1. app-streamlit.py +280 -0
app-streamlit.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, sys, re, json
2
+ import argparse
3
+ import shutil
4
+ import warnings
5
+ import whisper_timestamped as wt
6
+ from pdb import set_trace as b
7
+ from pprint import pprint as pp
8
+ from profanity_check import predict, predict_prob
9
+ from pydub import AudioSegment
10
+ from pydub.playback import play
11
+ from subprocess import Popen, PIPE
12
+
13
+ def parse_args():
14
+ """
15
+ """
16
+ parser = argparse.ArgumentParser(
17
+ description=('Tool to mute profanities in a song (source separation -> speech recognition -> profanity detection -> mask profanities -> re-mix)'),
18
+ usage=('see <py main.py --help> or run as local web app with streamlit: <streamlit run main.py>')
19
+ )
20
+
21
+ parser.add_argument(
22
+ '-i',
23
+ '--input',
24
+ default=None,
25
+ nargs='?',
26
+ #required=True,
27
+ help=("path to a mp3")
28
+ )
29
+ parser.add_argument(
30
+ '-m',
31
+ '--model',
32
+ default='small',
33
+ nargs='?',
34
+ help=("model used by whisper for speech recognition: tiny, small (default) or medium")
35
+ )
36
+ parser.add_argument(
37
+ '-p',
38
+ '--play',
39
+ default=False,
40
+ action='store_true',
41
+ help=("play output audio at the end")
42
+ )
43
+ parser.add_argument(
44
+ '-v',
45
+ '--verbose',
46
+ default=True,
47
+ action='store_true',
48
+ help=("print transcribed text and detected profanities to screen")
49
+ )
50
+ return parser.parse_args()
51
+
52
+
53
+ def main(args, input_file=None, model_size=None, verbose=False, play_output=False, skip_ss=False):
54
+ """
55
+ """
56
+ if not input_file:
57
+ input_file = args.input
58
+
59
+ if not model_size:
60
+ model_size = args.model
61
+
62
+ if not verbose:
63
+ verbose = args.verbose
64
+
65
+ if not play_output:
66
+ play_output = args.play
67
+
68
+ # exit if input file not found
69
+ if len(sys.argv)>1 and not os.path.isfile(input_file):
70
+ print('Error: --input file not found')
71
+ raise Exception
72
+
73
+ print(f'\nProcessing input file: {input_file}')
74
+
75
+ if not skip_ss:
76
+ # split audio into vocals + accompaniment
77
+ print('Running source separation')
78
+ stems_dir = source_separation(input_file, use_demucs=False, use_spleeter=True)
79
+ vocal_stem = os.path.join(stems_dir, 'vocals.wav')
80
+ #instr_stem = os.path.join(stems_dir, 'no_vocals.wav') # demucs
81
+ instr_stem = os.path.join(stems_dir, 'accompaniment.wav') # spleeter
82
+ print(f'Vocal stem written to: {vocal_stem}')
83
+ else:
84
+ vocal_stem = input_file
85
+ instr_stem = None
86
+
87
+ audio = wt.load_audio(vocal_stem)
88
+ model = wt.load_model(model_size, device='cpu')
89
+ text = wt.transcribe(model, audio, language='en')
90
+
91
+ if verbose:
92
+ print('\nTranscribed text:')
93
+ print(text['text']+'\n')
94
+
95
+ # checking for profanities in text
96
+ print('Run profanity detection on text')
97
+ profanities = profanity_detection(text)
98
+ if not profanities:
99
+ print(f'No profanities found in {input_file} - exiting')
100
+ return 'No profanities found', None, None
101
+
102
+ if verbose:
103
+ print('profanities found in text:')
104
+ pp(profanities)
105
+
106
+ # masking
107
+ print('Mask profanities in vocal stem')
108
+ vocals = mask_profanities(vocal_stem, profanities)
109
+
110
+ # re-mixing
111
+ print('Merge instrumentals stem and masked vocals stem')
112
+ if not skip_ss:
113
+ mix = AudioSegment.from_wav(instr_stem).overlay(vocals)
114
+ else:
115
+ mix = vocals
116
+
117
+ # write mix to file
118
+ outpath = input_file.replace('.mp3', '_masked.mp3').replace('.wav', '_masked.wav')
119
+ if input_file.endswith('.wav'):
120
+ mix.export(outpath, format="wav")
121
+ elif input_file.endswith('.mp3'):
122
+ mix.export(outpath, format="mp3")
123
+ print(f'Mixed file written to: {outpath}')
124
+
125
+ # play output
126
+ if play_output:
127
+ print('\nPlaying output...')
128
+ play(mix)
129
+
130
+ return outpath, vocal_stem, instr_stem
131
+
132
+
133
+ def source_separation(inpath, use_demucs=False, use_spleeter=True):
134
+ """
135
+ Execute shell command to run demucs and pipe stdout/stderr back to python
136
+ """
137
+ infile = os.path.basename(inpath)
138
+
139
+ if use_demucs:
140
+ cmd = f'demucs --two-stems=vocals --jobs 8 "{inpath}"'
141
+ #stems_dir = os.path.join(re.findall('/.*', stdout)[0], infile.replace('.mp3','').replace('.wav',''))
142
+ elif use_spleeter:
143
+ outdir = 'audio/separated'
144
+ cmd = f'spleeter separate {inpath} -p spleeter:2stems -o {outdir}'
145
+ stems_dir = os.path.join(outdir, os.path.splitext(infile)[0])
146
+
147
+ stdout, stderr = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True, executable='/bin/bash').communicate()
148
+ stdout = stdout.decode('utf8')
149
+
150
+ # exit if lib error'd out
151
+ if stderr:
152
+ stderr = stderr.decode('utf-8').lower()
153
+ if 'error' in stderr or 'not exist' in stderr:
154
+ print(stderr.decode('utf8').split('\n')[0])
155
+ raise Exception
156
+
157
+ # parse stems directory path from stdout and return it if successful
158
+ if not os.path.isdir(stems_dir):
159
+ print(f'Error: output stem directory "{stems_dir}" not found')
160
+ raise Exception
161
+
162
+ return stems_dir
163
+
164
+
165
+ def profanity_detection(text):
166
+ """
167
+ """
168
+ # detect profanities in text
169
+ profs = []
170
+ for segment in text['segments']:
171
+ for word in segment['words']:
172
+ #if word['confidence']<.25:
173
+ # print(word)
174
+ text = word['text'].replace('.','').replace(',','').lower()
175
+
176
+ # skip false positives
177
+ if text in ['cancer','hell','junk','die','lame','freak','freaky','white','stink','shut','spit','mouth','orders','eat','clouds','ugly','dirty','wet']:
178
+ continue
179
+
180
+ # assume anything returned by whisper with more than 1 * is profanity e.g n***a
181
+ if '**' in text:
182
+ profs.append(word)
183
+ continue
184
+
185
+ # add true negatives
186
+ if text in ['bitchy', 'puss']:
187
+ profs.append(word)
188
+ continue
189
+
190
+ # run profanity detection - returns 1 (True) or 0 (False)
191
+ if predict([word['text']])[0]:
192
+ profs.append(word)
193
+
194
+ return profs
195
+
196
+
197
+ def mask_profanities(vocal_stem, profanities):
198
+ """
199
+ """
200
+ # load vocal stem and mask profanities
201
+ vocals = AudioSegment.from_wav(vocal_stem)
202
+ for prof in profanities:
203
+ mask = vocals[prof['start']*1000:prof['end']*1000] # pydub works in milliseconds
204
+ mask -= 50 # reduce lvl by some dB (enough to ~mute it)
205
+ #mask = mask.silent(len(mask))
206
+ #mask = mask.fade_in(100).fade_out(100) # it prepends/appends fades so end up with longer mask
207
+ start = vocals[:prof['start']*1000]
208
+ end = vocals[prof['end']*1000:]
209
+ #print(f"masking {prof['text']} from {prof['start']} to {prof['end']}")
210
+ vocals = start + mask + end
211
+
212
+ return vocals
213
+
214
+
215
+ if __name__ == "__main__":
216
+ args = parse_args()
217
+
218
+ if len(sys.argv)>1:
219
+ main(args, skip_ss=False)
220
+ else:
221
+ import streamlit as st
222
+ st.title('Saylss')
223
+ with st.expander("About", expanded=False):
224
+ st.markdown('''
225
+ This app processes an input audio track (.mp3 or .wav) with the purpose of identifying and muting profanities in the song.
226
+
227
+ A larger model takes longer to run and is more accurate, and vice-versa.
228
+ Simply select the model size and upload your file!
229
+ ''')
230
+ model = st.selectbox('Choose model size:', ('tiny','small','medium'), index=1)
231
+
232
+ uploaded_file = st.file_uploader(
233
+ "Choose input track:",
234
+ type=[".mp3",".wav"],
235
+ accept_multiple_files=False,
236
+ )
237
+
238
+ if uploaded_file is not None:
239
+ uploaded_file.name = uploaded_file.name.replace(' ','_')
240
+ ext = os.path.splitext(uploaded_file.name)[1]
241
+ if ext == '.wav':
242
+ st_format = 'audio/wav'
243
+ elif ext == '.mp3':
244
+ st_format = 'audio/mp3'
245
+
246
+ uploaded_file_content = uploaded_file.getvalue()
247
+ with open(uploaded_file.name, 'wb') as f:
248
+ f.write(uploaded_file_content)
249
+
250
+ audio_bytes_input = uploaded_file_content
251
+ st.audio(audio_bytes_input, format=st_format)
252
+
253
+ # run code
254
+ with st.spinner('Processing input audio...'):
255
+ inpath = os.path.abspath(uploaded_file.name)
256
+ outpath, vocal_stem, instr_stem = main(args, input_file=inpath, model_size=model)
257
+
258
+ if outpath == 'No profanities found':
259
+ st.text(outpath + ' - Refresh the page and try a different song or model size')
260
+ sys.exit()
261
+
262
+ # display output audio
263
+ #st.text('Play output Track:')
264
+ st.text('\nOutput:')
265
+ audio_file = open(outpath, 'rb')
266
+ audio_bytes = audio_file.read()
267
+ st.audio(audio_bytes, format=st_format)
268
+
269
+ # flush all media
270
+ if os.path.isfile(inpath):
271
+ os.remove(inpath)
272
+ if os.path.isfile(outpath):
273
+ os.remove(outpath)
274
+ if os.path.isfile(vocal_stem):
275
+ os.remove(vocal_stem)
276
+ if os.path.isfile(instr_stem):
277
+ os.remove(instr_stem)
278
+ sep_dir = os.path.split(instr_stem)[0]
279
+ if os.path.isdir(sep_dir):
280
+ os.rmdir(sep_dir)