예제 #1
0
class tracker(MDBModule):
    def __init__(self, config, database):
        self.config = config
        self.musicdb = database
        self.fs = Filesystem(self.config.music.path)
        self.trackerdb = TrackerDatabase(self.config.tracker.dbpath)

    def GenerateDotFile(self, target, targetid, relations, dotfile):
        """
        This method generates a dot file visualizing the relations between the target and the related songs or artists.
        Also, the weights get visualized by the thickness of the edges of the generated graph.

        .. warning::

            If the file exists, its content gets replaced!

        Args:
            target (str): The target all IDs apply to. Can be ``"song"`` or ``"artist"``.
            targetid (int): ID of the song or artists, the relations belong to
            relations: A list of relations as returned by :meth:`lib.db.tracker.TrackerDatabase.GetRelations`
            dotfile (str): A path to write the dotfile to.

        Returns:
            ``True`` on success. If there is any error, ``False`` gets returned.
        """
        if target not in ["song", "artist"]:
            return False

        # give the IDs a name
        if target == "song":
            targetname = self.musicdb.GetSongById(targetid)["name"]
        elif target == "artist":
            targetname = self.musicdb.GetArtistById(targetid)["name"]
        else:
            return False

        for relation in relations:
            if target == "song":
                relation["name"] = self.musicdb.GetSongById(
                    relation["id"])["name"]
            elif target == "artist":
                relation["name"] = self.musicdb.GetArtistById(
                    relation["id"])["name"]
            else:
                return False

        dot = open(dotfile, "w")
        dot.write("digraph songenv {\n")

        # Related Song
        dot.write("\tsubgraph {\n")
        dot.write("\t\trank   = same; ")
        for relation in relations:
            dot.write("\"" + relation["name"] + "\"; ")
        dot.write("\n\t}\n")

        # center
        dot.write("\tsubgraph {\n")
        dot.write("\t\trank = same; " + targetname + ";\n")
        dot.write("\t}\n")

        dot.write("\tedge [ arrowhead=\"none\" ; len=7 ];\n\n")

        # edges
        for relation in relations:
            penwidth = max(1, int(relation["weight"] / 10))

            dot.write("\t\"" + relation["name"] + "\" -> \"" + targetname +
                      "\" [penwidth=" + str(penwidth) + "];\n")

        dot.write("}\n\n")
        dot.close()
        return True

    def ShowRelations(self, target, targetid, relations):
        """
        This method lists all entries in the relations list returned by the database for the given target ID

        Args:
            target (str): The target all IDs apply to. Can be ``"song"`` or ``"artist"``.
            targetid (int): ID of the song or artists, the relations belong to
            relations: A list of relations as returned by :meth:`lib.db.tracker.TrackerDatabase.GetRelations`

        Returns:
            ``True`` on success. If there is any error, ``False`` gets returned.
        """
        if target not in ["song", "artist"]:
            return False

        for relation in relations:
            # Get Weight
            weight = relation["weight"]

            # Get Name
            if target == "song":
                name = self.musicdb.GetSongById(relation["id"])["name"]
            elif target == "artist":
                name = self.musicdb.GetArtistById(relation["id"])["name"]
            else:
                return False

            # Get Color
            if target == "song":
                colorweight = weight
            elif target == "artist":
                colorweight = int(weight / 5)
            else:
                return False

            if colorweight <= 1:
                color = "\033[1;30m"
            elif colorweight == 2:
                color = "\033[1;34m"
            elif colorweight == 3:
                color = "\033[1;36m"
            else:
                color = "\033[1;37m"

            # Print
            print(" \033[1;35m[%2d] %s%s" % (weight, color, name))

        return True

    @staticmethod
    def MDBM_CreateArgumentParser(parserset, modulename):
        parser = parserset.add_parser(
            modulename, help="access information from the song tracker")
        parser.set_defaults(module=modulename)
        parser.add_argument("-s",
                            "--show",
                            action="store_true",
                            help="Show the related songs or artists")
        parser.add_argument(
            "-d",
            "--dot",
            action="store",
            metavar="dotfile",
            type=str,
            help=
            "if this option is given, a dot-file will be generated with the results"
        )
        parser.add_argument("--test",
                            action="store_true",
                            help="for testing - it's magic! read the code")
        parser.add_argument(
            "path",
            help=
            "Path to the song or artist on that the previos options will be applied"
        )

    # return exit-code
    def MDBM_Main(self, args):

        if args.test:
            from tqdm import tqdm
            print("\033[1;35mTranslating old table to new table …\033[0m")

            # # Translate old table to new table
            # sql = "SELECT song, successor, weight FROM graph"
            # results = self.trackerdb.GetFromDatabase(sql)
            # for result in results:
            #     for _ in range(result[2]):
            #         self.trackerdb.AddRelation("song", result[0], result[1])

            # # Generate artistrelations out of songrelations
            # sql = "SELECT songida, songidb, weight FROM songrelations"
            # results = self.trackerdb.GetFromDatabase(sql)
            # for result in tqdm(results):

            #     artista = self.musicdb.GetSongById(result[0])["artistid"]
            #     artistb = self.musicdb.GetSongById(result[1])["artistid"]

            #     for _ in range(result[2]):
            #         self.trackerdb.AddRelation("artist", artista, artistb)

            print("\033[1;32mdone!\033[0m")
            return 0

        # Genrate path relative to the music root directory - if possible
        try:
            path = self.fs.AbsolutePath(
                args.path)  # Be sure the path is absolute (resolve "./")
            path = self.fs.RemoveRoot(
                path)  # Now make a relative artist or song path
        except Exception as e:
            print(
                "\033[1;31mInvalid path. Determin relative path to the music root directory failed with error: %s",
                str(e))
            return 1

        # Identify target by path and get target ID
        if self.fs.IsFile(path):
            mdbsong = self.musicdb.GetSongByPath(path)
            if not mdbsong:
                print(
                    "\033[1;31mPath %s is a file, but it is not a song file!\033[0m"
                    % (path))
            target = "song"
            targetid = mdbsong["id"]

        elif self.fs.IsDirectory(path):
            mdbartist = self.musicdb.GetArtistByPath(path)
            if not mdbartist:
                print(
                    "\033[1;31mPath %s is a directory, but it is not an artist directory!\033[0m"
                    % (path))
            target = "artist"
            targetid = mdbartist["id"]

        else:
            print("\033[1;31mPath %s does not exist!\033[0m" % (path))
            return 1

        # Get target relation
        print(
            "\033[1;34mGetting \033[1;36m%s\033[1;34m relations from database … \033[0m"
            % (target))
        relations = self.trackerdb.GetRelations(target, targetid)
        print("\033[1;36m%d\033[1;34m entries found.\033[0m" %
              (len(relations)))

        # Apply parameters
        if args.show:
            self.ShowRelations(target, targetid, relations)

        if args.dot:
            rootfs = Filesystem()
            dotfile = rootfs.AbsolutePath(args.dot)
            self.GenerateDotFile(target, targetid, relations, dotfile)

        return 0
