Spaces:
Build error
Build error
| import atexit | |
| import sys | |
| import os | |
| import time | |
| import argparse | |
| from datetime import datetime | |
| import multiprocessing as mp | |
| from montreal_forced_aligner import __version__ | |
| from montreal_forced_aligner.utils import get_available_acoustic_languages, get_available_g2p_languages, \ | |
| get_available_dict_languages, get_available_lm_languages, get_available_ivector_languages | |
| from montreal_forced_aligner.command_line.align import run_align_corpus | |
| from mfa_usr.adapt import run_adapt_model | |
| from montreal_forced_aligner.command_line.train_and_align import run_train_corpus | |
| from montreal_forced_aligner.command_line.g2p import run_g2p | |
| from montreal_forced_aligner.command_line.train_g2p import run_train_g2p | |
| from montreal_forced_aligner.command_line.validate import run_validate_corpus | |
| from montreal_forced_aligner.command_line.download import run_download | |
| from montreal_forced_aligner.command_line.train_lm import run_train_lm | |
| from montreal_forced_aligner.command_line.thirdparty import run_thirdparty | |
| from montreal_forced_aligner.command_line.train_ivector_extractor import run_train_ivector_extractor | |
| from montreal_forced_aligner.command_line.classify_speakers import run_classify_speakers | |
| from montreal_forced_aligner.command_line.transcribe import run_transcribe_corpus | |
| from montreal_forced_aligner.command_line.train_dictionary import run_train_dictionary | |
| from montreal_forced_aligner.command_line.create_segments import run_create_segments | |
| from montreal_forced_aligner.exceptions import MFAError | |
| from montreal_forced_aligner.config import update_global_config, load_global_config, update_command_history, \ | |
| load_command_history | |
| class ExitHooks(object): | |
| def __init__(self): | |
| self.exit_code = None | |
| self.exception = None | |
| def hook(self): | |
| self._orig_exit = sys.exit | |
| sys.exit = self.exit | |
| sys.excepthook = self.exc_handler | |
| def exit(self, code=0): | |
| self.exit_code = code | |
| self._orig_exit(code) | |
| def exc_handler(self, exc_type, exc, *args): | |
| self.exception = exc | |
| hooks = ExitHooks() | |
| hooks.hook() | |
| BEGIN = time.time() | |
| BEGIN_DATE = datetime.now() | |
| def history_save_handler(): | |
| history_data = { | |
| 'command': ' '.join(sys.argv), | |
| 'execution_time': time.time() - BEGIN, | |
| 'date': BEGIN_DATE, | |
| 'version': __version__ | |
| } | |
| if hooks.exit_code is not None: | |
| history_data['exit_code'] = hooks.exit_code | |
| history_data['exception'] = '' | |
| elif hooks.exception is not None: | |
| history_data['exit_code'] = 1 | |
| history_data['exception'] = hooks.exception | |
| else: | |
| history_data['exception'] = '' | |
| history_data['exit_code'] = 0 | |
| update_command_history(history_data) | |
| atexit.register(history_save_handler) | |
| def fix_path(): | |
| from montreal_forced_aligner.config import TEMP_DIR | |
| thirdparty_dir = os.path.join(TEMP_DIR, 'thirdparty', 'bin') | |
| old_path = os.environ.get('PATH', '') | |
| if sys.platform == 'win32': | |
| os.environ['PATH'] = thirdparty_dir + ';' + old_path | |
| else: | |
| os.environ['PATH'] = thirdparty_dir + ':' + old_path | |
| os.environ['LD_LIBRARY_PATH'] = thirdparty_dir + ':' + os.environ.get('LD_LIBRARY_PATH', '') | |
| def unfix_path(): | |
| if sys.platform == 'win32': | |
| sep = ';' | |
| os.environ['PATH'] = sep.join(os.environ['PATH'].split(sep)[1:]) | |
| else: | |
| sep = ':' | |
| os.environ['PATH'] = sep.join(os.environ['PATH'].split(sep)[1:]) | |
| os.environ['LD_LIBRARY_PATH'] = sep.join(os.environ['PATH'].split(sep)[1:]) | |
| acoustic_languages = get_available_acoustic_languages() | |
| ivector_languages = get_available_ivector_languages() | |
| lm_languages = get_available_lm_languages() | |
| g2p_languages = get_available_g2p_languages() | |
| dict_languages = get_available_dict_languages() | |
| def create_parser(): | |
| GLOBAL_CONFIG = load_global_config() | |
| def add_global_options(subparser, textgrid_output=False): | |
| subparser.add_argument('-t', '--temp_directory', type=str, default=GLOBAL_CONFIG['temp_directory'], | |
| help=f"Temporary directory root to store MFA created files, default is {GLOBAL_CONFIG['temp_directory']}") | |
| subparser.add_argument('--disable_mp', | |
| help=f"Disable any multiprocessing during alignment (not recommended), default is {not GLOBAL_CONFIG['use_mp']}", | |
| action='store_true', | |
| default=not GLOBAL_CONFIG['use_mp']) | |
| subparser.add_argument('-j', '--num_jobs', type=int, default=GLOBAL_CONFIG['num_jobs'], | |
| help=f"Number of data splits (and cores to use if multiprocessing is enabled), defaults " | |
| f"is {GLOBAL_CONFIG['num_jobs']}") | |
| subparser.add_argument('-v', '--verbose', help=f"Output debug messages, default is {GLOBAL_CONFIG['verbose']}", | |
| action='store_true', | |
| default=GLOBAL_CONFIG['verbose']) | |
| subparser.add_argument('--clean', help=f"Remove files from previous runs, default is {GLOBAL_CONFIG['clean']}", | |
| action='store_true', | |
| default=GLOBAL_CONFIG['clean']) | |
| subparser.add_argument('--overwrite', | |
| help=f"Overwrite output files when they exist, default is {GLOBAL_CONFIG['overwrite']}", | |
| action='store_true', | |
| default=GLOBAL_CONFIG['overwrite']) | |
| subparser.add_argument('--debug', | |
| help=f"Run extra steps for debugging issues, default is {GLOBAL_CONFIG['debug']}", | |
| action='store_true', | |
| default=GLOBAL_CONFIG['debug']) | |
| if textgrid_output: | |
| subparser.add_argument('--disable_textgrid_cleanup', | |
| help=f"Disable extra clean up steps on TextGrid output, default is {not GLOBAL_CONFIG['cleanup_textgrids']}", | |
| action='store_true', | |
| default=not GLOBAL_CONFIG['cleanup_textgrids']) | |
| parser = argparse.ArgumentParser() | |
| subparsers = parser.add_subparsers(dest="subcommand") | |
| subparsers.required = True | |
| version_parser = subparsers.add_parser('version') | |
| align_parser = subparsers.add_parser('align') | |
| align_parser.add_argument('corpus_directory', help="Full path to the directory to align") | |
| align_parser.add_argument('dictionary_path', help="Full path to the pronunciation dictionary to use") | |
| align_parser.add_argument('acoustic_model_path', | |
| help=f"Full path to the archive containing pre-trained model or language ({', '.join(acoustic_languages)})") | |
| align_parser.add_argument('output_directory', | |
| help="Full path to output directory, will be created if it doesn't exist") | |
| align_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for alignment") | |
| align_parser.add_argument('-s', '--speaker_characters', type=str, default='0', | |
| help="Number of characters of file names to use for determining speaker, " | |
| 'default is to use directory names') | |
| align_parser.add_argument('-a', '--audio_directory', type=str, default='', | |
| help="Audio directory root to use for finding audio files") | |
| add_global_options(align_parser, textgrid_output=True) | |
| adapt_parser = subparsers.add_parser('adapt') | |
| adapt_parser.add_argument('corpus_directory', help="Full path to the directory to align") | |
| adapt_parser.add_argument('dictionary_path', help="Full path to the pronunciation dictionary to use") | |
| adapt_parser.add_argument('acoustic_model_path', | |
| help=f"Full path to the archive containing pre-trained model or language ({', '.join(acoustic_languages)})") | |
| adapt_parser.add_argument('output_model_path', | |
| help="Full path to save adapted_model") | |
| adapt_parser.add_argument('output_directory', | |
| help="Full path to output directory, will be created if it doesn't exist") | |
| adapt_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for alignment") | |
| adapt_parser.add_argument('-s', '--speaker_characters', type=str, default='0', | |
| help="Number of characters of file names to use for determining speaker, " | |
| 'default is to use directory names') | |
| adapt_parser.add_argument('-a', '--audio_directory', type=str, default='', | |
| help="Audio directory root to use for finding audio files") | |
| add_global_options(adapt_parser, textgrid_output=True) | |
| train_parser = subparsers.add_parser('train') | |
| train_parser.add_argument('corpus_directory', help="Full path to the source directory to align") | |
| train_parser.add_argument('dictionary_path', help="Full path to the pronunciation dictionary to use", | |
| default='') | |
| train_parser.add_argument('output_directory', | |
| help="Full path to output directory, will be created if it doesn't exist") | |
| train_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for training and alignment") | |
| train_parser.add_argument('-o', '--output_model_path', type=str, default='', | |
| help="Full path to save resulting acoustic and dictionary model") | |
| train_parser.add_argument('-s', '--speaker_characters', type=str, default='0', | |
| help="Number of characters of filenames to use for determining speaker, " | |
| 'default is to use directory names') | |
| train_parser.add_argument('-a', '--audio_directory', type=str, default='', | |
| help="Audio directory root to use for finding audio files") | |
| train_parser.add_argument('-m', '--acoustic_model_path', type=str, default='', | |
| help="Full path to save adapted_model") | |
| add_global_options(train_parser, textgrid_output=True) | |
| validate_parser = subparsers.add_parser('validate') | |
| validate_parser.add_argument('corpus_directory', help="Full path to the source directory to align") | |
| validate_parser.add_argument('dictionary_path', help="Full path to the pronunciation dictionary to use", | |
| default='') | |
| validate_parser.add_argument('acoustic_model_path', nargs='?', default='', | |
| help=f"Full path to the archive containing pre-trained model or language ({', '.join(acoustic_languages)})") | |
| validate_parser.add_argument('-s', '--speaker_characters', type=str, default='0', | |
| help="Number of characters of file names to use for determining speaker, " | |
| 'default is to use directory names') | |
| validate_parser.add_argument('--test_transcriptions', help="Test accuracy of transcriptions", action='store_true') | |
| validate_parser.add_argument('--ignore_acoustics', | |
| help="Skip acoustic feature generation and associated validation", | |
| action='store_true') | |
| add_global_options(validate_parser) | |
| g2p_model_help_message = f'''Full path to the archive containing pre-trained model or language ({', '.join(g2p_languages)}) | |
| If not specified, then orthographic transcription is split into pronunciations.''' | |
| g2p_parser = subparsers.add_parser('g2p') | |
| g2p_parser.add_argument("g2p_model_path", help=g2p_model_help_message, nargs='?') | |
| g2p_parser.add_argument("input_path", | |
| help="Corpus to base word list on or a text file of words to generate pronunciations") | |
| g2p_parser.add_argument("output_path", help="Path to save output dictionary") | |
| g2p_parser.add_argument('--include_bracketed', help="Included words enclosed by brackets, i.e. [...], (...), <...>", | |
| action='store_true') | |
| g2p_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for G2P") | |
| add_global_options(g2p_parser) | |
| train_g2p_parser = subparsers.add_parser('train_g2p') | |
| train_g2p_parser.add_argument("dictionary_path", help="Location of existing dictionary") | |
| train_g2p_parser.add_argument("output_model_path", help="Desired location of generated model") | |
| train_g2p_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for G2P") | |
| train_g2p_parser.add_argument("--validate", action='store_true', | |
| help="Perform an analysis of accuracy training on " | |
| "most of the data and validating on an unseen subset") | |
| add_global_options(train_g2p_parser) | |
| download_parser = subparsers.add_parser('download') | |
| download_parser.add_argument("model_type", | |
| help="Type of model to download, one of 'acoustic', 'g2p', or 'dictionary'") | |
| download_parser.add_argument("language", help="Name of language code to download, if not specified, " | |
| "will list all available languages", nargs='?') | |
| train_lm_parser = subparsers.add_parser('train_lm') | |
| train_lm_parser.add_argument('source_path', help="Full path to the source directory to train from, alternatively " | |
| 'an ARPA format language model to convert for MFA use') | |
| train_lm_parser.add_argument('output_model_path', type=str, | |
| help="Full path to save resulting language model") | |
| train_lm_parser.add_argument('-m', '--model_path', type=str, | |
| help="Full path to existing language model to merge probabilities") | |
| train_lm_parser.add_argument('-w', '--model_weight', type=float, default=1.0, | |
| help="Weight factor for supplemental language model, defaults to 1.0") | |
| train_lm_parser.add_argument('--dictionary_path', help="Full path to the pronunciation dictionary to use", | |
| default='') | |
| train_lm_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for training and alignment") | |
| add_global_options(train_lm_parser) | |
| train_dictionary_parser = subparsers.add_parser('train_dictionary') | |
| train_dictionary_parser.add_argument('corpus_directory', help="Full path to the directory to align") | |
| train_dictionary_parser.add_argument('dictionary_path', help="Full path to the pronunciation dictionary to use") | |
| train_dictionary_parser.add_argument('acoustic_model_path', | |
| help=f"Full path to the archive containing pre-trained model or language ({', '.join(acoustic_languages)})") | |
| train_dictionary_parser.add_argument('output_directory', | |
| help="Full path to output directory, will be created if it doesn't exist") | |
| train_dictionary_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for alignment") | |
| train_dictionary_parser.add_argument('-s', '--speaker_characters', type=str, default='0', | |
| help="Number of characters of file names to use for determining speaker, " | |
| 'default is to use directory names') | |
| add_global_options(train_dictionary_parser) | |
| train_ivector_parser = subparsers.add_parser('train_ivector') | |
| train_ivector_parser.add_argument('corpus_directory', help="Full path to the source directory to " | |
| 'train the ivector extractor') | |
| train_ivector_parser.add_argument('dictionary_path', help="Full path to the pronunciation dictionary to use") | |
| train_ivector_parser.add_argument('acoustic_model_path', type=str, default='', | |
| help="Full path to acoustic model for alignment") | |
| train_ivector_parser.add_argument('output_model_path', type=str, default='', | |
| help="Full path to save resulting ivector extractor") | |
| train_ivector_parser.add_argument('-s', '--speaker_characters', type=str, default='0', | |
| help="Number of characters of filenames to use for determining speaker, " | |
| 'default is to use directory names') | |
| train_ivector_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for training") | |
| add_global_options(train_ivector_parser) | |
| classify_speakers_parser = subparsers.add_parser('classify_speakers') | |
| classify_speakers_parser.add_argument('corpus_directory', help="Full path to the source directory to " | |
| 'run speaker classification') | |
| classify_speakers_parser.add_argument('ivector_extractor_path', type=str, default='', | |
| help="Full path to ivector extractor model") | |
| classify_speakers_parser.add_argument('output_directory', | |
| help="Full path to output directory, will be created if it doesn't exist") | |
| classify_speakers_parser.add_argument('-s', '--num_speakers', type=int, default=0, | |
| help="Number of speakers if known") | |
| classify_speakers_parser.add_argument('--cluster', help="Using clustering instead of classification", | |
| action='store_true') | |
| classify_speakers_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for ivector extraction") | |
| add_global_options(classify_speakers_parser) | |
| create_segments_parser = subparsers.add_parser('create_segments') | |
| create_segments_parser.add_argument('corpus_directory', help="Full path to the source directory to " | |
| 'run VAD segmentation') | |
| create_segments_parser.add_argument('output_directory', | |
| help="Full path to output directory, will be created if it doesn't exist") | |
| create_segments_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for segmentation") | |
| add_global_options(create_segments_parser) | |
| transcribe_parser = subparsers.add_parser('transcribe') | |
| transcribe_parser.add_argument('corpus_directory', help="Full path to the directory to transcribe") | |
| transcribe_parser.add_argument('dictionary_path', help="Full path to the pronunciation dictionary to use") | |
| transcribe_parser.add_argument('acoustic_model_path', | |
| help=f"Full path to the archive containing pre-trained model or language ({', '.join(acoustic_languages)})") | |
| transcribe_parser.add_argument('language_model_path', | |
| help=f"Full path to the archive containing pre-trained model or language ({', '.join(lm_languages)})") | |
| transcribe_parser.add_argument('output_directory', | |
| help="Full path to output directory, will be created if it doesn't exist") | |
| transcribe_parser.add_argument('--config_path', type=str, default='', | |
| help="Path to config file to use for transcription") | |
| transcribe_parser.add_argument('-s', '--speaker_characters', type=str, default='0', | |
| help="Number of characters of file names to use for determining speaker, " | |
| 'default is to use directory names') | |
| transcribe_parser.add_argument('-a', '--audio_directory', type=str, default='', | |
| help="Audio directory root to use for finding audio files") | |
| transcribe_parser.add_argument('-e', '--evaluate', help="Evaluate the transcription " | |
| "against golden texts", action='store_true') | |
| add_global_options(transcribe_parser) | |
| config_parser = subparsers.add_parser('configure', | |
| help="The configure command is used to set global defaults for MFA so " | |
| "you don't have to set them every time you call an MFA command.") | |
| config_parser.add_argument('-t', '--temp_directory', type=str, default='', | |
| help=f"Set the default temporary directory, default is {GLOBAL_CONFIG['temp_directory']}") | |
| config_parser.add_argument('-j', '--num_jobs', type=int, | |
| help=f"Set the number of processes to use by default, defaults to {GLOBAL_CONFIG['num_jobs']}") | |
| config_parser.add_argument('--always_clean', help="Always remove files from previous runs by default", | |
| action='store_true') | |
| config_parser.add_argument('--never_clean', help="Don't remove files from previous runs by default", | |
| action='store_true') | |
| config_parser.add_argument('--always_verbose', help="Default to verbose output", action='store_true') | |
| config_parser.add_argument('--never_verbose', help="Default to non-verbose output", action='store_true') | |
| config_parser.add_argument('--always_debug', help="Default to running debugging steps", action='store_true') | |
| config_parser.add_argument('--never_debug', help="Default to not running debugging steps", action='store_true') | |
| config_parser.add_argument('--always_overwrite', help="Always overwrite output files", action='store_true') | |
| config_parser.add_argument('--never_overwrite', help="Never overwrite output files (if file already exists, " | |
| "the output will be saved in the temp directory)", | |
| action='store_true') | |
| config_parser.add_argument('--disable_mp', help="Disable all multiprocessing (not recommended as it will usually " | |
| "increase processing times)", action='store_true') | |
| config_parser.add_argument('--enable_mp', help="Enable multiprocessing (recommended and enabled by default)", | |
| action='store_true') | |
| config_parser.add_argument('--disable_textgrid_cleanup', help="Disable postprocessing of TextGrids that cleans up " | |
| "silences and recombines compound words and clitics", | |
| action='store_true') | |
| config_parser.add_argument('--enable_textgrid_cleanup', help="Enable postprocessing of TextGrids that cleans up " | |
| "silences and recombines compound words and clitics", | |
| action='store_true') | |
| history_parser = subparsers.add_parser('history') | |
| history_parser.add_argument('depth', help='Number of commands to list', nargs='?', default=10) | |
| history_parser.add_argument('--verbose', help="Flag for whether to output additional information", | |
| action='store_true') | |
| annotator_parser = subparsers.add_parser('annotator') | |
| anchor_parser = subparsers.add_parser('anchor') | |
| thirdparty_parser = subparsers.add_parser('thirdparty') | |
| thirdparty_parser.add_argument("command", | |
| help="One of 'download', 'validate', or 'kaldi'") | |
| thirdparty_parser.add_argument('local_directory', | |
| help="Full path to the built executables to collect", nargs="?", | |
| default='') | |
| return parser | |
| parser = create_parser() | |
| def main(): | |
| parser = create_parser() | |
| mp.freeze_support() | |
| args, unknown = parser.parse_known_args() | |
| for short in ['-c', '-d']: | |
| if short in unknown: | |
| print(f'Due to the number of options that `{short}` could refer to, it is not accepted. ' | |
| 'Please specify the full argument') | |
| sys.exit(1) | |
| try: | |
| fix_path() | |
| if args.subcommand in ['align', 'train', 'train_ivector']: | |
| from montreal_forced_aligner.thirdparty.kaldi import validate_alignment_binaries | |
| if not validate_alignment_binaries(): | |
| print("There was an issue validating Kaldi binaries, please ensure you've downloaded them via the " | |
| "'mfa thirdparty download' command. See 'mfa thirdparty validate' for more detailed information " | |
| "on why this check failed.") | |
| sys.exit(1) | |
| elif args.subcommand in ['transcribe']: | |
| from montreal_forced_aligner.thirdparty.kaldi import validate_transcribe_binaries | |
| if not validate_transcribe_binaries(): | |
| print("There was an issue validating Kaldi binaries, please ensure you've downloaded them via the " | |
| "'mfa thirdparty download' command. See 'mfa thirdparty validate' for more detailed information " | |
| "on why this check failed. If you are on MacOS, please note that the thirdparty binaries available " | |
| "via the download command do not contain the transcription ones. To get this functionality working " | |
| "for the time being, please build kaldi locally and follow the instructions for running the " | |
| "'mfa thirdparty kaldi' command.") | |
| sys.exit(1) | |
| elif args.subcommand in ['train_dictionary']: | |
| from montreal_forced_aligner.thirdparty.kaldi import validate_train_dictionary_binaries | |
| if not validate_train_dictionary_binaries(): | |
| print("There was an issue validating Kaldi binaries, please ensure you've downloaded them via the " | |
| "'mfa thirdparty download' command. See 'mfa thirdparty validate' for more detailed information " | |
| "on why this check failed. If you are on MacOS, please note that the thirdparty binaries available " | |
| "via the download command do not contain the train_dictionary ones. To get this functionality working " | |
| "for the time being, please build kaldi locally and follow the instructions for running the " | |
| "'mfa thirdparty kaldi' command.") | |
| sys.exit(1) | |
| elif args.subcommand in ['g2p', 'train_g2p']: | |
| try: | |
| import pynini | |
| except ImportError: | |
| print("There was an issue importing Pynini, please ensure that it is installed. If you are on Windows, " | |
| "please use the Windows Subsystem for Linux to use g2p functionality.") | |
| sys.exit(1) | |
| if args.subcommand == 'align': | |
| run_align_corpus(args, unknown, acoustic_languages) | |
| elif args.subcommand == 'adapt': | |
| run_adapt_model(args, unknown, acoustic_languages) | |
| elif args.subcommand == 'train': | |
| run_train_corpus(args, unknown) | |
| elif args.subcommand == 'g2p': | |
| run_g2p(args, unknown, g2p_languages) | |
| elif args.subcommand == 'train_g2p': | |
| run_train_g2p(args, unknown) | |
| elif args.subcommand == 'validate': | |
| run_validate_corpus(args, unknown) | |
| elif args.subcommand == 'download': | |
| run_download(args) | |
| elif args.subcommand == 'train_lm': | |
| run_train_lm(args, unknown) | |
| elif args.subcommand == 'train_dictionary': | |
| run_train_dictionary(args, unknown) | |
| elif args.subcommand == 'train_ivector': | |
| run_train_ivector_extractor(args, unknown) | |
| elif args.subcommand == 'classify_speakers': | |
| run_classify_speakers(args, unknown) | |
| elif args.subcommand in ['annotator', 'anchor']: | |
| from montreal_forced_aligner.command_line.anchor import run_anchor | |
| run_anchor(args) | |
| elif args.subcommand == 'thirdparty': | |
| run_thirdparty(args) | |
| elif args.subcommand == 'transcribe': | |
| run_transcribe_corpus(args, unknown) | |
| elif args.subcommand == 'create_segments': | |
| run_create_segments(args, unknown) | |
| elif args.subcommand == 'configure': | |
| update_global_config(args) | |
| global GLOBAL_CONFIG | |
| GLOBAL_CONFIG = load_global_config() | |
| elif args.subcommand == 'history': | |
| depth = args.depth | |
| history = load_command_history()[-depth:] | |
| for h in history: | |
| if args.verbose: | |
| print('command\tDate\tExecution time\tVersion\tExit code\tException') | |
| for h in history: | |
| execution_time = time.strftime('%H:%M:%S', time.gmtime(h['execution_time'])) | |
| d = h['date'].isoformat() | |
| print( | |
| f"{h['command']}\t{d}\t{execution_time}\t{h['version']}\t{h['exit_code']}\t{h['exception']}") | |
| pass | |
| else: | |
| for h in history: | |
| print(h['command']) | |
| elif args.subcommand == 'version': | |
| print(__version__) | |
| except MFAError as e: | |
| if getattr(args, 'debug', False): | |
| raise | |
| print(e) | |
| sys.exit(1) | |
| finally: | |
| unfix_path() | |
| if __name__ == '__main__': | |
| main() | |