diff --git a/.gitignore b/.gitignore index 8f8bc7e4..155b9db6 100755 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ mp3 *.mp3 .DS_Store *.cnf +build/ +.idea/ +PyDejavu.egg-info/ diff --git a/dejavu.cnf.SAMPLE b/dejavu.cnf.SAMPLE index 3e980a99..309f6fca 100755 --- a/dejavu.cnf.SAMPLE +++ b/dejavu.cnf.SAMPLE @@ -2,7 +2,7 @@ "database": { "host": "127.0.0.1", "user": "root", - "passwd": "", + "passwd": "", "db": "dejavu" } } \ No newline at end of file diff --git a/dejavu/__init__.py b/dejavu/__init__.py index 4f6e6e8a..18418835 100755 --- a/dejavu/__init__.py +++ b/dejavu/__init__.py @@ -1,10 +1,9 @@ from dejavu.database import get_database, Database import dejavu.decoder as decoder -import fingerprint +import dejavu.fingerprint as fingerprint import multiprocessing import os -import traceback -import sys +import logging class Dejavu(object): @@ -15,6 +14,8 @@ class Dejavu(object): MATCH_TIME = 'match_time' OFFSET = 'offset' OFFSET_SECS = 'offset_seconds' + AUDIO_LENGTH = 'audio_length' + RELATIVE_CONFIDENCE = 'relative_confidence' def __init__(self, config): super(Dejavu, self).__init__() @@ -58,7 +59,7 @@ def fingerprint_directory(self, path, extensions, nprocesses=None): # don't refingerprint already fingerprinted files if decoder.unique_hash(filename) in self.songhashes_set: - print "%s already fingerprinted, continuing..." % filename + logging.getLogger('dejavu').warn("%s already fingerprinted, continuing..." % filename) continue filenames_to_fingerprint.append(filename) @@ -74,22 +75,21 @@ def fingerprint_directory(self, path, extensions, nprocesses=None): # Loop till we have all of them while True: try: - song_name, hashes, file_hash = iterator.next() + song_name, hashes, file_hash, audio_length = iterator.next() except multiprocessing.TimeoutError: continue except StopIteration: break except: - print("Failed fingerprinting") - # Print traceback because we can't reraise it here - traceback.print_exc(file=sys.stdout) + logging.getLogger('dejavu').exception("Failed fingerprinting") else: - sid = self.db.insert_song(song_name, file_hash) + logging.getLogger('dejavu').debug("Inserting " + song_name + " in database") + sid = self.db.insert_song(song_name, file_hash, audio_length) - self.db.insert_hashes(sid, hashes) + self.db.insert_hashes(sid, set([(x[0], int(x[1])) for x in hashes])) self.db.set_song_fingerprinted(sid) self.get_fingerprinted_songs() - + logging.getLogger('dejavu').info(song_name + " inserted in database") pool.close() pool.join() @@ -99,24 +99,31 @@ def fingerprint_file(self, filepath, song_name=None): song_name = song_name or songname # don't refingerprint already fingerprinted files if song_hash in self.songhashes_set: - print "%s already fingerprinted, continuing..." % song_name + logging.getLogger('dejavu').warn("%s already fingerprinted, continuing..." % song_name) else: - song_name, hashes, file_hash = _fingerprint_worker( + song_name, hashes, file_hash, audio_length = _fingerprint_worker( filepath, self.limit, song_name=song_name ) - sid = self.db.insert_song(song_name, file_hash) + logging.getLogger('dejavu').debug("Inserting " + song_name + " in database") + sid = self.db.insert_song(song_name, file_hash, audio_length) - self.db.insert_hashes(sid, hashes) + self.db.insert_hashes(sid, set([(x[0], int(x[1])) for x in hashes])) self.db.set_song_fingerprinted(sid) self.get_fingerprinted_songs() + logging.getLogger('dejavu').info(song_name + " inserted in database") def find_matches(self, samples, Fs=fingerprint.DEFAULT_FS): hashes = fingerprint.fingerprint(samples, Fs=Fs) - return self.db.return_matches(hashes) - - def align_matches(self, matches): + mapper = {} + total_hashes = 0 + for hash, offset in hashes: + mapper[hash.upper()[:fingerprint.FINGERPRINT_REDUCTION]] = offset + total_hashes += 1 + return (self.db.return_matches(mapper), total_hashes) + + def align_matches(self, matches, total_hashes): """ Finds hash matches that align in time with other matches and finds consensus about which hashes are "true" signal from the audio. @@ -157,6 +164,8 @@ def align_matches(self, matches): Dejavu.SONG_ID : song_id, Dejavu.SONG_NAME : songname, Dejavu.CONFIDENCE : largest_count, + Dejavu.AUDIO_LENGTH : song.get(Database.AUDIO_LENGTH, None), + Dejavu.RELATIVE_CONFIDENCE : (largest_count*100)/float(total_hashes), Dejavu.OFFSET : int(largest), Dejavu.OFFSET_SECS : nseconds, Database.FIELD_FILE_SHA1 : song.get(Database.FIELD_FILE_SHA1, None),} @@ -177,21 +186,17 @@ def _fingerprint_worker(filename, limit=None, song_name=None): songname, extension = os.path.splitext(os.path.basename(filename)) song_name = song_name or songname - channels, Fs, file_hash = decoder.read(filename, limit) + channels, Fs, file_hash, audio_length = decoder.read(filename, limit) result = set() channel_amount = len(channels) for channeln, channel in enumerate(channels): - # TODO: Remove prints or change them into optional logging. - print("Fingerprinting channel %d/%d for %s" % (channeln + 1, - channel_amount, - filename)) + logging.getLogger('dejavu').info("Fingerprinting channel %d/%d for %s" % (channeln + 1, channel_amount, filename)) hashes = fingerprint.fingerprint(channel, Fs=Fs) - print("Finished channel %d/%d for %s" % (channeln + 1, channel_amount, - filename)) + logging.getLogger('dejavu').debug("Finished channel %d/%d for %s" % (channeln + 1, channel_amount, filename)) result |= set(hashes) - return song_name, result, file_hash + return song_name, result, file_hash, audio_length def chunkify(lst, n): diff --git a/dejavu/database.py b/dejavu/database.py index e5732ff0..045a810c 100755 --- a/dejavu/database.py +++ b/dejavu/database.py @@ -10,6 +10,7 @@ class Database(object): FIELD_SONGNAME = 'song_name' FIELD_OFFSET = 'offset' FIELD_HASH = 'hash' + AUDIO_LENGTH = 'audio_length' # Name of your Database subclass, this is used in configuration # to refer to your class diff --git a/dejavu/database_sql.py b/dejavu/database_sql.py index 031bdcb4..f0a0c6cd 100755 --- a/dejavu/database_sql.py +++ b/dejavu/database_sql.py @@ -1,12 +1,22 @@ from __future__ import absolute_import -from itertools import izip_longest -import Queue -import MySQLdb as mysql -from MySQLdb.cursors import DictCursor +try: + import queue + from itertools import zip_longest +except ImportError: + import Queue as queue + from itertools import izip_longest as zip_longest +import math + +import pymysql as mysql +from pymysql.cursors import DictCursor from dejavu.database import Database +from dejavu.fingerprint import FINGERPRINT_REDUCTION + +from multiprocessing import cpu_count +from itertools import chain class SQLDatabase(Database): """ @@ -56,14 +66,14 @@ class SQLDatabase(Database): # creates CREATE_FINGERPRINTS_TABLE = """ CREATE TABLE IF NOT EXISTS `%s` ( - `%s` binary(10) not null, + `%s` binary (%s) not null, `%s` mediumint unsigned not null, `%s` int unsigned not null, INDEX (%s), UNIQUE KEY `unique_constraint` (%s, %s, %s), FOREIGN KEY (%s) REFERENCES %s(%s) ON DELETE CASCADE ) ENGINE=INNODB;""" % ( - FINGERPRINTS_TABLENAME, Database.FIELD_HASH, + FINGERPRINTS_TABLENAME, Database.FIELD_HASH, str(math.ceil(FINGERPRINT_REDUCTION/2.)), Database.FIELD_SONG_ID, Database.FIELD_OFFSET, Database.FIELD_HASH, Database.FIELD_SONG_ID, Database.FIELD_OFFSET, Database.FIELD_HASH, Database.FIELD_SONG_ID, SONGS_TABLENAME, Database.FIELD_SONG_ID @@ -75,11 +85,12 @@ class SQLDatabase(Database): `%s` varchar(250) not null, `%s` tinyint default 0, `%s` binary(20) not null, + `%s` float, PRIMARY KEY (`%s`), UNIQUE KEY `%s` (`%s`) ) ENGINE=INNODB;""" % ( SONGS_TABLENAME, Database.FIELD_SONG_ID, Database.FIELD_SONGNAME, FIELD_FINGERPRINTED, - Database.FIELD_FILE_SHA1, + Database.FIELD_FILE_SHA1, Database.AUDIO_LENGTH, Database.FIELD_SONG_ID, Database.FIELD_SONG_ID, Database.FIELD_SONG_ID, ) @@ -89,8 +100,8 @@ class SQLDatabase(Database): (UNHEX(%%s), %%s, %%s); """ % (FINGERPRINTS_TABLENAME, Database.FIELD_HASH, Database.FIELD_SONG_ID, Database.FIELD_OFFSET) - INSERT_SONG = "INSERT INTO %s (%s, %s) values (%%s, UNHEX(%%s));" % ( - SONGS_TABLENAME, Database.FIELD_SONGNAME, Database.FIELD_FILE_SHA1) + INSERT_SONG = "INSERT INTO %s (%s, %s, %s) values (%%s, UNHEX(%%s), %%s);" % ( + SONGS_TABLENAME, Database.FIELD_SONGNAME, Database.FIELD_FILE_SHA1, Database.AUDIO_LENGTH) # selects SELECT = """ @@ -107,8 +118,8 @@ class SQLDatabase(Database): """ % (Database.FIELD_SONG_ID, Database.FIELD_OFFSET, FINGERPRINTS_TABLENAME) SELECT_SONG = """ - SELECT %s, HEX(%s) as %s FROM %s WHERE %s = %%s; - """ % (Database.FIELD_SONGNAME, Database.FIELD_FILE_SHA1, Database.FIELD_FILE_SHA1, SONGS_TABLENAME, Database.FIELD_SONG_ID) + SELECT %s, HEX(%s) as %s, %s FROM %s WHERE %s = %%s; + """ % (Database.FIELD_SONGNAME, Database.FIELD_FILE_SHA1, Database.FIELD_FILE_SHA1, Database.AUDIO_LENGTH, SONGS_TABLENAME, Database.FIELD_SONG_ID) SELECT_NUM_FINGERPRINTS = """ SELECT COUNT(*) as n FROM %s @@ -234,12 +245,12 @@ def insert(self, hash, sid, offset): with self.cursor() as cur: cur.execute(self.INSERT_FINGERPRINT, (hash, sid, offset)) - def insert_song(self, songname, file_hash): + def insert_song(self, songname, file_hash, audio_length): """ Inserts song in the database and returns the ID of the inserted record. """ with self.cursor() as cur: - cur.execute(self.INSERT_SONG, (songname, file_hash)) + cur.execute(self.INSERT_SONG, (songname, file_hash, audio_length)) return cur.lastrowid def query(self, hash): @@ -272,34 +283,39 @@ def insert_hashes(self, sid, hashes): for hash, offset in hashes: values.append((hash, sid, offset)) + base_query = "INSERT IGNORE INTO fingerprints (%s, %s, %s) values " % (Database.FIELD_HASH, Database.FIELD_SONG_ID, Database.FIELD_OFFSET) with self.cursor() as cur: + values.sort(key=lambda tup: tup[0]) + cur.execute("START TRANSACTION;") for split_values in grouper(values, 1000): - cur.executemany(self.INSERT_FINGERPRINT, split_values) + split_values = list(split_values) + values2tuple = tuple(chain.from_iterable(split_values)) + query = base_query + ', '.join(['(UNHEX(%s), %s, %s)'] * len(split_values)) + query += ";" + cur.execute(query, values2tuple) + cur.execute("COMMIT;") + - def return_matches(self, hashes): + def return_matches(self, mapper): """ Return the (song_id, offset_diff) tuples associated with a list of (sha1, sample_offset) values. """ # Create a dictionary of hash => offset pairs for later lookups - mapper = {} - for hash, offset in hashes: - mapper[hash.upper()] = offset # Get an iteratable of all the hashes we need - values = mapper.keys() + values = list(mapper.keys()) with self.cursor() as cur: - for split_values in grouper(values, 1000): - # Create our IN part of the query - query = self.SELECT_MULTIPLE - query = query % ', '.join(['UNHEX(%s)'] * len(split_values)) + # Create our IN part of the query + query = self.SELECT_MULTIPLE + query = query % ', '.join(['UNHEX(%s)'] * len(values)) - cur.execute(query, split_values) + cur.execute(query, values) - for hash, sid, offset in cur: - # (sid, db_offset - song_sampled_offset) - yield (sid, offset - mapper[hash]) + for hash, sid, offset in cur: + # (sid, db_offset - song_sampled_offset) + yield (sid, offset - mapper[hash]) def __getstate__(self): return (self._options,) @@ -312,7 +328,7 @@ def __setstate__(self, state): def grouper(iterable, n, fillvalue=None): args = [iter(iterable)] * n return (filter(None, values) for values - in izip_longest(fillvalue=fillvalue, *args)) + in zip_longest(fillvalue=fillvalue, *args)) def cursor_factory(**factory_options): @@ -333,14 +349,14 @@ class Cursor(object): cur.execute(query) ``` """ - _cache = Queue.Queue(maxsize=5) def __init__(self, cursor_type=mysql.cursors.Cursor, **options): super(Cursor, self).__init__() + self._cache = queue.Queue(maxsize=cpu_count()) try: conn = self._cache.get_nowait() - except Queue.Empty: + except queue.Empty: conn = mysql.connect(**options) else: # Ping the connection before using it from the cache. @@ -352,7 +368,7 @@ def __init__(self, cursor_type=mysql.cursors.Cursor, **options): @classmethod def clear_cache(cls): - cls._cache = Queue.Queue(maxsize=5) + cls._cache = queue.Queue(maxsize=cpu_count()) def __enter__(self): self.cursor = self.conn.cursor(self.cursor_type) @@ -369,5 +385,5 @@ def __exit__(self, extype, exvalue, traceback): # Put it back on the queue try: self._cache.put_nowait(self.conn) - except Queue.Full: + except queue.Full: self.conn.close() diff --git a/dejavu/decoder.py b/dejavu/decoder.py index 04aa39f4..c77a7c35 100755 --- a/dejavu/decoder.py +++ b/dejavu/decoder.py @@ -3,8 +3,12 @@ import numpy as np from pydub import AudioSegment from pydub.utils import audioop -import wavio +import dejavu.wavio as wavio from hashlib import sha1 +from sys import version + +if int(version[0]) > 2: + xrange = range def unique_hash(filepath, blocksize=2**20): """ Small function to generate a hash to uniquely generate @@ -72,8 +76,7 @@ def read(filename, limit=None): channels = [] for chn in audiofile: channels.append(chn) - - return channels, audiofile.frame_rate, unique_hash(filename) + return channels, audiofile.frame_rate, unique_hash(filename), float(len(audiofile))/1000.0 def path_to_songname(path): diff --git a/dejavu/fingerprint.py b/dejavu/fingerprint.py index 4db321b5..a6558250 100755 --- a/dejavu/fingerprint.py +++ b/dejavu/fingerprint.py @@ -1,6 +1,5 @@ import numpy as np import matplotlib.mlab as mlab -import matplotlib.pyplot as plt from scipy.ndimage.filters import maximum_filter from scipy.ndimage.morphology import (generate_binary_structure, iterate_structure, binary_erosion) @@ -59,7 +58,11 @@ # Number of bits to throw away from the front of the SHA1 hash in the # fingerprint calculation. The more you throw away, the less storage, but # potentially higher collisions and misclassifications when identifying songs. -FINGERPRINT_REDUCTION = 20 +FINGERPRINT_REDUCTION = 40 + +# SHA1 has 40 hexadecimal chars to encode +FINGERPRINT_REDUCTION = (40 if FINGERPRINT_REDUCTION > 40 else FINGERPRINT_REDUCTION) +FINGERPRINT_REDUCTION = (FINGERPRINT_REDUCTION + 1 if FINGERPRINT_REDUCTION % 2 == 1 else FINGERPRINT_REDUCTION) def fingerprint(channel_samples, Fs=DEFAULT_FS, wsize=DEFAULT_WINDOW_SIZE, @@ -83,13 +86,13 @@ def fingerprint(channel_samples, Fs=DEFAULT_FS, arr2D[arr2D == -np.inf] = 0 # replace infs with zeros # find local maxima - local_maxima = get_2D_peaks(arr2D, plot=False, amp_min=amp_min) + local_maxima = get_2D_peaks(arr2D, amp_min=amp_min) # return hashes return generate_hashes(local_maxima, fan_value=fan_value) -def get_2D_peaks(arr2D, plot=False, amp_min=DEFAULT_AMP_MIN): +def get_2D_peaks(arr2D, amp_min=DEFAULT_AMP_MIN): # http://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.morphology.iterate_structure.html#scipy.ndimage.morphology.iterate_structure struct = generate_binary_structure(2, 1) neighborhood = iterate_structure(struct, PEAK_NEIGHBORHOOD_SIZE) @@ -101,7 +104,7 @@ def get_2D_peaks(arr2D, plot=False, amp_min=DEFAULT_AMP_MIN): border_value=1) # Boolean mask of arr2D with True at peaks - detected_peaks = local_max - eroded_background + detected_peaks = local_max ^ eroded_background # extract peaks amps = arr2D[detected_peaks] @@ -116,17 +119,6 @@ def get_2D_peaks(arr2D, plot=False, amp_min=DEFAULT_AMP_MIN): frequency_idx = [x[1] for x in peaks_filtered] time_idx = [x[0] for x in peaks_filtered] - if plot: - # scatter of the peaks - fig, ax = plt.subplots() - ax.imshow(arr2D) - ax.scatter(time_idx, frequency_idx) - ax.set_xlabel('Time') - ax.set_ylabel('Frequency') - ax.set_title("Spectrogram") - plt.gca().invert_yaxis() - plt.show() - return zip(frequency_idx, time_idx) @@ -136,6 +128,7 @@ def generate_hashes(peaks, fan_value=DEFAULT_FAN_VALUE): sha1_hash[0:20] time_offset [(e05b341a9b77a51fd26, 32), ... ] """ + peaks = list(peaks) if PEAK_SORT: peaks.sort(key=itemgetter(1)) @@ -150,6 +143,6 @@ def generate_hashes(peaks, fan_value=DEFAULT_FAN_VALUE): t_delta = t2 - t1 if t_delta >= MIN_HASH_TIME_DELTA and t_delta <= MAX_HASH_TIME_DELTA: - h = hashlib.sha1( - "%s|%s|%s" % (str(freq1), str(freq2), str(t_delta))) + h = hashlib.sha1(( + "%s|%s|%s" % (str(freq1), str(freq2), str(t_delta))).encode()) yield (h.hexdigest()[0:FINGERPRINT_REDUCTION], t1) diff --git a/dejavu/recognize.py b/dejavu/recognize.py index b43a8791..39587eb3 100755 --- a/dejavu/recognize.py +++ b/dejavu/recognize.py @@ -13,9 +13,12 @@ def __init__(self, dejavu): def _recognize(self, *data): matches = [] + total_hashes = 0 for d in data: - matches.extend(self.dejavu.find_matches(d, Fs=self.Fs)) - return self.dejavu.align_matches(matches) + extracted_matches = self.dejavu.find_matches(d, Fs=self.Fs) + total_hashes += extracted_matches[1] + matches.extend(extracted_matches[0]) + return self.dejavu.align_matches(matches, total_hashes) def recognize(self): pass # base class does nothing @@ -26,7 +29,7 @@ def __init__(self, dejavu): super(FileRecognizer, self).__init__(dejavu) def recognize_file(self, filename): - frames, self.Fs, file_hash = decoder.read(filename, self.dejavu.limit) + frames, self.Fs, file_hash, audio_length = decoder.read(filename, self.dejavu.limit) t = time.time() match = self._recognize(*frames) @@ -108,5 +111,21 @@ def recognize(self, seconds=10): return self.recognize_recording() +class NumpyArrayRecognizer(BaseRecognizer): + def __init__(self, dejavu): + super(NumpyArrayRecognizer, self).__init__(dejavu) + + def recognize_array(self, frames): + t = time.time() + match = self._recognize(*frames) + t = time.time() - t + if match: + match['match_time'] = t + return match + + def recognize(self, data): + return self.recognize_array(data) + + class NoRecordingError(Exception): pass diff --git a/dejavu/testing.py b/dejavu/testing.py index d2a3b484..7cde54c3 100644 --- a/dejavu/testing.py +++ b/dejavu/testing.py @@ -9,6 +9,10 @@ import subprocess import random import logging +from sys import version + +if int(version[0]) > 2: + xrange = range def set_seed(seed=None): """ @@ -37,7 +41,7 @@ def get_length_audio(audiopath, extension): try: audio = AudioSegment.from_file(audiopath, extension.replace(".", "")) except: - print "Error in get_length_audio(): %s" % traceback.format_exc() + print("Error in get_length_audio(): %s" % traceback.format_exc()) return None return int(len(audio) / 1000.0) @@ -75,8 +79,8 @@ def generate_test_files(src, dest, nseconds, fmts=[".mp3", ".wav"], padding=10): testsources = get_files_recursive(src, fmt) for audiosource in testsources: - print "audiosource:", audiosource - + print("audiosource:", audiosource) + filename, extension = os.path.splitext(os.path.basename(audiosource)) length = get_length_audio(audiosource, extension) starttime = get_starttime(length, nseconds, padding) @@ -96,7 +100,7 @@ def log_msg(msg, log=True, silent=False): if log: logging.debug(msg) if not silent: - print msg + print(msg) def autolabel(rects, ax): # attach some text labels @@ -120,26 +124,26 @@ def __init__(self, folder, seconds): self.test_seconds = seconds self.test_songs = [] - print "test_seconds", self.test_seconds + print ("test_seconds", self.test_seconds) self.test_files = [ f for f in os.listdir(self.test_folder) if os.path.isfile(os.path.join(self.test_folder, f)) and re.findall("[0-9]*sec", f)[0] in self.test_seconds] - print "test_files", self.test_files + print("test_files", self.test_files) self.n_columns = len(self.test_seconds) self.n_lines = int(len(self.test_files) / self.n_columns) - print "columns:", self.n_columns - print "length of test files:", len(self.test_files) - print "lines:", self.n_lines + print("columns:", self.n_columns) + print("length of test files:", len(self.test_files)) + print("lines:", self.n_lines) # variable match results (yes, no, invalid) self.result_match = [[0 for x in xrange(self.n_columns)] for x in xrange(self.n_lines)] - print "result_match matrix:", self.result_match + print("result_match matrix:", self.result_match) # variable match precision (if matched in the corrected time) self.result_matching_times = [[0 for x in xrange(self.n_columns)] for x in xrange(self.n_lines)] @@ -164,42 +168,6 @@ def get_line_id (self, song): self.test_songs.append(song) return len(self.test_songs) - 1 - def create_plots(self, name, results, results_folder): - for sec in range(0, len(self.test_seconds)): - ind = np.arange(self.n_lines) # - width = 0.25 # the width of the bars - - fig = plt.figure() - ax = fig.add_subplot(111) - ax.set_xlim([-1 * width, 2 * width]) - - means_dvj = [x[0] for x in results[sec]] - rects1 = ax.bar(ind, means_dvj, width, color='r') - - # add some - ax.set_ylabel(name) - ax.set_title("%s %s Results" % (self.test_seconds[sec], name)) - ax.set_xticks(ind + width) - - labels = [0 for x in range(0, self.n_lines)] - for x in range(0, self.n_lines): - labels[x] = "song %s" % (x+1) - ax.set_xticklabels(labels) - - box = ax.get_position() - ax.set_position([box.x0, box.y0, box.width * 0.75, box.height]) - - #ax.legend( (rects1[0]), ('Dejavu'), loc='center left', bbox_to_anchor=(1, 0.5)) - - if name == 'Confidence': - autolabel(rects1, ax) - else: - autolabeldoubles(rects1, ax) - - plt.grid() - - fig_name = os.path.join(results_folder, "%s_%s.png" % (name, self.test_seconds[sec])) - fig.savefig(fig_name) def begin(self): for f in self.test_files: @@ -246,7 +214,7 @@ def begin(self): self.result_match_confidence[line][col] = 0 else: log_msg('correct match') - print self.result_match + print(self.result_match) self.result_match[line][col] = 'yes' self.result_query_duration[line][col] = round(result[Dejavu.MATCH_TIME],3) self.result_match_confidence[line][col] = result[Dejavu.CONFIDENCE] diff --git a/requirements.txt b/requirements.txt index 9478f734..ef86eebe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,9 @@ ### BEGIN ### pydub>=0.9.4 PyAudio>=0.2.7 -numpy>=1.8.2 -scipy>=0.12.1 -matplotlib>=1.3.1 +numpy==1.13.3 +scipy==1.0.0 +matplotlib==2.1.2 +pymysql==0.9.3 ### END ### +