예제 #2
0
class musicai(MDBModule):
    def __init__(self, config, database):
        MDBModule.__init__(self)
        self.musicdb = database
        self.config  = config
        self.fs      = Filesystem(self.config.music.path)


    @staticmethod
    def MDBM_CreateArgumentParser(parserset, modulename):
        if AIModulesFound:
            logging.info("All dependencies for using AI are available.")
        else:
            logging.warning("Some dependencies to use AI are missing.")
            return

        parser = parserset.add_parser(modulename, help="Manager for the MusicDB-AI")
        parser.set_defaults(module=modulename)
        parser.add_argument("-f", "--feature", action="store_true", help="generate feature set from PATH")
        parser.add_argument("-t", "--train",   action="store_true", help="run training focusing on PATH if given")
        parser.add_argument("-p", "--predict"
            , action="store"
            , type=str
            , metavar="action"
            , help="let the AI predict the genre of PATH. Actions are \"show\" or \"store\"")
        parser.add_argument("-g", "--genre"
            , action="store"
            , type=str
            , metavar="genre"
            , help="use feature set for training for the given genre")
        parser.add_argument("-s", "--stats",   action="store_true", help="show some statistics")
        parser.add_argument(      "--test",    action="store_true", help="for testing - it's magic! Read the code!")
        parser.add_argument("path", nargs="?", help="Artist, Album or Song-Path for the songs to work with")

    
    def AnalysisThread(self, songid, songpath):
        """
        Calls the function :func:`~mdbapi.musicai.MusicAI.CreateFeatureset`. 
        This function is made to be used concurrently.

        Args:
            songid (int): ID of the song that shall be analyzed
            songpath (str): Path to the song that shall be analyzed

        Returns:
            status from the CreateFeatureset-call
        """
        musicai = MusicAI(self.config)
        print("\033[1;34m - Analysing \033[0;36m%s\033[0m" % songpath)
        logging.info("Analysing \033[0;36m%s\033[0m" % songpath)
        retval = musicai.CreateFeatureset(songid, songpath)
        return retval

    def GenerateFeatureset(self, mdbsongs):
        """
        This method creates feature sets for a list of songs.
        If one of the songs already has a feature set the song gets skipped.

        The creation is done by calling :meth:`~mod.musicai.musicai.AnalysisThread` in 6 threads in parallel.
        If an analysis fails the song gets skipped.

        Args:
            mdbsongs: A list of MusicDB song dictionaries

        Returns:
            *Nothing*
        """
        # It is faster to do this befor all the workers-environment ist set up
        print("\033[1;34mChecking for \033[0;36m%d\033[1;34m if feature-sets exists\033[0m"%(len(mdbsongs)))
        logging.info("\033[1;34mGenerating \033[0;36m%d\033[1;34m feature-sets\033[0m"%(len(mdbsongs)))
        requestlist = []
        musicai     = MusicAI(self.config)
        for mdbsong in mdbsongs:
            if musicai.HasFeatureset(mdbsong["id"]):
                print("\033[1;34m - Song \033[0;36m%s\033[1;34m already analysed\033[0m" % mdbsong["name"])
                logging.debug("Song \033[0;36m%s\033[1;30m already analysed" % mdbsong["name"])
            else:
                requestlist.append((mdbsong["id"], mdbsong["path"]))

        if len(requestlist) == 0:
            print("\033[1;34mFeature-set already generated")
            logging.info("Feature-set already generated")
            return

        print("\033[1;34mGenerating \033[0;36m%d\033[1;34m feature-sets\033[0m"%(len(requestlist)))
        logging.info("\033[1;34mGenerating \033[0;36m%d\033[1;34m feature-sets\033[0m"%(len(requestlist)))
        t_start = datetime.datetime.now()

        # do analyses IN THREADS
        errors  = 0
        with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor:
            thread = {executor.submit(self.AnalysisThread, request[0], request[1]): request for request in requestlist}
            for future in concurrent.futures.as_completed(thread):
                try:
                    retval = future.result(60)   # timeout of 1 minute
                    if retval != True:
                        raise AssertionError("generation faied!")
                except Exception as e:
                    print("\033[1;33mAnalysis Thread generated an exception: %s\033[0m" % e)
                    logging.warning("Analysis Thread generated an exception: %s" % e)
                    errors += 1

        t_diff = datetime.datetime.now() - t_start
        print("\033[1;34mFeature-set generated in \033[0;36m%s\033[0m"%(str(t_diff)))
        logging.info("\033[1;34mFeature-set generated in \033[0;36m%s\033[0m"%(str(t_diff)))
        if errors > 0:
            print("\033[1;33m%d / %d analysis failed!\033[0m"%(errors, len(mdbsongs)))
            logging.warning("%d / %d analysis failed!\033[0m"%(errors, len(mdbsongs)))


    def GenerateTrainingset(self, mdbsongs, genre):
        """
        This method creates training sets out of the feature set by augmenting them with a genre.
        If a song already has a training set, or no feature set it is skipped.

        Args:
            mdbsongs: A list of MusicDB song dictionaries
            genre (str): The genre the songs belong to
        """
        if not genre in self.genrelist:
            logging.error("The genre \"%s\" is not in the genrelist: \"%s\"!", genre, str(self.genrelist))
            print("\033[1;31mThe genre \"%s\" is not in the genrelist: \"%s\"!\033[0m"%(genre, str(self.genrelist)))
            return

        newsets = 0
        musicai = MusicAI(self.config)

        print("\033[1;34mGenerating \033[0;36m%d\033[1;34m training-sets for genre \033[0;36m%s\033[0;36m"%(len(mdbsongs), genre))
        logging.info("\033[1;34mGenerating \033[0;36m%d\033[1;34m training-sets for genre \033[0;36m%s\033[0;36m"%(len(mdbsongs), genre))

        # Add to trainingsset
        for mdbsong in tqdm(mdbsongs, unit="Songs"):
            songid = mdbsong["id"]
            
            if musicai.HasTrainingset(songid):
                print("\033[1;34mSong already registered for training! \033[1;30m(%s)\033[0m"%(mdbsong["name"]))
                logging.info("Song already registered for training! \033[1;30m(%s)\033[0m"%(mdbsong["name"]))
                continue

            if not musicai.HasFeatureset(songid):
                print("\033[1;31mSong does not have a featureset! \033[1;30m(%s)\033[0m"%(mdbsong["name"]))
                logging.error("\033[1;31mSong does not have a featureset! \033[1;30m(%s)\033[0m"%(mdbsong["name"]))
                continue

            musicai.AddSongToTrainingset(songid, genre)
            newsets += 1

        print("\033[0;36m%d\033[1;34m new training sets registered.\033[0m"%(newsets))
        logging.info("\033[0;36m%d\033[1;34m new training sets registered.\033[0m"%(newsets))


    def PerformTraining(self):
        """
        Direct interface to :meth:`mdbapi.musicai.MusicAI.PerformTraining`.

        Returns:
            *Nothing*
        """
        print("\033[1;34mStart training session\033[0m")
        logging.info("\033[1;34mStart training session\033[0m")
        musicai = MusicAI(self.config)

        # train on set
        t_start = datetime.datetime.now()
        musicai.PerformTraining()
        t_diff = datetime.datetime.now() - t_start
        print("\033[?25h\033[1;34mTraining session took \033[0;36m%s\033[0m"%(str(t_diff)))
        logging.info("\033[?25h\033[1;34mTraining session took \033[0;36m%s\033[0m"%(str(t_diff)))
        

    def PerformPrediction(self, mdbsongs):
        """
        This method performs a prediction on a list of songs.
        For each song the method :meth:`mdbapi.musicai.MusicAI.PerformPrediction` gets called.

        When calling this method after a CUDA update, the prediction of the first song will take some minutes because the network gets recompiled for the new libraries.

        The results will be returned and can then be used.
        It is a list of predictions. Each prediction is a tuple of *songid* and *confidence*.
        The confidence is a list with each element representing the confidence of one of the genres in the genre list.

        Args:
            mdbsongs: A list of MusicDB song dictionaries

        Returns:
            A set of values representing the prediction

        Example:

            .. code-block:: python

                predictionset = self.PerformPrediction(songs)
                for genreindex, genrename in enumerate(self.config.musicai.genrelist):
                    print("Genre name: %s" % (genrename))
                    for prediction in predictionset:
                        print("Song ID: %d",   % (prediction[0]))
                        print("Confidence: %d" % (prediction[1][genreindex]))
        """
        musicai = MusicAI(self.config)

        print("\033[1;34mStart prediction\033[0;36m")
        logging.info("\033[1;34mStart prediction\033[0;36m")
        predictionset = []
        t_start = datetime.datetime.now()
        for mdbsong in tqdm(mdbsongs, unit="Songs"):
            prediction = musicai.PerformPrediction(mdbsong["id"])
            if prediction == None:
                print("\033[1;31mNo feature set available!\033[0;36m")
                continue
            predictionset.append((mdbsong["id"], prediction))

        t_diff = datetime.datetime.now() - t_start
        print("\033[?25h\033[1;34mPrediction took \033[0;36m%s\033[0m"%(str(t_diff)))
        logging.info("\033[?25h\033[1;34mPrediction took \033[0;36m%s\033[0m"%(str(t_diff)))
        return predictionset

    def ShowPrediction(self, mdbsongs, predictionset):
        """
        The results will be printed in a table.
        The table shows the confidence of a song (abscissa) for each genre (ordinate).
        The last column shows the mean value of all songs that can be seen as genre-prediction for an Album or an Artist.

        The predictionset must be a list of tuple *(Song ID , Confidence)*

        Args:
            mdbsongs: A list of MusicDB song dictionaries
            predictionset: Set of predictions as returned by :meth:`~mod.musicai.musicai.PerformPrediction`

        Returns:
            *Nothing*
        """

        def Colormapper(value):
            prefix = "\033[1;"
            if value >= 0.79:
                return prefix + "37m"
            elif value >= 0.49:
                return prefix + "36m"
            elif value >= 0.29:
                return prefix + "34m"
            return prefix + "30m"

        # Print single song prediction
        print("\033[1;44;37m", end="")
        for genre in self.config.musicai.genrelist:
            print("%s" % (genre.ljust(5)[:5]), end=" ")
        print((" " * 2) + "Song name \033[0m")

        for index, mdbsong in enumerate(mdbsongs):
            # in predictionset[index][0] is the song ID - that should match with the current song
            try:
                probability = next(p[1] for p in predictionset if p[0] == mdbsong["id"])
            except StopIteration:
                # There may be no analysis of some songs because they are too short (like Intros)
                continue

            for prob in probability:
                print("%s%.3f"%(Colormapper(prob), prob), end=" ")
            print(" \033[1;34m%s"%(mdbsong["name"]))

        # Print song IDs for the results (table headline)
        print("\033[1;44;37mSong ID:  ", end="")
        for mdbsong in mdbsongs:
            print("%4i" % (mdbsong["id"]), end=" ")
        print((" " * 2) + "Album \033[0m")

        # Print single results
        for index, genre in enumerate(self.config.musicai.genrelist):
            print("\033[1;36m%s\033[1;34m"%(genre.ljust(7)), end=" | ")
            mean = 0
            for mdbsong in mdbsongs:
                try:
                    probabilityset = next(p[1] for p in predictionset if p[0] == mdbsong["id"])
                    probability    = probabilityset[index]
                except StopIteration:
                    # There may be no analysis of some songs because they are too short (like Intros)
                    probability = 0
                    print("%s%s"%("\033[0;33m", "None"), end=" ")
                else:
                    color = Colormapper(probability)
                    print("%s%.2f"%(color, probability), end=" ")
                mean += probability

            if predictionset:
                mean /= len(predictionset)
                color = Colormapper(mean)
                print("\033[1;34m| %s%.3f\033[0m"%(color, mean))
            else:
                print("\033[1;34m| \033[0;33mNo feature set found\033[0m")


    def StorePrediction(self, predictionset):
        """
        This method stores the predicted genres into the MusicDatabase.
        Son´, for each song, its genre gets stored with *approval* set to 0 (Tag set by AI), and the *confidence* set to the prediction confidence of the AI.
        The threshold of confidence so that the tag gets set is 0.3.

        Args:
            predictionset: Set of predictions as returned by :meth:`~mod.musicai.musicai.PerformPrediction`

        Returns:
            *Nothing*
        """
        # Get all necessary tags from the database
        genretags = {}
        for genrename in self.config.musicai.genrelist:
            mdbgenre = self.musicdb.GetTagByName(genrename, MusicDatabase.TAG_CLASS_GENRE)
            
            if not mdbgenre:
                logging.error("The genre \"%s\" does not exist in database!", genre)
                print("\033[1;31mThe genre \"%s\" does not exist in database!\033[0m"%(genre))
                return

            genretags[genrename] = mdbgenre

        for prediction in predictionset:
            songid      = prediction[0]
            confidences = prediction[1]

            for index, genre in enumerate(self.config.musicai.genrelist):
                confidence = confidences[index]

                if confidence >= 0.3:
                    genretag = genretags[genre]
                    self.musicdb.SetTargetTag("song", songid, genretag["id"], 0, confidence)


    def ShowStatistics(self):
        """
        This method prints the number of songs in the training set and the size of the training sets.

        Returns:
            *Nothing*
        """
        musicai = MusicAI(self.config)
        stats = musicai.GetStatistics()
        print("\033[1;34mTrainingset Size:  \033[1;36m%7i \033[0;34mFeatures\033[0m" 
                % (stats["setsize"]))
        print("\033[1;34mNum. Songs in Set: \033[1;36m%7i \033[0;34mSongs\033[0m" 
                % (stats["numofsongs"]))



    def GetSongsFromPath(self, path):
        """
        This method returns a list of MusicDB songs depending if the path points to a single song, an album or an artist.
        If the path is invalid ``None`` will be returned.

        Args:
            path (str): a path to a song, an album or an artist

        Returns:
            List of MusicDB songs if *path* is valid, otherwise ``None``
        """
        if not path:
            return None

        path = os.path.abspath(path)

        if not os.path.exists(path):
            print("\033[1;31mERROR: Path "+path+" does not exist!\033[0m")
            return None

        try:
            path = self.fs.RemoveRoot(path)
        except:
            print("\033[1;31mERROR: Path "+path+" is not part of the music collection!\033[0m")
            return None

        print("\033[1;34mCollecting songs from "+path)
        # Get song-pathes and ids from path
        if self.fs.IsArtistPath(path, self.config.music.ignorealbums, self.config.music.ignoresongs):
            mdbartist = self.musicdb.GetArtistByPath(path)
            mdbsongs  = self.musicdb.GetSongsByArtistId(mdbartist["id"])
            print("\033[1;34mWorking on \033[1;37mArtist-path\033[1;34m for artist \033[1;36m%s\033[0m"%(mdbartist["name"]))

        elif self.fs.IsAlbumPath(path, self.config.music.ignoresongs):
            mdbalbum = self.musicdb.GetAlbumByPath(path)
            mdbsongs = self.musicdb.GetSongsByAlbumId(mdbalbum["id"])
            print("\033[1;34mWorking on \033[1;37mAlbum-path\033[1;34m for album \033[1;36m%s\033[0m"%(mdbalbum["name"]))

        elif self.fs.IsSongPath(path):
            mdbsong  = self.musicdb.GetSongByPath(path)
            mdbsongs = [mdbsong]
            print("\033[1;34mWorking on \033[1;37mSong-path\033[1;34m for song \033[1;36m%s\033[0m"%(mdbsong["name"]))

        else:
            print("\033[1;31mERROR: Path does not address a Song, Album or Artist\033[0m")
            return None

        return mdbsongs


    # return exit-code
    def MDBM_Main(self, args):

        if not AIModulesFound:
            logging.error("Some dependencies to use AI are missing!")
            return 1

        if args.test:
            musicai = MusicAI(self.config)
            musicai.GetGenreMatrix()
            #print("\033[1;35mDoing a performance-test …\033[0m")
            #musicai = MusicAI(self.config)
            #x = musicai.GetTrainingset()
            return 0


        # Check parameters
        mdbsongs = self.GetSongsFromPath(args.path)
        if not mdbsongs:
            if args.feature or args.genre or args.predict:
                print("\033[1;31mThe path does not address songs. A path to a song, album or artist is needed for some given parameters!\033[0m")
                logging.error("The path does not address songs. A path to a song, album or artist is needed for some given parameters!")
                return 1

        # generate featureset
        if args.feature:
            self.GenerateFeatureset(mdbsongs)

        # generate trainingset
        if args.genre:
            self.GenerateTrainingset(mdbsongs, args.genre)

        # perform training
        if args.train:
            self.PerformTraining()

        # run prediction
        if args.predict:
            prediction = self.PerformPrediction(mdbsongs)
            if args.predict == "show":
                self.ShowPrediction(mdbsongs, prediction)
            elif args.predict == "store":
                self.StorePrediction(prediction)

        # show some statistics 
        if args.stats:
            self.ShowStatistics()


        return 0