          frames_and_faces_dir = ("\n\tMust Pass in a frames folder AND a faces"
                                  "\n\tfolder (-fr and -fc).")
          output_opts = "\n\tUse the output option (-o) to process\n\tresults."
          argument_list = list()
          argument_list.append({
              "opts": ("-j", "--job"),
              "type": str,
              "choices": ("draw", "extract", "manual",
 
</s> remove         frames_or_faces_dir = ("\n\tMust Pass in either a frames folder \n\t"
                               "OR a faces folder (-fr or -fc).")
        frames_and_faces_dir = ("\n\tMust Pass in a frames folder AND a\n\t"
                                "faces folder (-fr and -fc).")
 </s> add         frames_or_faces_dir = ("\n\tMust Pass in either a frames folder"
                               "\n\tOR a faces folder (-fr or -fc).")
        frames_and_faces_dir = ("\n\tMust Pass in a frames folder AND a faces"
                                "\n\tfolder (-fr and -fc).") </s> remove         argument_list.append({"opts": ("-j", "--job"),
                              "type": str,
                              "choices": ("draw", "extract",
                                          "missing-alignments",
                                          "missing-frames", "leftover-faces",
                                          "multi-faces", "no-faces",
                                          "reformat", "remove"),
                              "required": True,
                              "help": "R|Choose which action you want to "
                                      "perform.\nNB: All actions require an "
                                      "alignments file (-a) to\nbe passed "
                                      "in.\n"
                                      "'draw': Draw landmarks on frames in "
                                      "the selected\n\tfolder. A subfolder "
                                      "will be created within\n\tthe frames "
                                      "folder to hold the output." +
                                      frames_dir + "\n"
                                      "'extract': Re-extract faces from the "
                                      "source frames\n\tbased on alignment "
                                      "data. This is a\n\tlot quicker than "
                                      "re-detecting faces. Can\n\toptionally "
                                      "use the align-eyes switch (-ae)." +
                                      frames_and_faces_dir + "\n"
                                      "'missing-alignments': Identify frames "
                                      "that do not\n\texist in the "
                                      "alignments file." + output_opts +
                                      frames_dir + "\n"
                                      "'missing-frames': Identify frames in "
                                      "the alignments\n\tfile that do not "
                                      "appear within the frames\n\t"
                                      "folder." + output_opts +
                                      frames_dir + "\n" +
                                      "'leftover-faces': Identify faces in "
                                      "the faces folder\n\tthat do not exist "
                                      "in the alignments file." + output_opts +
                                      faces_dir + "\n"
                                      "'multi-faces': Identify where multiple "
                                      "faces exist\n\twithin the alignments "
                                      "file." + output_opts +
                                      frames_or_faces_dir + "\n"
                                      "'no-faces': Identify frames that exist "
                                      "within the\n\talignment file but no "
                                      "faces were detected." + output_opts +
                                      frames_dir + "\n"
                                      "'reformat': Save a copy of alignments "
                                      "file in a\n\tdifferent format. "
                                      "Specify a format with\n\tthe -fmt "
                                      "option.\n\tAlignments can be "
                                      "converted from\n\tDeepFaceLab by "
                                      "specifing:\n\t    -a dfl\n\t    -fc "
                                      "<source faces folder>\n"
                                      "'remove': Remove deleted faces from "
                                      "an alignments\n\tfile. The original "
                                      "alignments file will be\n\tbacked up. "
                                      "A different file format for the\n\t"
                                      "alignments file can optionally be\n\t"
                                      "specified (-fmt).\n\tMust pass in a "
                                      "faces folder (-fc).\n"})
 </s> add         argument_list.append({
            "opts": ("-j", "--job"),
            "type": str,
            "choices": ("draw", "extract", "manual",
                        "missing-alignments", "missing-frames",
                        "leftover-faces", "multi-faces", "no-faces",
                        "reformat", "remove-faces", "remove-frames",
                        "rotate", "sort-x", "sort-y"),
            "required": True,
            "help": "R|Choose which action you want to perform.\n"
                    "NB: All actions require an alignments file (-a) to"
                    "\nbe passed in."
                    "\n'draw': Draw landmarks on frames in the selected"
                    "\n\tfolder. A subfolder will be created within"
                    "\n\tthe frames folder to hold the output." +
                    frames_dir + align_eyes +
                    "\n'extract': Re-extract faces from the source frames"
                    "\n\tbased on alignment data. This is a"
                    "\n\tlot quicker than re-detecting faces." +
                    frames_and_faces_dir + align_eyes +
                    "\n'manual': Manually view and edit landmarks." +
                    frames_dir + align_eyes +
                    "\n'missing-alignments': Identify frames that do not"
                    "\n\texist in the alignments file." + output_opts +
                    frames_dir +
                    "\n'missing-frames': Identify frames in the alignments"
                    "\n\tfile that do not appear within the frames"
                    "\n\tfolder." + output_opts + frames_dir +
                    "\n'leftover-faces': Identify faces in the faces"
                    "\n\tfolder that do not exist in the alignments file."
                    + output_opts + faces_dir +
                    "\n'multi-faces': Identify where multiple faces exist"
                    "\n\twithin the alignments file." + output_opts +
                    frames_or_faces_dir +
                    "\n'no-faces': Identify frames that exist within the"
                    "\n\talignment file but no faces were detected." +
                    output_opts + frames_dir +
                    "\n'reformat': Save a copy of alignments file in a"
                    "\n\tdifferent format. Specify a format with"
                    "\n\tthe -fmt option."
                    "\n\tAlignments can be converted from"
                    "\n\tDeepFaceLab by specifing:"
                    "\n\t    -a dfl"
                    "\n\t    -fc <source faces folder>"
                    "\n'remove-faces': Remove deleted faces from an"
                    "\n\talignments file. The original alignments file"
                    "\n\t will be backed up. A different file format or"
                    "\n\tthe alignments file can optionally be specified"
                    "\n\t(-fmt)." + faces_dir +
                    "\n'remove-frames': Remove deleted frames from an"
                    "\n\talignments file. The original alignments file"
                    "\n\twill be backed up. A different file format for"
                    "\n\tthe alignments file can optionally be specified"
                    "\n\t(-fmt)." + frames_dir +
                    "\n'rotate' - Rotate landmarks and bounding boxes. Legacy"
                    "\n\talignments files hold an 'r' parameter indicating"
                    "\n\tthat the image needs to be rotated for convert. This"
                    "\n\tmeans that the stored landarks and bounding box do"
                    "\n\tnot correspond to the actual frame. This command"
                    "\n\tupdates the alignments file with the correct"
                    "\n\tlandmarks and bounding boxes and removes the 'r'"
                    "\n\tparameter." + frames_dir +
                    "\n'sort-x' - Re-index the alignments from left to"
                    "\n\tright. For alignments with multiple faces this will"
                    "\n\tensure that the left-most face is at index 0"
                    "\n\tOptionally pass in a faces folder (-fc) to also"
                    "\n\trename extracted faces."
                    "\n'sort-y' - Re-index the alignments from top to"
                    "\n\tbottom. For alignments with multiple faces this will"
                    "\n\tensure that the top-most face is at index 0"
                    "\n\tOptionally pass in a faces folder (-fc) to also"
                    "\n\trename extracted faces."}) </s> remove         argument_list.append({"opts": ("-o", "--output"),
                              "type": str,
                              "choices": ("console", "file", "move"),
                              "default": "console",
                              "help": "R|How to output discovered items "
                                      "('faces'\nand 'frames' only):\n"
                                      "'console': Print the list of frames to "
                                      "the screen.\n\t(DEFAULT)\n"
                                      "'file': Output the list of frames to a "
                                      "text file\n\t(stored within the source "
                                      "directory).\n"
                                      "'move': Move the discovered items to a "
                                      "sub-folder\n\twithin the source "
                                      "directory."})
 </s> add         argument_list.append({
            "opts": ("-o", "--output"),
            "type": str,
            "choices": ("console", "file", "move"),
            "default": "console",
            "help": "R|How to output discovered items ('faces' and"
                    "\n'frames' only):"
                    "\n'console': Print the list of frames to the screen."
                    "\n\t(DEFAULT)"
                    "\n'file': Output the list of frames to a text file"
                    "\n\t(stored within the source directory)."
                    "\n'move': Move the discovered items to a sub-folder"
                    "\n\twithin the source directory."}) </s> remove             print("{} is not a supported serializer. "
                  "Exiting".format(self.src_format))
            exit(0)

        if self.verbose:
            print("Destination format set to {}".format(dst_fmt))

        return dst_fmt

    def set_destination_serializer(self):
        """ set the destination serializer """
        self.serializer = Serializer.get_serializer(self.dst_format)

    def load(self):
        """ Read the alignments data from the correct format """
        print("Loading alignments from {}".format(self.file))
        with open(self.file, self.serializer.roptions) as align:
            alignments = self.serializer.unmarshal(align.read())
        return alignments

    def save_alignments(self):
        """ Backup copy of old alignments and save new alignments """
        dst = os.path.splitext(self.file)[0]
        dst += ".{}".format(self.serializer.ext)
        self.backup_alignments()

        print("Saving alignments to {}".format(dst))
        with open(dst, self.serializer.woptions) as align:
            align.write(self.serializer.marshal(self.alignments))

    def backup_alignments(self):
        """ Backup copy of old alignments """
        if not os.path.isfile(self.file):
            return
        now = datetime.now().strftime("%Y%m%d_%H%M%S")
        src = self.file
        dst = src.split(".")
        dst[0] += "_" + now + "."
        dst = dst[0] + dst[1]
        print("Backing up original alignments to {}".format(dst))
        os.rename(src, dst)

    def get_alignments_one_image(self):
        """ Return the face alignments for one image """
        for image, alignments in self.alignments.items():
            image_stripped = image[:image.rindex(".")]
            number_alignments = len(alignments)
            yield image_stripped, alignments, number_alignments

    @staticmethod
    def get_one_alignment_index_reverse(image_alignments, number_alignments):
        """ Return the correct original index for
            alignment in reverse order """
        for idx, _ in enumerate(reversed(image_alignments)):
            original_idx = number_alignments - 1 - idx
            yield original_idx

    def has_alignments(self, filename, alignments):
        """ Check whether this frame has alignments """
        if not alignments:
            if self.verbose:
                print("Skipping {} - Alignments not found".format(filename))
            return False
        return True


class MediaLoader():
    """ Class to load filenames from folder """
    def __init__(self, folder, verbose):
        print("\n[{} DATA]".format(self.__class__.__name__.upper()))
        self.verbose = verbose
        self.folder = folder
        self.check_folder_exists()
        self.file_list_sorted = self.sorted_items()
        self.items = self.load_items()
        self.count = len(self.file_list_sorted)
        if self.verbose:
            print("{} items loaded".format(self.count))

    def check_folder_exists(self):
        """ makes sure that the faces folder exists """
        err = None
        loadtype = self.__class__.__name__
        if not self.folder:
            err = "ERROR: A {} folder must be specified".format(loadtype)
        elif not os.path.isdir(self.folder):
            err = ("ERROR: The {} folder {} could not be "
                   "found".format(loadtype, self.folder))
        if err:
            print(err)
            exit(0)

        if self.verbose:
            print("Folder exists at {}".format(self.folder))

    @staticmethod
    def valid_extension(filename):
        """ Check whether passed in file has a valid extension """
        extension = os.path.splitext(filename)[1]
        return bool(extension in _image_extensions)

    @staticmethod
    def sorted_items():
        """ Override for specific folder processing """
        return list()

    @staticmethod
    def process_folder():
        """ Override for specific folder processing """
        return list()

    @staticmethod
    def load_items():
        """ Override for specific item loading """
        return dict()

    def load_image(self, filename):
        """ Load an image """
        src = os.path.join(self.folder, filename)
        image = imread(src)
        return image

    @staticmethod
    def save_image(output_folder, filename, image):
        """ Save an image """
        output_file = os.path.join(output_folder, filename)
        imwrite(output_file, image)


class Faces(MediaLoader):
    """ Object to hold the faces that are to be swapped out """

    def process_folder(self):
        """ Iterate through the faces dir pulling out various information """
        print("Loading file list from {}".format(self.folder))
        for face in os.listdir(self.folder):
            if not self.valid_extension(face):
                continue
            filename = os.path.splitext(face)[0]
            file_extension = os.path.splitext(face)[1]
            index = int(filename[filename.rindex("_") + 1:])
            original_file = "{}".format(filename[:filename.rindex("_")])
            yield (filename, file_extension, original_file, index)

    def load_items(self):
        """ Load the face names into dictionary """
        faces = dict()
        for face in self.file_list_sorted:
            original_file, index = face[2:4]
            if faces.get(original_file, "") == "":
                faces[original_file] = [index]
            else:
                faces[original_file].append(index)
        return faces

    def sorted_items(self):
        """ Return the items sorted by filename then index """
        return sorted([item for item in self.process_folder()],
                      key=lambda x: (x[2], x[3]))


class Frames(MediaLoader):
    """ Object to hold the frames that are to be checked against """

    def process_folder(self):
        """ Iterate through the frames dir pulling the base filename """
        print("Loading file list from {}".format(self.folder))
        for frame in os.listdir(self.folder):
            if not self.valid_extension(frame):
                continue
            filename = os.path.basename(frame)
            yield filename

    def load_items(self):
        """ Load the frame info into dictionary """
        frames = dict()
        for frame in self.file_list_sorted:
            frames[frame] = (frame[:frame.rfind(".")],
                             frame[frame.rfind("."):])
        return frames

    def sorted_items(self):
        """ Return the items sorted by filename """
        return sorted([item for item in self.process_folder()])


class Draw():
    """ Draw Alignments on passed in images """
    def __init__(self, alignments, arguments):
        self.verbose = arguments.verbose
        self.alignments = alignments
        self.frames = Frames(arguments.frames_dir, self.verbose)
        self.output_folder = self.set_output()

    def set_output(self):
        """ Set the output folder path """
        now = datetime.now().strftime("%Y%m%d_%H%M%S")
        folder_name = "drawn_landmarks_{}".format(now)
        output_folder = os.path.join(self.frames.folder, folder_name)
        os.makedirs(output_folder)
        return output_folder

    def process(self):
        """ Run the draw alignments process """
        print("\n[DRAW LANDMARKS]")  # Tidy up cli output
        frames_drawn = 0
        for frame, alignments in tqdm(self.get_frame_alignments(),
                                      desc="Drawing landmarks",
                                      total=self.frames.count):
            if not self.alignments.has_alignments(frame, alignments):
                continue
            self.annotate_image(frame, alignments)
            frames_drawn += 1
        print("{} Frame(s) output".format(frames_drawn))

    def get_frame_alignments(self):
        """ Retrieve each frame and it's corresponding alignments """
        for frame in self.frames.file_list_sorted:
            alignments = self.alignments.alignments.get(frame, None)
            yield frame, alignments

    def annotate_image(self, frame, alignments):
        """ Draw the alignments """
        image = self.frames.load_image(frame)
        for alignment in alignments:
            self.draw_bounding_box(image, alignment)
            self.draw_landmarks(image, alignment["landmarksXY"])
        self.frames.save_image(self.output_folder, frame, image)

    @staticmethod
    def draw_bounding_box(image, alignment):
        """ Draw the bounding box around face """
        top_left = (alignment["x"], alignment["y"])
        bottom_right = (alignment["x"] + alignment["w"],
                        alignment["y"] + alignment["h"])
        rectangle(image, top_left, bottom_right, (0, 0, 255), 1)

    @staticmethod
    def draw_landmarks(image, landmarks):
        """ Draw the facial landmarks """
        for (pos_x, pos_y) in landmarks:
            circle(image, (pos_x, pos_y), 1, (0, 255, 0), -1)


class Reformat():
    """ Reformat Alignment file """
    def __init__(self, alignments, arguments):
        self.verbose = arguments.verbose
        self.alignments = alignments
        if self.alignments.src_format == "dfl":
            self.faces = Faces(arguments.faces_dir,
                               self.verbose)

    def process(self):
        """ Run reformat """
        print("\n[REFORMAT ALIGNMENTS]")  # Tidy up cli output
        if self.alignments.src_format == "dfl":
            self.alignments.alignments = self.load_dfl()
            self.alignments.file = os.path.join(self.faces.folder,
                                                "alignments.json")
        self.alignments.save_alignments()

    def load_dfl(self):
        """ Load alignments from DeepFaceLab and format for Faceswap """
        alignments = dict()
        for face in self.faces.file_list_sorted:
            if face[1] != ".png":
                if self.verbose:
                    print("{} is not a png. "
                          "Skipping".format(face[0] + face[1]))
                continue

            fullpath = os.path.join(self.faces.folder, face[0] + face[1])
            dfl = self.get_dfl_alignment(fullpath)

            if not dfl:
                continue

            self.convert_dfl_alignment(dfl, alignments)
        return alignments

    @staticmethod
    def get_dfl_alignment(filename):
        """ Process the alignment of one face """
        with open(filename, "rb") as dfl:
            header = dfl.read(8)
            if header != b"\x89PNG\r\n\x1a\n":
                print("ERROR: No Valid PNG header: {}".format(filename))
                return None
            while True:
                chunk_start = dfl.tell()
                chunk_hdr = dfl.read(8)
                if not chunk_hdr:
                    break
                chunk_length, chunk_name = struct.unpack("!I4s", chunk_hdr)
                dfl.seek(chunk_start, os.SEEK_SET)
                if chunk_name == b"fcWp":
                    chunk = dfl.read(chunk_length + 12)
                    return pickle.loads(chunk[8:-4])
                else:
                    dfl.seek(chunk_length+12, os.SEEK_CUR)
            print("ERROR: Couldn't find DFL alignments: {}".format(filename))

    @staticmethod
    def convert_dfl_alignment(dfl_alignments, alignments):
        """ Add DFL Alignments to alignments in Faceswap format """
        sourcefile = dfl_alignments["source_filename"]
        if not alignments.get(sourcefile, None):
            alignments[sourcefile] = list()

        left, top, right, bottom = dfl_alignments["source_rect"]
        alignment = {"r": 0,
                     "x": left,
                     "w": right - left,
                     "y": top,
                     "h": bottom - top,
                     "landmarksXY": dfl_alignments["source_landmarks"]}

        alignments[sourcefile].append(alignment)


class Extract():
    """ Re-extract faces from source frames based on
        Alignment data """
    def __init__(self, alignments, arguments):
        self.verbose = arguments.verbose
        self.alignments = alignments
        self.faces_dir = arguments.faces_dir
        self.align_eyes = arguments.align_eyes
        self.frames = Frames(arguments.frames_dir, self.verbose)
        self.extractor = None

    class DetectedFace():
        """ Detected face and landmark information """
        def __init__(self, image, r, x, w, y, h, landmarksXY):
            self.image = image
            self.r = r
            self.x = x
            self.w = w
            self.y = y
            self.h = h
            self.landmarksXY = landmarksXY

        def landmarks_as_xy(self):
            """ Landmarks as XY """
            return self.landmarksXY

    def process(self):
        """ Run extraction """
        print("\n[EXTRACT FACES]")  # Tidy up cli output
        self.check_folder()
        self.extractor = PluginLoader.get_extractor("Align")()
        self.export_faces()

    def check_folder(self):
        """ Check that the faces folder doesn't pre-exist
            and create """
        err = None
        if not self.faces_dir:
            err = "ERROR: Output faces folder not provided."
        elif os.path.isdir(self.faces_dir):
            err = "ERROR: Folder already exists at {}".format(self.faces_dir)
        if err:
            print(err)
            exit(0)
        if self.verbose:
            print("Creating output folder at {}".format(self.faces_dir))
        os.makedirs(self.faces_dir)

    def export_faces(self):
        """ Export the faces """
        extracted_faces = 0
        for frame, frame_info, alignments in tqdm(self.get_frame_alignments(),
                                                  desc="Extracting faces",
                                                  total=self.frames.count):
            if not self.alignments.has_alignments(frame, alignments):
                continue
            extracted_faces += self.output_faces(frame,
                                                 frame_info,
                                                 alignments)
        print("{} face(s) extracted".format(extracted_faces))

    def get_frame_alignments(self):
        """ Return the alignments for each frame """
        for key, value in self.frames.items.items():
            alignments = self.alignments.alignments.get(key, None)
            yield key, value, alignments

    def output_faces(self, frame, frame_info, alignments):
        """ Output the frame's faces to file """
        face_count = 0
        image = self.frames.load_image(frame)
        name, extension = frame_info
        for idx, alignment in enumerate(alignments):
            face = self.DetectedFace(image,
                                     alignment["r"],
                                     alignment["x"],
                                     alignment["w"],
                                     alignment["y"],
                                     alignment["h"],
                                     alignment["landmarksXY"])
            resized_face, _ = self.extractor.extract(image,
                                                     face,
                                                     256,
                                                     self.align_eyes)
            output = "{}_{}{}".format(name, str(idx), extension)
            self.frames.save_image(self.faces_dir, output, resized_face)
            face_count += 1
        return face_count


class RemoveAlignments():
    """ Remove items from alignments file """
    def __init__(self, alignments, arguments):
        self.verbose = arguments.verbose
        self.alignments = alignments
        self.faces = Faces(arguments.faces_dir, self.verbose)
        self.removed = set()

    def process(self):
        """ run removal """
        print("\n[REMOVE ALIGNMENTS DATA]")  # Tidy up cli output
        del_count = 0
        for item in tqdm(self.alignments.get_alignments_one_image(),
                         desc="Processing alignments file",
                         total=self.alignments.count):
            if self.faces_count_matches(item):
                continue
            del_count += self.remove_alignment(item)

        if del_count == 0:
            print("No changes made to alignments file. Exiting")
            return

        print("{} alignments(s) were removed from "
              "alignments file".format(del_count))
        self.alignments.save_alignments()
        self.rename_faces()

    def faces_count_matches(self, item):
        """ Check the selected face exits """
        image_name, number_alignments = item[0], item[2]
        number_faces = len(self.faces.items.get(image_name, list()))
        return bool(number_alignments == 0
                    or number_alignments == number_faces)

    def remove_alignment(self, item):
        """ Remove the alignment from the alignments file """
        del_count = 0
        image_name, alignments, number_alignments = item
        processor = self.alignments.get_one_alignment_index_reverse
        for idx in processor(alignments, number_alignments):
            face_indexes = self.faces.items.get(image_name, [-1])
            if idx not in face_indexes:
                del alignments[idx]
                self.removed.add(image_name)
                if self.verbose:
                    print("Removed alignment data for image:{} "
                          "index: {}".format(image_name, str(idx)))
                del_count += 1
        return del_count

    def rename_faces(self):
        """ Rename the aligned faces to match their "
            new index in alignments file """
        current_image = ""
        current_index = 0
        rename_count = 0
        for item in tqdm(self.faces.file_list_sorted,
                         desc="Renaming aligned faces",
                         total=self.faces.count):
            filename, extension, original_file, index = item
            if original_file not in self.removed:
                continue
            current_index, current_image = self.set_image_index(current_index,
                                                                current_image,
                                                                original_file)
            if current_index != index:
                rename_count += self.rename_file(filename,
                                                 extension,
                                                 current_image,
                                                 current_index)

            current_index += 1
        if rename_count == 0:
            print("No files were renamed. Exiting")
            return
        print("{} face(s) were renamed to match with "
              "alignments file".format(rename_count))

    @staticmethod
    def set_image_index(index, current, original):
        """ Set the current processing image and index """
        idx = 0 if current != original else index
        return idx, original

    def rename_file(self, filename, extension, image, index):
        """ Rename the selected file """
        old_file = filename + extension
        new_file = "{}_{}{}".format(image, str(index), extension)
        src = os.path.join(self.faces.folder, old_file)
        dst = os.path.join(self.faces.folder, new_file)
        os.rename(src, dst)
        if self.verbose:
            print("Renamed {} to {}".format(src, dst))
        return 1


class Check():
    """ Frames and faces checking tasks """
    def __init__(self, alignments, arguments):
        self.alignments_data = alignments.count_per_frame
        self.job = arguments.job
        self.type = None
        self.output = arguments.output
        self.source_dir = self.get_source_dir(arguments)
        self.items = self.get_items(arguments)

        self.items_output = []
        self.items_discovered = 0
        self.output_message = ""

    def get_source_dir(self, arguments):
        """ Set the correct source dir """
        if hasattr(arguments, "faces_dir") and arguments.faces_dir:
            self.type = "faces"
            source_dir = arguments.faces_dir
        elif hasattr(arguments, "frames_dir") and arguments.frames_dir:
            self.type = "frames"
            source_dir = arguments.frames_dir
        else:
            print("No source folder (-fr or -fc) was provided")
            exit(0)
        return source_dir

    def get_items(self, arguments):
        """ Set the correct items to process """
        items = Frames
        if self.type == "faces":
            items = Faces
        return items(self.source_dir, arguments.verbose).file_list_sorted

    def process(self):
        """ Process the frames check against the alignments file """
        print("\n[CHECK {}]".format(self.type.upper()))
        self.validate()
        self.compile_output()
        self.output_results()

    def validate(self):
        """ Check that the selected type is valid for
            selected task and job """
        if self.job == "missing-frames" and self.output == "move":
            print("WARNING: missing_frames was selected with move output, but "
                  "there will be nothing to move. "
                  "Defaulting to output: console")
            self.output = "console"
        elif self.type == "faces" and self.job not in ("multi-faces",
                                                       "leftover-faces"):
            print("WARNING: The selected folder is not valid. "
                  "Only folder set with '-fc' is supported for "
                  "'multi-faces' and 'leftover-faces'")
            exit(0)

    def compile_output(self):
        """ Compile list of frames that meet criteria """
        action = self.job.replace("-", "_")
        processor = getattr(self, "get_{}".format(action))
        self.items_output = [item for item in processor()]

    def get_no_faces(self):
        """ yield each frame that has no face match in alignments file """
        self.output_message = "Frames with no faces"
        for item in self.items:
            if self.alignments_data.get(item, -1) == 0:
                yield item

    def get_multi_faces(self):
        """ yield each frame that has multiple faces
            matched in alignments file """
        self.output_message = "Frames with multiple faces"
        items = self.items
        if self.type == "faces":
            self.output_message = "Multiple faces in frame"
        for item in items:
            check_item = item
            return_item = item
            if self.type == "faces":
                check_item = str(item[2]) + str(item[1])
                return_item = str(item[0]) + str(item[1])
            if self.alignments_data.get(check_item, -1) > 1:
                yield return_item

    def get_missing_alignments(self):
        """ yield each frame that does not exist in alignments file """
        self.output_message = "Frames missing from alignments file"
        exclude_filetypes = ["yaml", "yml", "p", "json", "txt"]
        for item in self.items:
            extension = item[item.rindex(".") + 1:]
            if (extension not in exclude_filetypes
                    and self.alignments_data.get(item, -1)) == -1:
                yield item

    def get_missing_frames(self):
        """ yield each frame in alignments that does
            not have a matching file """
        self.output_message = "Missing frames that are in alignments file"
        for item in self.alignments_data.keys():
            if item not in self.items:
                yield item

    def get_leftover_faces(self):
        """yield each face that isn't in the alignments file."""
        self.output_message = "Faces missing from the alignments file"
        for item in self.items:
            frame_id = item[2] + item[1]

            if (frame_id not in self.alignments_data
                    or self.alignments_data[frame_id] <= item[3]):
                yield item[0] + item[1]

    def output_results(self):
        """ Output the results in the requested format """
        self.items_discovered = len(self.items_output)
        if self.items_discovered == 0:
            print("No {} were found meeting the criteria".format(self.type))
            return
        if self.output == "move":
            self.move_file()
            return
        output_message = "-----------------------------------------------\r\n"
        output_message += " {} ({})\r\n".format(self.output_message,
                                                self.items_discovered)
        output_message += "-----------------------------------------------\r\n"
        output_message += "\r\n".join([frame for frame in self.items_output])
        if self.output == "console":
            print("\n" + output_message)
        if self.output == "file":
            self.output_file(output_message)

    def output_file(self, output_message):
        """ Save the output to a text file in the frames directory """
        now = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = self.output_message.replace(" ", "_").lower()
        filename += "_" + now + ".txt"
        output_file = os.path.join(self.source_dir, filename)
        print("Saving {} result(s) to {}".format(self.items_discovered,
                                                 output_file))
        with open(output_file, "w") as f_output:
            f_output.write(output_message)

    def move_file(self):
        """ Move the identified frames to a new subfolder """
        now = datetime.now().strftime("%Y%m%d_%H%M%S")
        folder_name = self.output_message.replace(" ", "_").lower()
        folder_name += "_" + now
        output_folder = os.path.join(self.source_dir, folder_name)
        os.makedirs(output_folder)
        move = getattr(self, "move_{}".format(self.type))
        move(output_folder)

    def move_frames(self, output_folder):
        """ Move frames into single subfolder """
        print("Moving {} frame(s) to {}".format(self.items_discovered,
                                                output_folder))
        for frame in self.items_output:
            src = os.path.join(self.source_dir, frame)
            dst = os.path.join(output_folder, frame)
            os.rename(src, dst)

    def move_faces(self, output_folder):
        """ Make additional subdirs for each face that appears
            Enables easier manual sorting """
        print("Moving {} faces(s) to {}".format(self.items_discovered,
                                                output_folder))
        for frame in self.items_output:
            idx = frame[frame.rfind("_") + 1:frame.rfind(".")]
            src = os.path.join(self.source_dir, frame)
            dst_folder = os.path.join(output_folder, idx)
            if not os.path.isdir(dst_folder):
                os.makedirs(dst_folder)
            dst = os.path.join(dst_folder, frame)
            os.rename(src, dst)
 </s> add             job = globals()[self.args.job.title()]
        job = job(self.alignments, self.args)
        job.process() </s> remove         Path(x).suffix) for x in exclude]
 </s> add                               Path(x).suffix) for x in exclude]
        align_eyes = "\n\tCan optionally use the align-eyes switch (-ae)."
          frames_and_faces_dir = ("\n\tMust Pass in a frames folder AND a faces"
                                  "\n\tfolder (-fr and -fc).")
          output_opts = "\n\tUse the output option (-o) to process\n\tresults."
        align_eyes = "\n\tCan optionally use the align-eyes switch (-ae)."
          argument_list = list()
          argument_list.append({
              "opts": ("-j", "--job"),
              "type": str,
              "choices": ("draw", "extract", "manual",
keep keep add keep keep keep keep keep
 Alignments Tool - Manual (#503)

* Initial alignment tool implementation

* Add face processing. Fix helptext formatting for cli and gui

* Extract faces from alignments file

* Add draw landmarks

* fix analysis hours past 24 bug

* Remove tools flag. Enlarge GUI tooltips

* Add filename extension check for image folders

* Fix folder nonetype bug and linting

* Bugfix renaming of faces

* Import alignments from DeepFaceLab

* Remove frames requirement for dfl

* Alignments Tool: Add remove frames

* Alignments: Add remove frames (#492)

* Alignments Tool: Add remove frames

* Implement Frame display for manual extract

* Alignments refactor

* Further navigation improvements

* Further navigation improvements

* Alignment Tool: Refactor

* Move rotate_image_by_angle() to utils

* Further refactoring improvements

* Reduce key bindings. Add skip to has faces, Add resize frame binding

* More navigation options. Delete faces added. Save alignments added

* Add: Reload alignments. Add extra api calls. Better redraw handling.

* Manual Alignments - Alpha Build

* Rotate landmarks for rotated images

* Rebase to remove OriginalHiRes changes 
tools/cli.py