diff --git a/lib/python3.12/site-packages/IPython/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/IPython/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4476d763438e9e2782c7d1ddb637b9622e40a7f Binary files /dev/null and b/lib/python3.12/site-packages/IPython/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/__pycache__/__main__.cpython-312.pyc b/lib/python3.12/site-packages/IPython/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a855c7601fda050ac62cda4c71983a2760d6375e Binary files /dev/null and b/lib/python3.12/site-packages/IPython/__pycache__/__main__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/__pycache__/display.cpython-312.pyc b/lib/python3.12/site-packages/IPython/__pycache__/display.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..701e934c47e6dd345b7c56e4888d74c7b32f9905 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/__pycache__/display.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/__pycache__/paths.cpython-312.pyc b/lib/python3.12/site-packages/IPython/__pycache__/paths.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3fda02d28d510188a88bd22519f4c4f8108ee923 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/__pycache__/paths.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/extensions/__init__.py b/lib/python3.12/site-packages/IPython/extensions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..db7f79fca6aba2547d08ed15bc4a67f9fbcaec01 --- /dev/null +++ b/lib/python3.12/site-packages/IPython/extensions/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""This directory is meant for IPython extensions.""" diff --git a/lib/python3.12/site-packages/IPython/extensions/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/IPython/extensions/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f2a1f83fe688aded3b51ef1fddca12aa00db54b Binary files /dev/null and b/lib/python3.12/site-packages/IPython/extensions/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/extensions/__pycache__/autoreload.cpython-312.pyc b/lib/python3.12/site-packages/IPython/extensions/__pycache__/autoreload.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b8547ce8623848fd5c2884d7d4ffa0216349984 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/extensions/__pycache__/autoreload.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/extensions/__pycache__/storemagic.cpython-312.pyc b/lib/python3.12/site-packages/IPython/extensions/__pycache__/storemagic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8684c0c54bb98372d56cf6103c4b7f8e765bd082 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/extensions/__pycache__/storemagic.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/extensions/autoreload.py b/lib/python3.12/site-packages/IPython/extensions/autoreload.py new file mode 100644 index 0000000000000000000000000000000000000000..d67c9daa897d68ac8efc117df1b7d9c41f45fcaf --- /dev/null +++ b/lib/python3.12/site-packages/IPython/extensions/autoreload.py @@ -0,0 +1,915 @@ +"""IPython extension to reload modules before executing user code. + +``autoreload`` reloads modules automatically before entering the execution of +code typed at the IPython prompt. + +This makes for example the following workflow possible: + +.. sourcecode:: ipython + + In [1]: %load_ext autoreload + + In [2]: %autoreload 2 + + In [3]: from foo import some_function + + In [4]: some_function() + Out[4]: 42 + + In [5]: # open foo.py in an editor and change some_function to return 43 + + In [6]: some_function() + Out[6]: 43 + +The module was reloaded without reloading it explicitly, and the object +imported with ``from foo import ...`` was also updated. + +Usage +===== + +The following magic commands are provided: + +``%autoreload``, ``%autoreload now`` + + Reload all modules (except those excluded by ``%aimport``) + automatically now. + +``%autoreload 0``, ``%autoreload off`` + + Disable automatic reloading. + +``%autoreload 1``, ``%autoreload explicit`` + + Reload all modules imported with ``%aimport`` every time before + executing the Python code typed. + +``%autoreload 2``, ``%autoreload all`` + + Reload all modules (except those excluded by ``%aimport``) every + time before executing the Python code typed. + +``%autoreload 3``, ``%autoreload complete`` + + Same as 2/all, but also adds any new objects in the module. See + unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects + + Adding ``--print`` or ``-p`` to the ``%autoreload`` line will print autoreload activity to + standard out. ``--log`` or ``-l`` will do it to the log at INFO level; both can be used + simultaneously. + +``%aimport`` + + List modules which are to be automatically imported or not to be imported. + +``%aimport foo`` + + Import module 'foo' and mark it to be autoreloaded for ``%autoreload 1`` + +``%aimport foo, bar`` + + Import modules 'foo', 'bar' and mark them to be autoreloaded for ``%autoreload 1`` + +``%aimport -foo`` + + Mark module 'foo' to not be autoreloaded. + +Import Conflict Resolution +========================== + +In ``%autoreload 3`` mode, the extension tracks ``from X import Y`` style imports +and intelligently resolves conflicts when the same name is imported multiple ways. + +Import tracking occurs after successful code execution, ensuring that only valid +imports are tracked. This approach handles edge cases such as: + +- Importing a name that doesn't initially exist in a module, then adding that name + to the module and importing it again +- Conflicts between aliased imports (``from X import Y as Z``) and direct imports + (``from X import Z``) + +When conflicts occur: + +- If you first do ``from X import Y as Z`` then later ``from X import Z``, + the extension will switch to reloading ``Z`` instead of ``Y`` under the name ``Z``. + +- Similarly, if you first do ``from X import Z`` then later ``from X import Y as Z``, + the extension will switch to reloading ``Y`` as ``Z`` instead of the original ``Z``. + +- The most recent successful import always takes precedence in conflict resolution. + +Caveats +======= + +Reloading Python modules in a reliable way is in general difficult, +and unexpected things may occur. ``%autoreload`` tries to work around +common pitfalls by replacing function code objects and parts of +classes previously in the module with new versions. This makes the +following things to work: + +- Functions and classes imported via 'from xxx import foo' are upgraded + to new versions when 'xxx' is reloaded. + +- Methods and properties of classes are upgraded on reload, so that + calling 'c.foo()' on an object 'c' created before the reload causes + the new code for 'foo' to be executed. + +Some of the known remaining caveats are: + +- Replacing code objects does not always succeed: changing a @property + in a class to an ordinary method or a method to a member variable + can cause problems (but in old objects only). + +- Functions that are removed (eg. via monkey-patching) from a module + before it is reloaded are not upgraded. + +- C extension modules cannot be reloaded, and so cannot be autoreloaded. + +- While comparing Enum and Flag, the 'is' Identity Operator is used (even in the case '==' has been used (Similar to the 'None' keyword)). + +- Reloading a module, or importing the same module by a different name, creates new Enums. These may look the same, but are not. +""" + +from IPython.core import magic_arguments +from IPython.core.magic import Magics, magics_class, line_magic +from IPython.extensions.deduperreload.deduperreload import DeduperReloader + +__skip_doctest__ = True + +# ----------------------------------------------------------------------------- +# Copyright (C) 2000 Thomas Heller +# Copyright (C) 2008 Pauli Virtanen +# Copyright (C) 2012 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +# ----------------------------------------------------------------------------- +# +# This IPython module is written by Pauli Virtanen, based on the autoreload +# code by Thomas Heller. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- + +import ast +import os +import sys +import traceback +import types +import weakref +import gc +import logging +from importlib import import_module, reload +from importlib.util import source_from_cache + +# ------------------------------------------------------------------------------ +# Autoreload functionality +# ------------------------------------------------------------------------------ + + +class ModuleReloader: + enabled = False + """Whether this reloader is enabled""" + + check_all = True + """Autoreload all modules, not just those listed in 'modules'""" + + autoload_obj = False + """Autoreload all modules AND autoload all new objects""" + + def __init__(self, shell=None): + # Modules that failed to reload: {module: mtime-on-failed-reload, ...} + self.failed = {} + # Modules specially marked as autoreloadable. + self.modules = {} + # Modules specially marked as not autoreloadable. + self.skip_modules = {} + # (module-name, name) -> weakref, for replacing old code objects + self.old_objects = {} + # Module modification timestamps + self.modules_mtimes = {} + self.shell = shell + + # Reporting callable for verbosity + self._report = lambda msg: None # by default, be quiet. + + # Deduper reloader + self.deduper_reloader = DeduperReloader() + + # Persistent import tracker for from-imports + self.import_from_tracker = ImportFromTracker({}, {}) + + # Cache module modification times + self.check(check_all=True, do_reload=False) + + # To hide autoreload errors + self.hide_errors = False + + def mark_module_skipped(self, module_name): + """Skip reloading the named module in the future""" + try: + del self.modules[module_name] + except KeyError: + pass + self.skip_modules[module_name] = True + + def mark_module_reloadable(self, module_name): + """Reload the named module in the future (if it is imported)""" + try: + del self.skip_modules[module_name] + except KeyError: + pass + self.modules[module_name] = True + + def clear_import_tracker(self): + """Clear the persistent import tracker state""" + self.import_from_tracker = ImportFromTracker({}, {}) + + def aimport_module(self, module_name): + """Import a module, and mark it reloadable + + Returns + ------- + top_module : module + The imported module if it is top-level, or the top-level + top_name : module + Name of top_module + + """ + self.mark_module_reloadable(module_name) + + import_module(module_name) + top_name = module_name.split(".")[0] + top_module = sys.modules[top_name] + return top_module, top_name + + def filename_and_mtime(self, module): + if not hasattr(module, "__file__") or module.__file__ is None: + return None, None + + if getattr(module, "__name__", None) in [None, "__mp_main__", "__main__"]: + # we cannot reload(__main__) or reload(__mp_main__) + return None, None + + filename = module.__file__ + path, ext = os.path.splitext(filename) + + if ext.lower() == ".py": + py_filename = filename + else: + try: + py_filename = source_from_cache(filename) + except ValueError: + return None, None + + try: + pymtime = os.stat(py_filename).st_mtime + except OSError: + return None, None + + return py_filename, pymtime + + def check(self, check_all=False, do_reload=True, execution_info=None): + """Check whether some modules need to be reloaded.""" + + if not self.enabled and not check_all: + return + + if check_all or self.check_all: + modules = list(sys.modules.keys()) + else: + modules = list(self.modules.keys()) + + # Use the persistent import_from_tracker + import_from_tracker = ( + self.import_from_tracker if self.import_from_tracker.imports_froms else None + ) + for modname in modules: + m = sys.modules.get(modname, None) + + if modname in self.skip_modules: + continue + + py_filename, pymtime = self.filename_and_mtime(m) + if py_filename is None: + continue + + try: + if pymtime <= self.modules_mtimes[modname]: + continue + except KeyError: + self.modules_mtimes[modname] = pymtime + continue + else: + if self.failed.get(py_filename, None) == pymtime: + continue + + self.modules_mtimes[modname] = pymtime + + # If we've reached this point, we should try to reload the module + if do_reload: + self._report(f"Reloading '{modname}'.") + try: + if self.autoload_obj: + superreload( + m, + reload, + self.old_objects, + self.shell, + import_from_tracker=import_from_tracker, + ) + # if not using autoload, check if deduperreload is viable for this module + elif self.deduper_reloader.maybe_reload_module(m): + pass + else: + superreload(m, reload, self.old_objects) + if py_filename in self.failed: + del self.failed[py_filename] + except: + if not self.hide_errors: + logger = logging.getLogger("autoreload") + logger.exception( + f"Failed to reload module '{modname}' from file '{py_filename}'" + ) + print( + "[autoreload of {} failed: {}]".format( + modname, traceback.format_exc(10) + ), + file=sys.stderr, + ) + self.failed[py_filename] = pymtime + self.deduper_reloader.update_sources() + + +# ------------------------------------------------------------------------------ +# superreload +# ------------------------------------------------------------------------------ + + +func_attrs = [ + "__code__", + "__defaults__", + "__doc__", + "__closure__", + "__globals__", + "__dict__", +] + + +def update_function(old, new): + """Upgrade the code object of a function""" + for name in func_attrs: + try: + setattr(old, name, getattr(new, name)) + except (AttributeError, TypeError): + pass + + +def update_instances(old, new): + """Use garbage collector to find all instances that refer to the old + class definition and update their __class__ to point to the new class + definition""" + + refs = gc.get_referrers(old) + + for ref in refs: + if type(ref) is old: + object.__setattr__(ref, "__class__", new) + + +def update_class(old, new): + """Replace stuff in the __dict__ of a class, and upgrade + method code objects, and add new methods, if any""" + for key in list(old.__dict__.keys()): + old_obj = getattr(old, key) + try: + new_obj = getattr(new, key) + # explicitly checking that comparison returns True to handle + # cases where `==` doesn't return a boolean. + if (old_obj == new_obj) is True: + continue + except AttributeError: + # obsolete attribute: remove it + try: + delattr(old, key) + except (AttributeError, TypeError): + pass + continue + except ValueError: + # can't compare nested structures containing + # numpy arrays using `==` + pass + + if update_generic(old_obj, new_obj): + continue + + try: + setattr(old, key, getattr(new, key)) + except (AttributeError, TypeError): + pass # skip non-writable attributes + + for key in list(new.__dict__.keys()): + if key not in list(old.__dict__.keys()): + try: + setattr(old, key, getattr(new, key)) + except (AttributeError, TypeError): + pass # skip non-writable attributes + + # update all instances of class + update_instances(old, new) + + +def update_property(old, new): + """Replace get/set/del functions of a property""" + update_generic(old.fdel, new.fdel) + update_generic(old.fget, new.fget) + update_generic(old.fset, new.fset) + + +def isinstance2(a, b, typ): + return isinstance(a, typ) and isinstance(b, typ) + + +UPDATE_RULES = [ + (lambda a, b: isinstance2(a, b, type), update_class), + (lambda a, b: isinstance2(a, b, types.FunctionType), update_function), + (lambda a, b: isinstance2(a, b, property), update_property), +] +UPDATE_RULES.extend( + [ + ( + lambda a, b: isinstance2(a, b, types.MethodType), + lambda a, b: update_function(a.__func__, b.__func__), + ), + ] +) + + +def update_generic(a, b): + for type_check, update in UPDATE_RULES: + if type_check(a, b): + update(a, b) + return True + return False + + +class StrongRef: + def __init__(self, obj): + self.obj = obj + + def __call__(self): + return self.obj + + +mod_attrs = [ + "__name__", + "__doc__", + "__package__", + "__loader__", + "__spec__", + "__file__", + "__cached__", + "__builtins__", +] + + +class ImportFromTracker: + def __init__(self, imports_froms: dict, symbol_map: dict): + self.imports_froms = imports_froms + # symbol_map maps original_name -> list of resolved_names + self.symbol_map = {} + if symbol_map: + for module_name, mappings in symbol_map.items(): + self.symbol_map[module_name] = {} + for original_name, resolved_names in mappings.items(): + if isinstance(resolved_names, list): + self.symbol_map[module_name][original_name] = resolved_names[:] + else: + self.symbol_map[module_name][original_name] = [resolved_names] + else: + self.symbol_map = symbol_map or {} + + def add_import( + self, module_name: str, original_name: str, resolved_name: str + ) -> None: + """Add an import, handling conflicts with existing imports. + + This method is called after successful code execution, so we know the import is valid. + """ + if module_name not in self.imports_froms: + self.imports_froms[module_name] = [] + if module_name not in self.symbol_map: + self.symbol_map[module_name] = {} + + # Check if there's already a different mapping for the same resolved_name from a different original_name + # We need to remove any conflicting mappings + for orig_name, res_names in list(self.symbol_map[module_name].items()): + if resolved_name in res_names and orig_name != original_name: + # Remove the conflicting resolved_name from the other original_name's list + res_names.remove(resolved_name) + if ( + not res_names + ): # If the list is now empty, remove the original_name entirely + if orig_name in self.imports_froms[module_name]: + self.imports_froms[module_name].remove(orig_name) + del self.symbol_map[module_name][orig_name] + + # Add the new mapping + if original_name not in self.imports_froms[module_name]: + self.imports_froms[module_name].append(original_name) + + if original_name not in self.symbol_map[module_name]: + self.symbol_map[module_name][original_name] = [] + + # Add the resolved_name if it's not already in the list + if resolved_name not in self.symbol_map[module_name][original_name]: + self.symbol_map[module_name][original_name].append(resolved_name) + + +def append_obj(module, d, name, obj, autoload=False): + in_module = hasattr(obj, "__module__") and obj.__module__ == module.__name__ + if autoload: + # check needed for module global built-ins + if not in_module and name in mod_attrs: + return False + else: + if not in_module: + return False + + key = (module.__name__, name) + try: + d.setdefault(key, []).append(weakref.ref(obj)) + except TypeError: + pass + return True + + +def superreload( + module, reload=reload, old_objects=None, shell=None, import_from_tracker=None +): + """Enhanced version of the builtin reload function. + + superreload remembers objects previously in the module, and + + - upgrades the class dictionary of every old class in the module + - upgrades the code object of every old function and method + - clears the module's namespace before reloading + + """ + if old_objects is None: + old_objects = {} + + # collect old objects in the module + for name, obj in list(module.__dict__.items()): + if not append_obj(module, old_objects, name, obj): + continue + key = (module.__name__, name) + try: + old_objects.setdefault(key, []).append(weakref.ref(obj)) + except TypeError: + pass + + # reload module + try: + # clear namespace first from old cruft + old_dict = module.__dict__.copy() + old_name = module.__name__ + module.__dict__.clear() + module.__dict__["__name__"] = old_name + module.__dict__["__loader__"] = old_dict["__loader__"] + except (TypeError, AttributeError, KeyError): + pass + + try: + module = reload(module) + except: + # restore module dictionary on failed reload + module.__dict__.update(old_dict) + raise + + for name, new_obj in list(module.__dict__.items()): + key = (module.__name__, name) + if key not in old_objects: + # here 'shell' acts both as a flag and as an output var + imports_froms = ( + import_from_tracker.imports_froms if import_from_tracker else None + ) + symbol_map = import_from_tracker.symbol_map if import_from_tracker else None + if ( + shell is None + or name == "Enum" + or not append_obj(module, old_objects, name, new_obj, True) + or ( + imports_froms + and module.__name__ in imports_froms + and "*" not in imports_froms[module.__name__] + and name not in imports_froms[module.__name__] + ) + ): + continue + + # Handle symbol mapping - now supporting multiple resolved names per original name + if symbol_map and name in symbol_map.get(module.__name__, {}): + resolved_names = symbol_map.get(module.__name__, {})[name] + for resolved_name in resolved_names: + shell.user_ns[resolved_name] = new_obj + else: + shell.user_ns[name] = new_obj + + new_refs = [] + for old_ref in old_objects[key]: + old_obj = old_ref() + if old_obj is None: + continue + new_refs.append(old_ref) + update_generic(old_obj, new_obj) + + if new_refs: + old_objects[key] = new_refs + else: + del old_objects[key] + + return module + + +# ------------------------------------------------------------------------------ +# IPython connectivity +# ------------------------------------------------------------------------------ + + +@magics_class +class AutoreloadMagics(Magics): + def __init__(self, *a, **kw): + super().__init__(*a, **kw) + self._reloader = ModuleReloader(self.shell) + self._reloader.check_all = False + self._reloader.autoload_obj = False + self.loaded_modules = set(sys.modules) + + @line_magic + @magic_arguments.magic_arguments() + @magic_arguments.argument( + "mode", + type=str, + default="now", + nargs="?", + help="""blank or 'now' - Reload all modules (except those excluded by %%aimport) + automatically now. + + '0' or 'off' - Disable automatic reloading. + + '1' or 'explicit' - Reload only modules imported with %%aimport every + time before executing the Python code typed. + + '2' or 'all' - Reload all modules (except those excluded by %%aimport) + every time before executing the Python code typed. + + '3' or 'complete' - Same as 2/all, but also adds any new + objects in the module. + + By default, a newer autoreload algorithm that diffs the module's source code + with the previous version and only reloads changed parts is applied for modes + 2 and below. To use the original algorithm, add the `-` suffix to the mode, + e.g. '%autoreload 2-', or pass in --full. + """, + ) + @magic_arguments.argument( + "-p", + "--print", + action="store_true", + default=False, + help="Show autoreload activity using `print` statements", + ) + @magic_arguments.argument( + "-l", + "--log", + action="store_true", + default=False, + help="Show autoreload activity using the logger", + ) + @magic_arguments.argument( + "--hide-errors", + action="store_true", + default=False, + help="Hide autoreload errors", + ) + @magic_arguments.argument( + "--full", + action="store_true", + default=False, + help="Don't ever use new diffing algorithm", + ) + def autoreload(self, line=""): + r"""%autoreload => Reload modules automatically + + %autoreload or %autoreload now + Reload all modules (except those excluded by %aimport) automatically + now. + + %autoreload 0 or %autoreload off + Disable automatic reloading. + + %autoreload 1 or %autoreload explicit + Reload only modules imported with %aimport every time before executing + the Python code typed. + + %autoreload 2 or %autoreload all + Reload all modules (except those excluded by %aimport) every time + before executing the Python code typed. + + %autoreload 3 or %autoreload complete + Same as 2/all, but also but also adds any new objects in the module. See + unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects + + The optional arguments --print and --log control display of autoreload activity. The default + is to act silently; --print (or -p) will print out the names of modules that are being + reloaded, and --log (or -l) outputs them to the log at INFO level. + + The optional argument --hide-errors hides any errors that can happen when trying to + reload code. + + Reloading Python modules in a reliable way is in general + difficult, and unexpected things may occur. %autoreload tries to + work around common pitfalls by replacing function code objects and + parts of classes previously in the module with new versions. This + makes the following things to work: + + - Functions and classes imported via 'from xxx import foo' are upgraded + to new versions when 'xxx' is reloaded. + + - Methods and properties of classes are upgraded on reload, so that + calling 'c.foo()' on an object 'c' created before the reload causes + the new code for 'foo' to be executed. + + Some of the known remaining caveats are: + + - Replacing code objects does not always succeed: changing a @property + in a class to an ordinary method or a method to a member variable + can cause problems (but in old objects only). + + - Functions that are removed (eg. via monkey-patching) from a module + before it is reloaded are not upgraded. + + - C extension modules cannot be reloaded, and so cannot be + autoreloaded. + + """ + args = magic_arguments.parse_argstring(self.autoreload, line) + mode = args.mode.lower() + + enable_deduperreload = not args.full + if mode.endswith("-"): + enable_deduperreload = False + mode = mode[:-1] + self._reloader.deduper_reloader.enabled = enable_deduperreload + + p = print + + logger = logging.getLogger("autoreload") + + l = logger.info + + def pl(msg): + p(msg) + l(msg) + + if args.print is False and args.log is False: + self._reloader._report = lambda msg: None + elif args.print is True: + if args.log is True: + self._reloader._report = pl + else: + self._reloader._report = p + elif args.log is True: + self._reloader._report = l + + self._reloader.hide_errors = args.hide_errors + + if mode == "" or mode == "now": + self._reloader.check(True) + elif mode == "0" or mode == "off": + self._reloader.enabled = False + elif mode == "1" or mode == "explicit": + self._reloader.enabled = True + self._reloader.check_all = False + self._reloader.autoload_obj = False + elif mode == "2" or mode == "all": + self._reloader.enabled = True + self._reloader.check_all = True + self._reloader.autoload_obj = False + elif mode == "3" or mode == "complete": + self._reloader.enabled = True + self._reloader.check_all = True + self._reloader.autoload_obj = True + else: + raise ValueError(f'Unrecognized autoreload mode "{mode}".') + + @line_magic + def aimport(self, parameter_s="", stream=None): + """%aimport => Import modules for automatic reloading. + + %aimport + List modules to automatically import and not to import. + + %aimport foo + Import module 'foo' and mark it to be autoreloaded for %autoreload explicit + + %aimport foo, bar + Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload explicit + + %aimport -foo, bar + Mark module 'foo' to not be autoreloaded for %autoreload explicit, all, or complete, and 'bar' + to be autoreloaded for mode explicit. + """ + modname = parameter_s + if not modname: + to_reload = sorted(self._reloader.modules.keys()) + to_skip = sorted(self._reloader.skip_modules.keys()) + if stream is None: + stream = sys.stdout + if self._reloader.check_all: + stream.write("Modules to reload:\nall-except-skipped\n") + else: + stream.write("Modules to reload:\n%s\n" % " ".join(to_reload)) + stream.write("\nModules to skip:\n%s\n" % " ".join(to_skip)) + else: + for _module in [_.strip() for _ in modname.split(",")]: + if _module.startswith("-"): + _module = _module[1:].strip() + self._reloader.mark_module_skipped(_module) + else: + top_module, top_name = self._reloader.aimport_module(_module) + + # Inject module to user namespace + self.shell.push({top_name: top_module}) + + def pre_run_cell(self, info): + # Store the execution info for later use in post_execute_hook + self._last_execution_info = info + + if self._reloader.enabled: + try: + self._reloader.check() + except: + pass + + def post_execute_hook(self): + """Cache the modification times of any modules imported in this execution and track imports""" + + # Track imports from the recently executed code if autoreload 3 is enabled + if self._reloader.enabled and self._reloader.autoload_obj: + # Use the stored execution info + if ( + hasattr(self, "_last_execution_info") + and self._last_execution_info + and self._last_execution_info.transformed_cell + ): + self._track_imports_from_code( + self._last_execution_info.transformed_cell + ) + + newly_loaded_modules = set(sys.modules) - self.loaded_modules + for modname in newly_loaded_modules: + _, pymtime = self._reloader.filename_and_mtime(sys.modules[modname]) + if pymtime is not None: + self._reloader.modules_mtimes[modname] = pymtime + + self.loaded_modules.update(newly_loaded_modules) + + def _track_imports_from_code(self, code: str) -> None: + """Track import statements from executed code""" + try: + tree = ast.parse(code) + + for node in ast.walk(tree): + # Handle "from X import Y" style imports + if isinstance(node, ast.ImportFrom): + mod = node.module + + # Skip relative imports that don't have a module name + if mod is None: + continue + + for name in node.names: + # name.name is going to be actual name that we want to import from module + # name.asname is Z in the case of from X import Y as Z + # we should update Z in the shell in this situation, so track it too. + original_name = name.name + resolved_name = name.asname if name.asname else name.name + + # Since the code executed successfully, we know this import is valid + self._reloader.import_from_tracker.add_import( + mod, original_name, resolved_name + ) + except (SyntaxError, ValueError): + # If there's a syntax error, skip import tracking + # (though this shouldn't happen since the code already executed successfully) + pass + + +def load_ipython_extension(ip): + """Load the extension in IPython.""" + auto_reload = AutoreloadMagics(ip) + ip.register_magics(auto_reload) + ip.events.register("pre_run_cell", auto_reload.pre_run_cell) + ip.events.register("post_execute", auto_reload.post_execute_hook) diff --git a/lib/python3.12/site-packages/IPython/extensions/deduperreload/__init__.py b/lib/python3.12/site-packages/IPython/extensions/deduperreload/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..efd88889665cb499581ab6e7fa3194a59d1d6dd6 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/deduperreload.cpython-312.pyc b/lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/deduperreload.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24c424d4529d09803107166fafead8801a28cd1c Binary files /dev/null and b/lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/deduperreload.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/deduperreload_patching.cpython-312.pyc b/lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/deduperreload_patching.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b585228e470d933645c9f3e359fe5972bc4c11df Binary files /dev/null and b/lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/deduperreload_patching.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/extensions/deduperreload/deduperreload.py b/lib/python3.12/site-packages/IPython/extensions/deduperreload/deduperreload.py new file mode 100644 index 0000000000000000000000000000000000000000..ad470b6d46c3cf9a45712d22b278e07b71de0fbc --- /dev/null +++ b/lib/python3.12/site-packages/IPython/extensions/deduperreload/deduperreload.py @@ -0,0 +1,637 @@ +from __future__ import annotations +import ast +import builtins +import contextlib +import itertools +import logging +import os +import pickle +import platform +import sys +import textwrap +from types import ModuleType +from typing import TYPE_CHECKING, Any, NamedTuple, cast +from collections.abc import Generator, Iterable + +from IPython.extensions.deduperreload.deduperreload_patching import ( + DeduperReloaderPatchingMixin, +) + +if TYPE_CHECKING: + TDefinitionAst = ( + ast.FunctionDef + | ast.AsyncFunctionDef + | ast.Import + | ast.ImportFrom + | ast.Assign + | ast.AnnAssign + ) + + +def get_module_file_name(module: ModuleType | str) -> str | None: + """Returns the module's file path, or the empty string if it's inaccessible""" + if (mod := sys.modules.get(module) if isinstance(module, str) else module) is None: + return "" + return getattr(mod, "__file__", "") or "" + + +def compare_ast(node1: ast.AST | list[ast.AST], node2: ast.AST | list[ast.AST]) -> bool: + """Checks if node1 and node2 have identical AST structure/values, apart from some attributes""" + if type(node1) is not type(node2): + return False + + if isinstance(node1, ast.AST): + for k, v in node1.__dict__.items(): + if k in ( + "lineno", + "end_lineno", + "col_offset", + "end_col_offset", + "ctx", + "parent", + ): + continue + if not hasattr(node2, k) or not compare_ast(v, getattr(node2, k)): + return False + return True + + elif isinstance(node1, list) and isinstance( # type:ignore [redundant-expr] + node2, list + ): + return len(node1) == len(node2) and all( + compare_ast(n1, n2) for n1, n2 in zip(node1, node2) + ) + else: + return node1 == node2 + + +class DependencyNode(NamedTuple): + """ + Each node represents a function. + qualified_name: string which represents the namespace/name of the function + abstract_syntax_tree: subtree of the overall module which corresponds to this function + + qualified_name is of the structure: (namespace1, namespace2, ..., name) + + For example, foo() in the following would be represented as (A, B, foo): + + class A: + class B: + def foo(): + pass + """ + + qualified_name: tuple[str, ...] + abstract_syntax_tree: ast.AST + + +class GatherResult(NamedTuple): + import_defs: list[tuple[tuple[str, ...], ast.Import | ast.ImportFrom]] = [] + assign_defs: list[tuple[tuple[str, ...], ast.Assign | ast.AnnAssign]] = [] + function_defs: list[ + tuple[tuple[str, ...], ast.FunctionDef | ast.AsyncFunctionDef] + ] = [] + classes: dict[str, ast.ClassDef] = {} + unfixable: list[ast.AST] = [] + + @classmethod + def create(cls) -> GatherResult: + return cls([], [], [], {}, []) + + def all_defs(self) -> Iterable[tuple[tuple[str, ...], TDefinitionAst]]: + return itertools.chain(self.import_defs, self.assign_defs, self.function_defs) + + def inplace_merge(self, other: GatherResult) -> None: + self.import_defs.extend(other.import_defs) + self.assign_defs.extend(other.assign_defs) + self.function_defs.extend(other.function_defs) + self.classes.update(other.classes) + self.unfixable.extend(other.unfixable) + + +class ConstexprDetector(ast.NodeVisitor): + def __init__(self) -> None: + self.is_constexpr = True + self._allow_builtins_exceptions = True + + @contextlib.contextmanager + def disallow_builtins_exceptions(self) -> Generator[None, None, None]: + prev_allow = self._allow_builtins_exceptions + self._allow_builtins_exceptions = False + try: + yield + finally: + self._allow_builtins_exceptions = prev_allow + + def visit_Attribute(self, node: ast.Attribute) -> None: + with self.disallow_builtins_exceptions(): + self.visit(node.value) + + def visit_Name(self, node: ast.Name) -> None: + if self._allow_builtins_exceptions and hasattr(builtins, node.id): + return + self.is_constexpr = False + + def visit(self, node: ast.AST) -> None: + if not self.is_constexpr: + # can short-circuit if we've already detected that it's not a constexpr + return + super().visit(node) + + def __call__(self, node: ast.AST) -> bool: + self.is_constexpr = True + self.visit(node) + return self.is_constexpr + + +class AutoreloadTree: + """ + Recursive data structure to keep track of reloadable functions/methods. Each object corresponds to a specific scope level. + children: classes inside given scope, maps class name to autoreload tree for that class's scope + funcs_to_autoreload: list of function names that can be autoreloaded in given scope. + new_nested_classes: Classes getting added in new autoreload cycle + """ + + def __init__(self) -> None: + self.children: dict[str, AutoreloadTree] = {} + self.defs_to_reload: list[tuple[tuple[str, ...], ast.AST]] = [] + self.defs_to_delete: set[str] = set() + self.new_nested_classes: dict[str, ast.AST] = {} + + def traverse_prefixes(self, prefixes: list[str]) -> AutoreloadTree: + """ + Return ref to the AutoreloadTree at the namespace specified by prefixes + """ + cur = self + for prefix in prefixes: + if prefix not in cur.children: + cur.children[prefix] = AutoreloadTree() + cur = cur.children[prefix] + return cur + + +class DeduperReloader(DeduperReloaderPatchingMixin): + """ + This version of autoreload detects when we can leverage targeted recompilation of a subset of a module and patching + existing function/method objects to reflect these changes. + + Detects what functions/methods can be reloaded by recursively comparing the old/new AST of module-level classes, + module-level classes' methods, recursing through nested classes' methods. If other changes are made, original + autoreload algorithm is called directly. + """ + + def __init__(self) -> None: + self._to_autoreload: AutoreloadTree = AutoreloadTree() + self.source_by_modname: dict[str, str] = {} + self.dependency_graph: dict[tuple[str, ...], list[DependencyNode]] = {} + self._enabled = True + + @property + def enabled(self) -> bool: + return self._enabled and platform.python_implementation() == "CPython" + + @enabled.setter + def enabled(self, value: bool) -> None: + self._enabled = value + + def update_sources(self) -> None: + """ + Update dictionary source_by_modname with current modules' source codes. + """ + if not self.enabled: + return + for new_modname in sys.modules.keys() - self.source_by_modname.keys(): + new_module = sys.modules[new_modname] + if ( + (fname := get_module_file_name(new_module)) is None + or "site-packages" in fname + or "dist-packages" in fname + or not os.access(fname, os.R_OK) + ): + self.source_by_modname[new_modname] = "" + continue + # Skip binary files (e.g., .so, .pyd, .pyo) + if not fname.endswith(".py"): + self.source_by_modname[new_modname] = "" + continue + try: + with open(fname, "r") as f: + self.source_by_modname[new_modname] = f.read() + except Exception as e: + logger = logging.getLogger("autoreload") + logger.exception( + f"Failed to read module file '{fname}' for module '{new_modname}': {type(e).__name__}" + ) + self.source_by_modname[new_modname] = "" + + constexpr_detector = ConstexprDetector() + + @staticmethod + def is_enum_subclass(node: ast.Module | ast.ClassDef) -> bool: + if isinstance(node, ast.Module): + return False + for base in node.bases: + if isinstance(base, ast.Name) and base.id == "Enum": + return True + elif ( + isinstance(base, ast.Attribute) + and base.attr == "Enum" + and isinstance(base.value, ast.Name) + and base.value.id == "enum" + ): + return True + return False + + @classmethod + def is_constexpr_assign( + cls, node: ast.AST, parent_node: ast.Module | ast.ClassDef + ) -> bool: + if not isinstance(node, (ast.Assign, ast.AnnAssign)) or node.value is None: + return False + if cls.is_enum_subclass(parent_node): + return False + for target in node.targets if isinstance(node, ast.Assign) else [node.target]: + if not isinstance(target, ast.Name): + return False + return cls.constexpr_detector(node.value) + + @classmethod + def _gather_children( + cls, body: list[ast.stmt], parent_node: ast.Module | ast.ClassDef + ) -> GatherResult: + """ + Given list of ast elements, return: + 1. dict mapping function names to their ASTs. + 2. dict mapping class names to their ASTs. + 3. list of any other ASTs. + """ + result = GatherResult.create() + for ast_node in body: + ast_elt: ast.expr | ast.stmt = ast_node + while isinstance(ast_elt, ast.Expr): + ast_elt = ast_elt.value + if isinstance(ast_elt, (ast.FunctionDef, ast.AsyncFunctionDef)): + result.function_defs.append(((ast_elt.name,), ast_elt)) + elif isinstance(ast_elt, (ast.Import, ast.ImportFrom)): + result.import_defs.append( + (tuple(name.asname or name.name for name in ast_elt.names), ast_elt) + ) + elif isinstance(ast_elt, ast.ClassDef): + result.classes[ast_elt.name] = ast_elt + elif isinstance(ast_elt, ast.If): + result.unfixable.append(ast_elt.test) + result.inplace_merge(cls._gather_children(ast_elt.body, parent_node)) + result.inplace_merge(cls._gather_children(ast_elt.orelse, parent_node)) + elif isinstance(ast_elt, (ast.AsyncWith, ast.With)): + result.unfixable.extend(ast_elt.items) + result.inplace_merge(cls._gather_children(ast_elt.body, parent_node)) + elif isinstance(ast_elt, ast.Try): + result.inplace_merge(cls._gather_children(ast_elt.body, parent_node)) + result.inplace_merge(cls._gather_children(ast_elt.orelse, parent_node)) + result.inplace_merge( + cls._gather_children(ast_elt.finalbody, parent_node) + ) + for handler in ast_elt.handlers: + if handler.type is not None: + result.unfixable.append(handler.type) + result.inplace_merge( + cls._gather_children(handler.body, parent_node) + ) + elif not isinstance(ast_elt, (ast.Constant, ast.Pass)): + if cls.is_constexpr_assign(ast_elt, parent_node): + assert isinstance(ast_elt, (ast.Assign, ast.AnnAssign)) + targets = ( + ast_elt.targets + if isinstance(ast_elt, ast.Assign) + else [ast_elt.target] + ) + result.assign_defs.append( + ( + tuple(cast(ast.Name, target).id for target in targets), + ast_elt, + ) + ) + else: + result.unfixable.append(ast_elt) + return result + + def detect_autoreload( + self, + old_node: ast.Module | ast.ClassDef, + new_node: ast.Module | ast.ClassDef, + prefixes: list[str] | None = None, + ) -> bool: + """ + Returns + ------- + `True` if we can run our targeted autoreload algorithm safely. + `False` if we should instead use IPython's original autoreload implementation. + """ + if not self.enabled: + return False + prefixes = prefixes or [] + + old_result = self._gather_children(old_node.body, old_node) + new_result = self._gather_children(new_node.body, new_node) + old_defs_by_name: dict[str, ast.AST] = { + name: ast_def for names, ast_def in old_result.all_defs() for name in names + } + new_defs_by_name: dict[str, ast.AST] = { + name: ast_def for names, ast_def in new_result.all_defs() for name in names + } + + if not compare_ast(old_result.unfixable, new_result.unfixable): + return False + + cur = self._to_autoreload.traverse_prefixes(prefixes) + for names, new_ast_def in new_result.all_defs(): + names_to_reload = [] + for name in names: + if new_defs_by_name[name] is not new_ast_def: + continue + if name not in old_defs_by_name or not compare_ast( + new_ast_def, old_defs_by_name[name] + ): + names_to_reload.append(name) + if names_to_reload: + cur.defs_to_reload.append((tuple(names), new_ast_def)) + cur.defs_to_delete |= set(old_defs_by_name.keys()) - set( + new_defs_by_name.keys() + ) + for name, new_ast_def_class in new_result.classes.items(): + if name not in old_result.classes: + cur.new_nested_classes[name] = new_ast_def_class + elif not compare_ast( + new_ast_def_class, old_result.classes[name] + ) and not self.detect_autoreload( + old_result.classes[name], new_ast_def_class, prefixes + [name] + ): + return False + return True + + def _check_dependents(self) -> bool: + """ + If a decorator function is modified, we should similarly reload the functions which are decorated by this + decorator. Iterate through the Dependency Graph to find such cases in the given AutoreloadTree. + """ + for node in self._check_dependents_inner(): + self._add_node_to_autoreload_tree(node) + return True + + def _add_node_to_autoreload_tree(self, node: DependencyNode) -> None: + """ + Given a node of the dependency graph, add decorator dependencies to the autoreload tree. + """ + if len(node.qualified_name) == 0: + return + cur = self._to_autoreload.traverse_prefixes(list(node.qualified_name[:-1])) + if node.abstract_syntax_tree is not None: + cur.defs_to_reload.append( + ((node.qualified_name[-1],), node.abstract_syntax_tree) + ) + + def _check_dependents_inner( + self, prefixes: list[str] | None = None + ) -> list[DependencyNode]: + prefixes = prefixes or [] + cur = self._to_autoreload.traverse_prefixes(prefixes) + ans = [] + for (func_name, *_), _ in cur.defs_to_reload: + node = tuple(prefixes + [func_name]) + ans.extend(self._gen_dependents(node)) + for class_name in cur.new_nested_classes: + ans.extend(self._check_dependents_inner(prefixes + [class_name])) + return ans + + def _gen_dependents(self, qualname: tuple[str, ...]) -> list[DependencyNode]: + ans = [] + if qualname not in self.dependency_graph: + return [] + for elt in self.dependency_graph[qualname]: + ans.extend(self._gen_dependents(elt.qualified_name)) + ans.append(elt) + return ans + + def _patch_namespace_inner( + self, ns: ModuleType | type, prefixes: list[str] | None = None + ) -> bool: + """ + This function patches module functions and methods. Specifically, only objects with their name in + self.to_autoreload will be considered for patching. If an object has been marked to be autoreloaded, + new_source_code gets executed in the old version's global environment. Then, replace the old function's + attributes with the new function's attributes. + """ + prefixes = prefixes or [] + cur = self._to_autoreload.traverse_prefixes(prefixes) + namespace_to_check = ns + for prefix in prefixes: + namespace_to_check = namespace_to_check.__dict__[prefix] + seen_names: set[str] = set() + for names, new_ast_def in cur.defs_to_reload: + if len(names) == 1 and names[0] in seen_names: + continue + seen_names.update(names) + local_env: dict[str, Any] = {} + if ( + isinstance(new_ast_def, (ast.FunctionDef, ast.AsyncFunctionDef)) + and (name := names[0]) in namespace_to_check.__dict__ + ): + assert len(names) == 1 + to_patch_to = namespace_to_check.__dict__[name] + if isinstance(to_patch_to, (staticmethod, classmethod)): + to_patch_to = to_patch_to.__func__ + # exec new source code using old function's (obj) globals environment. + func_code = textwrap.dedent(ast.unparse(new_ast_def)) + if is_method := (len(prefixes) > 0): + func_code = "class __autoreload_class__:\n" + textwrap.indent( + func_code, " " + ) + global_env = ns.__dict__ + if not isinstance(global_env, dict): + global_env = dict(global_env) + # Compile with correct filename to preserve in traceback + filename = ( + getattr(to_patch_to, "__code__", None) + and to_patch_to.__code__.co_filename + or "" + ) + func_asts = [ast.parse(func_code)] + if len(cast(ast.FunctionDef, func_asts[0].body[0]).decorator_list) > 0: + without_decorator_list = pickle.loads(pickle.dumps(func_asts[0])) + cast( + ast.FunctionDef, without_decorator_list.body[0] + ).decorator_list = [] + func_asts.insert(0, without_decorator_list) + for func_ast in func_asts: + compiled_code = compile( + func_ast, filename, mode="exec", dont_inherit=True + ) + exec(compiled_code, global_env, local_env) # type: ignore[arg-type] + # local_env contains the function exec'd from new version of function + if is_method: + to_patch_from = getattr(local_env["__autoreload_class__"], name) + else: + to_patch_from = local_env[name] + if isinstance(to_patch_from, (staticmethod, classmethod)): + to_patch_from = to_patch_from.__func__ + if isinstance(to_patch_to, property) and isinstance( + to_patch_from, property + ): + for attr in ("fget", "fset", "fdel"): + if ( + getattr(to_patch_to, attr) is None + or getattr(to_patch_from, attr) is None + ): + self.try_patch_attr(to_patch_to, to_patch_from, attr) + else: + self.patch_function( + getattr(to_patch_to, attr), + getattr(to_patch_from, attr), + is_method, + ) + elif not isinstance(to_patch_to, property) and not isinstance( + to_patch_from, property + ): + self.patch_function(to_patch_to, to_patch_from, is_method) + else: + raise ValueError( + "adding or removing property decorations not supported" + ) + else: + exec( + ast.unparse(new_ast_def), + ns.__dict__ | namespace_to_check.__dict__, + local_env, + ) + for name in names: + setattr(namespace_to_check, name, local_env[name]) + cur.defs_to_reload.clear() + for name in cur.defs_to_delete: + try: + delattr(namespace_to_check, name) + except (AttributeError, TypeError, ValueError): + # give up on deleting the attribute, let the stale one dangle + pass + cur.defs_to_delete.clear() + for class_name, class_ast_node in cur.new_nested_classes.items(): + local_env_class: dict[str, Any] = {} + exec( + ast.unparse(class_ast_node), + ns.__dict__ | namespace_to_check.__dict__, + local_env_class, + ) + setattr(namespace_to_check, class_name, local_env_class[class_name]) + cur.new_nested_classes.clear() + for class_name in cur.children.keys(): + if not self._patch_namespace(ns, prefixes + [class_name]): + return False + cur.children.clear() + return True + + def _patch_namespace( + self, ns: ModuleType | type, prefixes: list[str] | None = None + ) -> bool: + """ + Wrapper for patching all elements in a namespace as specified by the to_autoreload member variable. + Returns `true` if patching was successful, and `false` if unsuccessful. + """ + try: + return self._patch_namespace_inner(ns, prefixes=prefixes) + except Exception: + return False + + def maybe_reload_module(self, module: ModuleType) -> bool: + """ + Uses Deduperreload to try to update a module. + Returns `true` on success and `false` on failure. + """ + if not self.enabled: + return False + if not (modname := getattr(module, "__name__", None)): + return False + if (fname := get_module_file_name(module)) is None: + return False + try: + with open(fname, "r") as f: + new_source_code = f.read() + except Exception as e: + logger = logging.getLogger("autoreload") + logger.exception( + f"Failed to read module file '{fname}' for module '{modname}': {type(e).__name__}" + ) + return False + patched_flag = False + if old_source_code := self.source_by_modname.get(modname): + # get old/new module ast + try: + old_module_ast = ast.parse(old_source_code) + new_module_ast = ast.parse(new_source_code) + except Exception: + return False + # detect if we are able to use our autoreload algorithm + ctx = contextlib.suppress() + with ctx: + self._build_dependency_graph(new_module_ast) + if ( + self.detect_autoreload(old_module_ast, new_module_ast) + and self._check_dependents() + and self._patch_namespace(module) + ): + patched_flag = True + + self.source_by_modname[modname] = new_source_code + self._to_autoreload = AutoreloadTree() + return patched_flag + + def _separate_name( + self, + decorator: ast.Attribute | ast.Name | ast.Call | ast.expr, + accept_calls: bool, + ) -> list[str] | None: + """ + Generates a qualified name for a given decorator by finding its relative namespace. + """ + if isinstance(decorator, ast.Name): + return [decorator.id] + elif isinstance(decorator, ast.Call): + if accept_calls: + return self._separate_name(decorator.func, False) + else: + return None + if not isinstance(decorator, ast.Attribute): + return None + if pref := self._separate_name(decorator.value, False): + return pref + [decorator.attr] + else: + return None + + def _gather_dependents( + self, body: list[ast.stmt], body_prefixes: list[str] | None = None + ) -> bool: + body_prefixes = body_prefixes or [] + for ast_node in body: + ast_elt: ast.expr | ast.stmt = ast_node + if isinstance(ast_elt, ast.ClassDef): + self._gather_dependents(ast_elt.body, body_prefixes + [ast_elt.name]) + continue + if not isinstance(ast_elt, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + qualified_name = tuple(body_prefixes + [ast_elt.name]) + cur_dependency_node = DependencyNode(qualified_name, ast_elt) + for decorator in ast_elt.decorator_list: + decorator_path = self._separate_name(decorator, True) + if not decorator_path: + continue + decorator_path_tuple = tuple(decorator_path) + self.dependency_graph.setdefault(decorator_path_tuple, []).append( + cur_dependency_node + ) + return True + + def _build_dependency_graph(self, new_ast: ast.Module | ast.ClassDef) -> bool: + """ + Wrapper function for generating dependency graph given some AST. + Returns `true` on success. Returns `false` on failure. + Currently, only returns `true` as we do not block on failure to build this graph. + """ + return self._gather_dependents(new_ast.body) diff --git a/lib/python3.12/site-packages/IPython/extensions/deduperreload/deduperreload_patching.py b/lib/python3.12/site-packages/IPython/extensions/deduperreload/deduperreload_patching.py new file mode 100644 index 0000000000000000000000000000000000000000..3bd2a2c928d751d29bf1e13e6012f4b1409847f7 --- /dev/null +++ b/lib/python3.12/site-packages/IPython/extensions/deduperreload/deduperreload_patching.py @@ -0,0 +1,142 @@ +from __future__ import annotations +import ctypes +import sys +from typing import Any + +NOT_FOUND: object = object() +NULL: object = object() +_MAX_FIELD_SEARCH_OFFSET = 50 + +POINTER_N_BYTES = ctypes.sizeof(ctypes.c_void_p) + + +class DeduperReloaderPatchingMixin: + @staticmethod + def infer_field_offset( + obj: object, + field: str, + ) -> int: + field_value = getattr(obj, field, NOT_FOUND) + if field_value is NOT_FOUND: + return -1 + obj_addr = ctypes.c_void_p.from_buffer(ctypes.py_object(obj)).value + field_addr = ctypes.c_void_p.from_buffer(ctypes.py_object(field_value)).value + if obj_addr is None or field_addr is None: + return -1 + ret = -1 + for offset in range(1, _MAX_FIELD_SEARCH_OFFSET): + if ( + ctypes.cast( + obj_addr + POINTER_N_BYTES * offset, ctypes.POINTER(ctypes.c_void_p) + ).contents.value + == field_addr + ): + ret = offset + break + return ret + + @classmethod + def try_write_readonly_attr( + cls, + obj: object, + field: str, + new_value: object, + offset: int | None = None, + ) -> None: + prev_value = getattr(obj, field, NOT_FOUND) + if prev_value is NOT_FOUND: + return + if offset is None: + offset = cls.infer_field_offset(obj, field) + if offset == -1: + return + obj_addr = ctypes.c_void_p.from_buffer(ctypes.py_object(obj)).value + if new_value is NULL: + new_value_addr: int | None = 0 + else: + new_value_addr = ctypes.c_void_p.from_buffer( + ctypes.py_object(new_value) + ).value + if obj_addr is None or new_value_addr is None: + return + if prev_value is not None: + ctypes.pythonapi.Py_DecRef(ctypes.py_object(prev_value)) + if new_value not in (None, NULL): + ctypes.pythonapi.Py_IncRef(ctypes.py_object(new_value)) + ctypes.cast( + obj_addr + POINTER_N_BYTES * offset, ctypes.POINTER(ctypes.c_void_p) + ).contents.value = new_value_addr + + @classmethod + def try_patch_readonly_attr( + cls, + old: object, + new: object, + field: str, + new_is_value: bool = False, + offset: int = -1, + ) -> None: + old_value = getattr(old, field, NOT_FOUND) + new_value = new if new_is_value else getattr(new, field, NOT_FOUND) + if old_value is NOT_FOUND or new_value is NOT_FOUND: + return + elif old_value is new_value: + return + elif old_value is not None and offset < 0: + offset = cls.infer_field_offset(old, field) + elif offset < 0: + assert not new_is_value + assert new_value is not None + offset = cls.infer_field_offset(new, field) + cls.try_write_readonly_attr(old, field, new_value, offset=offset) + + @classmethod + def try_patch_attr( + cls, + old: object, + new: object, + field: str, + new_is_value: bool = False, + offset: int = -1, + ) -> None: + try: + setattr(old, field, new if new_is_value else getattr(new, field)) + except (AttributeError, TypeError, ValueError): + cls.try_patch_readonly_attr(old, new, field, new_is_value, offset) + + @classmethod + def patch_function( + cls, to_patch_to: Any, to_patch_from: Any, is_method: bool + ) -> None: + new_closure = [] + for freevar, closure_val in zip( + to_patch_from.__code__.co_freevars or [], to_patch_from.__closure__ or [] + ): + if ( + callable(closure_val.cell_contents) + and freevar in to_patch_to.__code__.co_freevars + ): + new_closure.append( + to_patch_to.__closure__[ + to_patch_to.__code__.co_freevars.index(freevar) + ] + ) + else: + new_closure.append(closure_val) + # lambdas may complain if there is more than one freevar + cls.try_patch_attr(to_patch_to, to_patch_from, "__code__") + offset = -1 + if to_patch_to.__closure__ is None and to_patch_from.__closure__ is not None: + offset = cls.infer_field_offset(to_patch_from, "__closure__") + if to_patch_to.__closure__ is not None or to_patch_from.__closure__ is not None: + cls.try_patch_readonly_attr( + to_patch_to, + tuple(new_closure) or NULL, + "__closure__", + new_is_value=True, + offset=offset, + ) + for attr in ("__defaults__", "__kwdefaults__", "__doc__", "__dict__"): + cls.try_patch_attr(to_patch_to, to_patch_from, attr) + if is_method: + cls.try_patch_readonly_attr(to_patch_to, to_patch_from, "__self__") diff --git a/lib/python3.12/site-packages/IPython/extensions/storemagic.py b/lib/python3.12/site-packages/IPython/extensions/storemagic.py new file mode 100644 index 0000000000000000000000000000000000000000..1ab56f72ea8cf571039a1bcd38541ca2a1b1cc79 --- /dev/null +++ b/lib/python3.12/site-packages/IPython/extensions/storemagic.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +""" +%store magic for lightweight persistence. + +Stores variables, aliases and macros in IPython's database. + +To automatically restore stored variables at startup, add this to your +:file:`ipython_config.py` file:: + + c.StoreMagics.autorestore = True +""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +import inspect, os, sys, textwrap + +from IPython.core.error import UsageError +from IPython.core.magic import Magics, magics_class, line_magic +from IPython.testing.skipdoctest import skip_doctest +from traitlets import Bool + + +def restore_aliases(ip, alias=None): + staliases = ip.db.get('stored_aliases', {}) + if alias is None: + for k,v in staliases.items(): + # print("restore alias",k,v) # dbg + #self.alias_table[k] = v + ip.alias_manager.define_alias(k,v) + else: + ip.alias_manager.define_alias(alias, staliases[alias]) + + +def refresh_variables(ip): + db = ip.db + for key in db.keys('autorestore/*'): + # strip autorestore + justkey = os.path.basename(key) + try: + obj = db[key] + except KeyError: + print("Unable to restore variable '%s', ignoring (use %%store -d to forget!)" % justkey) + print("The error was:", sys.exc_info()[0]) + else: + # print("restored",justkey,"=",obj) # dbg + ip.user_ns[justkey] = obj + + +def restore_dhist(ip): + ip.user_ns['_dh'] = ip.db.get('dhist',[]) + + +def restore_data(ip): + refresh_variables(ip) + restore_aliases(ip) + restore_dhist(ip) + + +@magics_class +class StoreMagics(Magics): + """Lightweight persistence for python variables. + + Provides the %store magic.""" + + autorestore = Bool(False, help= + """If True, any %store-d variables will be automatically restored + when IPython starts. + """ + ).tag(config=True) + + def __init__(self, shell): + super(StoreMagics, self).__init__(shell=shell) + self.shell.configurables.append(self) + if self.autorestore: + restore_data(self.shell) + + @skip_doctest + @line_magic + def store(self, parameter_s=''): + """Lightweight persistence for python variables. + + Example:: + + In [1]: l = ['hello',10,'world'] + In [2]: %store l + Stored 'l' (list) + In [3]: exit + + (IPython session is closed and started again...) + + ville@badger:~$ ipython + In [1]: l + NameError: name 'l' is not defined + In [2]: %store -r + In [3]: l + Out[3]: ['hello', 10, 'world'] + + Usage: + + * ``%store`` - Show list of all variables and their current + values + * ``%store spam bar`` - Store the *current* value of the variables spam + and bar to disk + * ``%store -d spam`` - Remove the variable and its value from storage + * ``%store -z`` - Remove all variables from storage + * ``%store -r`` - Refresh all variables, aliases and directory history + from store (overwrite current vals) + * ``%store -r spam bar`` - Refresh specified variables and aliases from store + (delete current val) + * ``%store foo >a.txt`` - Store value of foo to new file a.txt + * ``%store foo >>a.txt`` - Append value of foo to file a.txt + + It should be noted that if you change the value of a variable, you + need to %store it again if you want to persist the new value. + + Note also that the variables will need to be pickleable; most basic + python types can be safely %store'd. + + Also aliases can be %store'd across sessions. + To remove an alias from the storage, use the %unalias magic. + """ + + opts,argsl = self.parse_options(parameter_s,'drz',mode='string') + args = argsl.split() + ip = self.shell + db = ip.db + # delete + if 'd' in opts: + try: + todel = args[0] + except IndexError as e: + raise UsageError('You must provide the variable to forget') from e + else: + try: + del db['autorestore/' + todel] + except BaseException as e: + raise UsageError("Can't delete variable '%s'" % todel) from e + # reset + elif 'z' in opts: + for k in db.keys('autorestore/*'): + del db[k] + + elif 'r' in opts: + if args: + for arg in args: + try: + obj = db["autorestore/" + arg] + except KeyError: + try: + restore_aliases(ip, alias=arg) + except KeyError: + print("no stored variable or alias %s" % arg) + else: + ip.user_ns[arg] = obj + else: + restore_data(ip) + + # run without arguments -> list variables & values + elif not args: + vars = db.keys('autorestore/*') + vars.sort() + if vars: + size = max(map(len, vars)) + else: + size = 0 + + print('Stored variables and their in-db values:') + fmt = '%-'+str(size)+'s -> %s' + get = db.get + for var in vars: + justkey = os.path.basename(var) + # print 30 first characters from every var + print(fmt % (justkey, repr(get(var, ''))[:50])) + + # default action - store the variable + else: + # %store foo >file.txt or >>file.txt + if len(args) > 1 and args[1].startswith(">"): + fnam = os.path.expanduser(args[1].lstrip(">").lstrip()) + if args[1].startswith(">>"): + fil = open(fnam, "a", encoding="utf-8") + else: + fil = open(fnam, "w", encoding="utf-8") + with fil: + obj = ip.ev(args[0]) + print("Writing '%s' (%s) to file '%s'." % (args[0], + obj.__class__.__name__, fnam)) + + if not isinstance (obj, str): + from pprint import pprint + pprint(obj, fil) + else: + fil.write(obj) + if not obj.endswith('\n'): + fil.write('\n') + + return + + # %store foo + for arg in args: + try: + obj = ip.user_ns[arg] + except KeyError: + # it might be an alias + name = arg + try: + cmd = ip.alias_manager.retrieve_alias(name) + except ValueError as e: + raise UsageError("Unknown variable '%s'" % name) from e + + staliases = db.get('stored_aliases',{}) + staliases[name] = cmd + db['stored_aliases'] = staliases + print("Alias stored: %s (%s)" % (name, cmd)) + return + + else: + modname = getattr(inspect.getmodule(obj), '__name__', '') + if modname == '__main__': + print(textwrap.dedent("""\ + Warning:%s is %s + Proper storage of interactively declared classes (or instances + of those classes) is not possible! Only instances + of classes in real modules on file system can be %%store'd. + """ % (arg, obj) )) + return + #pickled = pickle.dumps(obj) + db[ 'autorestore/' + arg ] = obj + print("Stored '%s' (%s)" % (arg, obj.__class__.__name__)) + + +def load_ipython_extension(ip): + """Load the extension in IPython.""" + ip.register_magics(StoreMagics) + diff --git a/lib/python3.12/site-packages/IPython/terminal/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..691d040ba6fc733c318c6de21a0c9cae41dde9cf Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/__pycache__/debugger.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/__pycache__/debugger.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0b93a05bc0b1df84e945f80f1377f724d090548 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/__pycache__/debugger.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/__pycache__/embed.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/__pycache__/embed.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d91a064fa5f9362a583855113fa4d9b18bff333 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/__pycache__/embed.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/__pycache__/interactiveshell.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/__pycache__/interactiveshell.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e28311c93f6a03275d193dce1e8fea9bc8fcc5ba Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/__pycache__/interactiveshell.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/__pycache__/ipapp.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/__pycache__/ipapp.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3a352b26a184e6fad560d5f5cfbe31956f036dd Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/__pycache__/ipapp.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/__pycache__/magics.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/__pycache__/magics.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b7f5f274f47de1273bd2b692b85d880f2fa0604b Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/__pycache__/magics.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/__pycache__/prompts.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/__pycache__/prompts.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a24bac0ba608e61824e528d35a7f7736478c5639 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/__pycache__/prompts.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/__pycache__/ptutils.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/__pycache__/ptutils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37dfc9d2df537bd3004568612fd65900e91740e7 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/__pycache__/ptutils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f1ee770e606747d792f5205992648b08ffefd2b Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/auto_match.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/auto_match.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b304badf579552549c0104596e096c18f2e8b220 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/auto_match.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/auto_suggest.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/auto_suggest.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14c49054a8ded803957776a978a83aee8f2293d7 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/auto_suggest.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/filters.cpython-312.pyc b/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/filters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..520142cd10e6bb338b7bf282b7c043088a24faf3 Binary files /dev/null and b/lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/filters.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/IPython/terminal/shortcuts/auto_suggest.py b/lib/python3.12/site-packages/IPython/terminal/shortcuts/auto_suggest.py new file mode 100644 index 0000000000000000000000000000000000000000..91d238329ca501f9e71818962c252fc57548d1b6 --- /dev/null +++ b/lib/python3.12/site-packages/IPython/terminal/shortcuts/auto_suggest.py @@ -0,0 +1,658 @@ +import re +import asyncio +import tokenize +from io import StringIO +from typing import List, Optional, Union, Tuple, ClassVar, Any +from collections.abc import Callable, Generator +import warnings + +import prompt_toolkit +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.key_binding import KeyPressEvent +from prompt_toolkit.key_binding.bindings import named_commands as nc +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion +from prompt_toolkit.document import Document +from prompt_toolkit.history import History +from prompt_toolkit.shortcuts import PromptSession +from prompt_toolkit.layout.processors import ( + Processor, + Transformation, + TransformationInput, +) + +from IPython.core.getipython import get_ipython +from IPython.utils.tokenutil import generate_tokens + +from .filters import pass_through + + +def _get_query(document: Document): + return document.lines[document.cursor_position_row] + + +class AppendAutoSuggestionInAnyLine(Processor): + """ + Append the auto suggestion to lines other than the last (appending to the + last line is natively supported by the prompt toolkit). + + This has a private `_debug` attribute that can be set to True to display + debug information as virtual suggestion on the end of any line. You can do + so with: + + >>> from IPython.terminal.shortcuts.auto_suggest import AppendAutoSuggestionInAnyLine + >>> AppendAutoSuggestionInAnyLine._debug = True + + """ + + _debug: ClassVar[bool] = False + + def __init__(self, style: str = "class:auto-suggestion") -> None: + self.style = style + + def apply_transformation(self, ti: TransformationInput) -> Transformation: + """ + Apply transformation to the line that is currently being edited. + + This is a variation of the original implementation in prompt toolkit + that allows to not only append suggestions to any line, but also to show + multi-line suggestions. + + As transformation are applied on a line-by-line basis; we need to trick + a bit, and elide any line that is after the line we are currently + editing, until we run out of completions. We cannot shift the existing + lines + + There are multiple cases to handle: + + The completions ends before the end of the buffer: + We can resume showing the normal line, and say that some code may + be hidden. + + The completions ends at the end of the buffer + We can just say that some code may be hidden. + + And separately: + + The completions ends beyond the end of the buffer + We need to both say that some code may be hidden, and that some + lines are not shown. + + """ + last_line_number = ti.document.line_count - 1 + is_last_line = ti.lineno == last_line_number + + noop = lambda text: Transformation( + fragments=ti.fragments + [(self.style, " " + text if self._debug else "")] + ) + if ti.document.line_count == 1: + return noop("noop:oneline") + if ti.document.cursor_position_row == last_line_number and is_last_line: + # prompt toolkit already appends something; just leave it be + return noop("noop:last line and cursor") + + # first everything before the current line is unchanged. + if ti.lineno < ti.document.cursor_position_row: + return noop("noop:before cursor") + + buffer = ti.buffer_control.buffer + if not buffer.suggestion or not ti.document.is_cursor_at_the_end_of_line: + return noop("noop:not eol") + + delta = ti.lineno - ti.document.cursor_position_row + suggestions = buffer.suggestion.text.splitlines() + + if len(suggestions) == 0: + return noop("noop: no suggestions") + + if prompt_toolkit.VERSION < (3, 0, 49): + if len(suggestions) > 1 and prompt_toolkit.VERSION < (3, 0, 49): + if ti.lineno == ti.document.cursor_position_row: + return Transformation( + fragments=ti.fragments + + [ + ( + "red", + "(Cannot show multiline suggestion; requires prompt_toolkit > 3.0.49)", + ) + ] + ) + else: + return Transformation(fragments=ti.fragments) + elif len(suggestions) == 1: + if ti.lineno == ti.document.cursor_position_row: + return Transformation( + fragments=ti.fragments + [(self.style, suggestions[0])] + ) + return Transformation(fragments=ti.fragments) + + if delta == 0: + suggestion = suggestions[0] + return Transformation(fragments=ti.fragments + [(self.style, suggestion)]) + if is_last_line: + if delta < len(suggestions): + suggestion = f"… rest of suggestion ({len(suggestions) - delta} lines) and code hidden" + return Transformation([(self.style, suggestion)]) + + n_elided = len(suggestions) + for i in range(len(suggestions)): + ll = ti.get_line(last_line_number - i) + el = "".join(l[1] for l in ll).strip() + if el: + break + else: + n_elided -= 1 + if n_elided: + return Transformation([(self.style, f"… {n_elided} line(s) hidden")]) + else: + return Transformation( + ti.get_line(last_line_number - len(suggestions) + 1) + + ([(self.style, "shift-last-line")] if self._debug else []) + ) + + elif delta < len(suggestions): + suggestion = suggestions[delta] + return Transformation([(self.style, suggestion)]) + else: + shift = ti.lineno - len(suggestions) + 1 + return Transformation(ti.get_line(shift)) + + +class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory): + """ + A subclass of AutoSuggestFromHistory that allow navigation to next/previous + suggestion from history. To do so it remembers the current position, but it + state need to carefully be cleared on the right events. + """ + + skip_lines: int + _connected_apps: list[PromptSession] + + # handle to the currently running llm task that appends suggestions to the + # current buffer; we keep a handle to it in order to cancel it when there is a cursor movement, or + # another request. + _llm_task: asyncio.Task | None = None + + # This is the constructor of the LLM provider from jupyter-ai + # to which we forward the request to generate inline completions. + _init_llm_provider: Callable | None + + _llm_provider_instance: Any | None + _llm_prefixer: Callable = lambda self, x: "wrong" + + def __init__(self): + super().__init__() + self.skip_lines = 0 + self._connected_apps = [] + self._llm_provider_instance = None + self._init_llm_provider = None + self._request_number = 0 + + def reset_history_position(self, _: Buffer) -> None: + self.skip_lines = 0 + + def disconnect(self) -> None: + self._cancel_running_llm_task() + for pt_app in self._connected_apps: + text_insert_event = pt_app.default_buffer.on_text_insert + text_insert_event.remove_handler(self.reset_history_position) + + def connect(self, pt_app: PromptSession) -> None: + self._connected_apps.append(pt_app) + # note: `on_text_changed` could be used for a bit different behaviour + # on character deletion (i.e. resetting history position on backspace) + pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position) + pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss) + + def get_suggestion( + self, buffer: Buffer, document: Document + ) -> Optional[Suggestion]: + text = _get_query(document) + + if text.strip(): + for suggestion, _ in self._find_next_match( + text, self.skip_lines, buffer.history + ): + return Suggestion(suggestion) + + return None + + def _dismiss(self, buffer, *args, **kwargs) -> None: + self._cancel_running_llm_task() + buffer.suggestion = None + + def _find_match( + self, text: str, skip_lines: float, history: History, previous: bool + ) -> Generator[Tuple[str, float], None, None]: + """ + text : str + Text content to find a match for, the user cursor is most of the + time at the end of this text. + skip_lines : float + number of items to skip in the search, this is used to indicate how + far in the list the user has navigated by pressing up or down. + The float type is used as the base value is +inf + history : History + prompt_toolkit History instance to fetch previous entries from. + previous : bool + Direction of the search, whether we are looking previous match + (True), or next match (False). + + Yields + ------ + Tuple with: + str: + current suggestion. + float: + will actually yield only ints, which is passed back via skip_lines, + which may be a +inf (float) + + + """ + line_number = -1 + for string in reversed(list(history.get_strings())): + for line in reversed(string.splitlines()): + line_number += 1 + if not previous and line_number < skip_lines: + continue + # do not return empty suggestions as these + # close the auto-suggestion overlay (and are useless) + if line.startswith(text) and len(line) > len(text): + yield line[len(text) :], line_number + if previous and line_number >= skip_lines: + return + + def _find_next_match( + self, text: str, skip_lines: float, history: History + ) -> Generator[Tuple[str, float], None, None]: + return self._find_match(text, skip_lines, history, previous=False) + + def _find_previous_match(self, text: str, skip_lines: float, history: History): + return reversed( + list(self._find_match(text, skip_lines, history, previous=True)) + ) + + def up(self, query: str, other_than: str, history: History) -> None: + self._cancel_running_llm_task() + for suggestion, line_number in self._find_next_match( + query, self.skip_lines, history + ): + # if user has history ['very.a', 'very', 'very.b'] and typed 'very' + # we want to switch from 'very.b' to 'very.a' because a) if the + # suggestion equals current text, prompt-toolkit aborts suggesting + # b) user likely would not be interested in 'very' anyways (they + # already typed it). + if query + suggestion != other_than: + self.skip_lines = line_number + break + else: + # no matches found, cycle back to beginning + self.skip_lines = 0 + + def down(self, query: str, other_than: str, history: History) -> None: + self._cancel_running_llm_task() + for suggestion, line_number in self._find_previous_match( + query, self.skip_lines, history + ): + if query + suggestion != other_than: + self.skip_lines = line_number + break + else: + # no matches found, cycle to end + for suggestion, line_number in self._find_previous_match( + query, float("Inf"), history + ): + if query + suggestion != other_than: + self.skip_lines = line_number + break + + def _cancel_running_llm_task(self) -> None: + """ + Try to cancel the currently running llm_task if exists, and set it to None. + """ + if self._llm_task is not None: + if self._llm_task.done(): + self._llm_task = None + return + cancelled = self._llm_task.cancel() + if cancelled: + self._llm_task = None + if not cancelled: + warnings.warn( + "LLM task not cancelled, does your provider support cancellation?" + ) + + @property + def _llm_provider(self): + """Lazy-initialized instance of the LLM provider. + + Do not use in the constructor, as `_init_llm_provider` can trigger slow side-effects. + """ + if self._llm_provider_instance is None and self._init_llm_provider: + self._llm_provider_instance = self._init_llm_provider() + return self._llm_provider_instance + + async def _trigger_llm(self, buffer) -> None: + """ + This will ask the current llm provider a suggestion for the current buffer. + + If there is a currently running llm task, it will cancel it. + """ + # we likely want to store the current cursor position, and cancel if the cursor has moved. + try: + import jupyter_ai_magics + except ModuleNotFoundError: + jupyter_ai_magics = None + if not self._llm_provider: + warnings.warn("No LLM provider found, cannot trigger LLM completions") + return + if jupyter_ai_magics is None: + warnings.warn("LLM Completion requires `jupyter_ai_magics` to be installed") + + self._cancel_running_llm_task() + + async def error_catcher(buffer): + """ + This catches and log any errors, as otherwise this is just + lost in the void of the future running task. + """ + try: + await self._trigger_llm_core(buffer) + except Exception as e: + get_ipython().log.error("error %s", e) + raise + + # here we need a cancellable task so we can't just await the error caught + self._llm_task = asyncio.create_task(error_catcher(buffer)) + await self._llm_task + + async def _trigger_llm_core(self, buffer: Buffer): + """ + This is the core of the current llm request. + + Here we build a compatible `InlineCompletionRequest` and ask the llm + provider to stream it's response back to us iteratively setting it as + the suggestion on the current buffer. + + Unlike with JupyterAi, as we do not have multiple cells, the cell id + is always set to `None`. + + We set the prefix to the current cell content, but could also insert the + rest of the history or even just the non-fail history. + + In the same way, we do not have cell id. + + LLM provider may return multiple suggestion stream, but for the time + being we only support one. + + Here we make the assumption that the provider will have + stream_inline_completions, I'm not sure it is the case for all + providers. + """ + try: + import jupyter_ai.completions.models as jai_models + except ModuleNotFoundError: + jai_models = None + + if not jai_models: + raise ValueError("jupyter-ai is not installed") + + if not self._llm_provider: + raise ValueError("No LLM provider found, cannot trigger LLM completions") + + hm = buffer.history.shell.history_manager + prefix = self._llm_prefixer(hm) + get_ipython().log.debug("prefix: %s", prefix) + + self._request_number += 1 + request_number = self._request_number + + request = jai_models.InlineCompletionRequest( + number=request_number, + prefix=prefix + buffer.document.text_before_cursor, + suffix=buffer.document.text_after_cursor, + mime="text/x-python", + stream=True, + path=None, + language="python", + cell_id=None, + ) + + async for reply_and_chunks in self._llm_provider.stream_inline_completions( + request + ): + if self._request_number != request_number: + # If a new suggestion was requested, skip processing this one. + return + if isinstance(reply_and_chunks, jai_models.InlineCompletionReply): + if len(reply_and_chunks.list.items) > 1: + raise ValueError( + "Terminal IPython cannot deal with multiple LLM suggestions at once" + ) + buffer.suggestion = Suggestion( + reply_and_chunks.list.items[0].insertText + ) + buffer.on_suggestion_set.fire() + elif isinstance(reply_and_chunks, jai_models.InlineCompletionStreamChunk): + buffer.suggestion = Suggestion(reply_and_chunks.response.insertText) + buffer.on_suggestion_set.fire() + return + + +async def llm_autosuggestion(event: KeyPressEvent): + """ + Ask the AutoSuggester from history to delegate to ask an LLM for completion + + This will first make sure that the current buffer have _MIN_LINES (7) + available lines to insert the LLM completion + + Provisional as of 8.32, may change without warnings + + """ + _MIN_LINES = 5 + provider = get_ipython().auto_suggest + if not isinstance(provider, NavigableAutoSuggestFromHistory): + return + doc = event.current_buffer.document + lines_to_insert = max(0, _MIN_LINES - doc.line_count + doc.cursor_position_row) + for _ in range(lines_to_insert): + event.current_buffer.insert_text("\n", move_cursor=False, fire_event=False) + + await provider._trigger_llm(event.current_buffer) + + +def accept_or_jump_to_end(event: KeyPressEvent): + """Apply autosuggestion or jump to end of line.""" + buffer = event.current_buffer + d = buffer.document + after_cursor = d.text[d.cursor_position :] + lines = after_cursor.split("\n") + end_of_current_line = lines[0].strip() + suggestion = buffer.suggestion + if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): + buffer.insert_text(suggestion.text) + else: + nc.end_of_line(event) + + +def accept(event: KeyPressEvent): + """Accept autosuggestion""" + buffer = event.current_buffer + suggestion = buffer.suggestion + if suggestion: + buffer.insert_text(suggestion.text) + else: + nc.forward_char(event) + + +def discard(event: KeyPressEvent): + """Discard autosuggestion""" + buffer = event.current_buffer + buffer.suggestion = None + + +def accept_word(event: KeyPressEvent): + """Fill partial autosuggestion by word""" + buffer = event.current_buffer + suggestion = buffer.suggestion + if suggestion: + t = re.split(r"(\S+\s+)", suggestion.text) + buffer.insert_text(next((x for x in t if x), "")) + else: + nc.forward_word(event) + + +def accept_character(event: KeyPressEvent): + """Fill partial autosuggestion by character""" + b = event.current_buffer + suggestion = b.suggestion + if suggestion and suggestion.text: + b.insert_text(suggestion.text[0]) + + +def accept_and_keep_cursor(event: KeyPressEvent): + """Accept autosuggestion and keep cursor in place""" + buffer = event.current_buffer + old_position = buffer.cursor_position + suggestion = buffer.suggestion + if suggestion: + buffer.insert_text(suggestion.text) + buffer.cursor_position = old_position + + +def accept_and_move_cursor_left(event: KeyPressEvent): + """Accept autosuggestion and move cursor left in place""" + accept_and_keep_cursor(event) + nc.backward_char(event) + + +def _update_hint(buffer: Buffer): + if buffer.auto_suggest: + suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document) + buffer.suggestion = suggestion + + +def backspace_and_resume_hint(event: KeyPressEvent): + """Resume autosuggestions after deleting last character""" + nc.backward_delete_char(event) + _update_hint(event.current_buffer) + + +def resume_hinting(event: KeyPressEvent): + """Resume autosuggestions""" + pass_through.reply(event) + # Order matters: if update happened first and event reply second, the + # suggestion would be auto-accepted if both actions are bound to same key. + _update_hint(event.current_buffer) + + +def up_and_update_hint(event: KeyPressEvent): + """Go up and update hint""" + current_buffer = event.current_buffer + + current_buffer.auto_up(count=event.arg) + _update_hint(current_buffer) + + +def down_and_update_hint(event: KeyPressEvent): + """Go down and update hint""" + current_buffer = event.current_buffer + + current_buffer.auto_down(count=event.arg) + _update_hint(current_buffer) + + +def accept_token(event: KeyPressEvent): + """Fill partial autosuggestion by token""" + b = event.current_buffer + suggestion = b.suggestion + + if suggestion: + prefix = _get_query(b.document) + text = prefix + suggestion.text + + tokens: List[Optional[str]] = [None, None, None] + substrings = [""] + i = 0 + + for token in generate_tokens(StringIO(text).readline): + if token.type == tokenize.NEWLINE: + index = len(text) + else: + index = text.index(token[1], len(substrings[-1])) + substrings.append(text[:index]) + tokenized_so_far = substrings[-1] + if tokenized_so_far.startswith(prefix): + if i == 0 and len(tokenized_so_far) > len(prefix): + tokens[0] = tokenized_so_far[len(prefix) :] + substrings.append(tokenized_so_far) + i += 1 + tokens[i] = token[1] + if i == 2: + break + i += 1 + + if tokens[0]: + to_insert: str + insert_text = substrings[-2] + if tokens[1] and len(tokens[1]) == 1: + insert_text = substrings[-1] + to_insert = insert_text[len(prefix) :] + b.insert_text(to_insert) + return + + nc.forward_word(event) + + +Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None] + + +def _swap_autosuggestion( + buffer: Buffer, + provider: NavigableAutoSuggestFromHistory, + direction_method: Callable, +): + """ + We skip most recent history entry (in either direction) if it equals the + current autosuggestion because if user cycles when auto-suggestion is shown + they most likely want something else than what was suggested (otherwise + they would have accepted the suggestion). + """ + suggestion = buffer.suggestion + if not suggestion: + return + + query = _get_query(buffer.document) + current = query + suggestion.text + + direction_method(query=query, other_than=current, history=buffer.history) + + new_suggestion = provider.get_suggestion(buffer, buffer.document) + buffer.suggestion = new_suggestion + + +def swap_autosuggestion_up(event: KeyPressEvent): + """Get next autosuggestion from history.""" + shell = get_ipython() + provider = shell.auto_suggest + + if not isinstance(provider, NavigableAutoSuggestFromHistory): + return + + return _swap_autosuggestion( + buffer=event.current_buffer, provider=provider, direction_method=provider.up + ) + + +def swap_autosuggestion_down(event: KeyPressEvent): + """Get previous autosuggestion from history.""" + shell = get_ipython() + provider = shell.auto_suggest + + if not isinstance(provider, NavigableAutoSuggestFromHistory): + return + + return _swap_autosuggestion( + buffer=event.current_buffer, + provider=provider, + direction_method=provider.down, + ) diff --git a/lib/python3.12/site-packages/attrs/__init__.py b/lib/python3.12/site-packages/attrs/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..dc1ce4b974ea1b3971ced44e3e112e897cf29b0a --- /dev/null +++ b/lib/python3.12/site-packages/attrs/__init__.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MIT + +from attr import ( + NOTHING, + Attribute, + AttrsInstance, + Converter, + Factory, + NothingType, + _make_getattr, + assoc, + cmp_using, + define, + evolve, + field, + fields, + fields_dict, + frozen, + has, + make_class, + mutable, + resolve_types, + validate, +) +from attr._make import ClassProps +from attr._next_gen import asdict, astuple, inspect + +from . import converters, exceptions, filters, setters, validators + + +__all__ = [ + "NOTHING", + "Attribute", + "AttrsInstance", + "ClassProps", + "Converter", + "Factory", + "NothingType", + "__author__", + "__copyright__", + "__description__", + "__doc__", + "__email__", + "__license__", + "__title__", + "__url__", + "__version__", + "__version_info__", + "asdict", + "assoc", + "astuple", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "field", + "fields", + "fields_dict", + "filters", + "frozen", + "has", + "inspect", + "make_class", + "mutable", + "resolve_types", + "setters", + "validate", + "validators", +] + +__getattr__ = _make_getattr(__name__) diff --git a/lib/python3.12/site-packages/attrs/__init__.pyi b/lib/python3.12/site-packages/attrs/__init__.pyi new file mode 100644 index 0000000000000000000000000000000000000000..6364bac4ea29f860934803e5f3ba741812c6dfd1 --- /dev/null +++ b/lib/python3.12/site-packages/attrs/__init__.pyi @@ -0,0 +1,314 @@ +import sys + +from typing import ( + Any, + Callable, + Mapping, + Sequence, + overload, + TypeVar, +) + +# Because we need to type our own stuff, we have to make everything from +# attr explicitly public too. +from attr import __author__ as __author__ +from attr import __copyright__ as __copyright__ +from attr import __description__ as __description__ +from attr import __email__ as __email__ +from attr import __license__ as __license__ +from attr import __title__ as __title__ +from attr import __url__ as __url__ +from attr import __version__ as __version__ +from attr import __version_info__ as __version_info__ +from attr import assoc as assoc +from attr import Attribute as Attribute +from attr import AttrsInstance as AttrsInstance +from attr import cmp_using as cmp_using +from attr import converters as converters +from attr import Converter as Converter +from attr import evolve as evolve +from attr import exceptions as exceptions +from attr import Factory as Factory +from attr import fields as fields +from attr import fields_dict as fields_dict +from attr import filters as filters +from attr import has as has +from attr import make_class as make_class +from attr import NOTHING as NOTHING +from attr import resolve_types as resolve_types +from attr import setters as setters +from attr import validate as validate +from attr import validators as validators +from attr import attrib, asdict as asdict, astuple as astuple +from attr import NothingType as NothingType + +if sys.version_info >= (3, 11): + from typing import dataclass_transform +else: + from typing_extensions import dataclass_transform + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) + +_EqOrderType = bool | Callable[[Any], Any] +_ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any] +_CallableConverterType = Callable[[Any], Any] +_ConverterType = _CallableConverterType | Converter[Any, Any] +_ReprType = Callable[[Any], str] +_ReprArgType = bool | _ReprType +_OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any] +_OnSetAttrArgType = _OnSetAttrType | list[_OnSetAttrType] | setters._NoOpType +_FieldTransformer = Callable[ + [type, list["Attribute[Any]"]], list["Attribute[Any]"] +] +# FIXME: in reality, if multiple validators are passed they must be in a list +# or tuple, but those are invariant and so would prevent subtypes of +# _ValidatorType from working when passed in a list or tuple. +_ValidatorArgType = _ValidatorType[_T] | Sequence[_ValidatorType[_T]] + +@overload +def field( + *, + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool | None = ..., + eq: bool | None = ..., + order: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def field( + *, + default: None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType, ...] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def field( + *, + default: _T, + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType, ...] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def field( + *, + default: _T | None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType, ...] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> Any: ... +@overload +@dataclass_transform(field_specifiers=(attrib, field)) +def define( + maybe_cls: _C, + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@dataclass_transform(field_specifiers=(attrib, field)) +def define( + maybe_cls: None = ..., + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... + +mutable = define + +@overload +@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) +def frozen( + maybe_cls: _C, + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) +def frozen( + maybe_cls: None = ..., + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... + +class ClassProps: + # XXX: somehow when defining/using enums Mypy starts looking at our own + # (untyped) code and causes tons of errors. + Hashability: Any + KeywordOnly: Any + + is_exception: bool + is_slotted: bool + has_weakref_slot: bool + is_frozen: bool + # kw_only: ClassProps.KeywordOnly + kw_only: Any + collected_fields_by_mro: bool + added_init: bool + added_repr: bool + added_eq: bool + added_ordering: bool + # hashability: ClassProps.Hashability + hashability: Any + added_match_args: bool + added_str: bool + added_pickling: bool + on_setattr_hook: _OnSetAttrType | None + field_transformer: Callable[[Attribute[Any]], Attribute[Any]] | None + + def __init__( + self, + is_exception: bool, + is_slotted: bool, + has_weakref_slot: bool, + is_frozen: bool, + # kw_only: ClassProps.KeywordOnly + kw_only: Any, + collected_fields_by_mro: bool, + added_init: bool, + added_repr: bool, + added_eq: bool, + added_ordering: bool, + # hashability: ClassProps.Hashability + hashability: Any, + added_match_args: bool, + added_str: bool, + added_pickling: bool, + on_setattr_hook: _OnSetAttrType, + field_transformer: Callable[[Attribute[Any]], Attribute[Any]], + ) -> None: ... + @property + def is_hashable(self) -> bool: ... + +def inspect(cls: type) -> ClassProps: ... diff --git a/lib/python3.12/site-packages/attrs/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/attrs/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22fbd4f3eb590938c06c63ba54479b04feeb90b8 Binary files /dev/null and b/lib/python3.12/site-packages/attrs/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attrs/__pycache__/converters.cpython-312.pyc b/lib/python3.12/site-packages/attrs/__pycache__/converters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cce11d9b17644b359a6c712f34f370317bfc7a5 Binary files /dev/null and b/lib/python3.12/site-packages/attrs/__pycache__/converters.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attrs/__pycache__/exceptions.cpython-312.pyc b/lib/python3.12/site-packages/attrs/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e5d1faada170dad0648401191b2d0c8802d0e7c Binary files /dev/null and b/lib/python3.12/site-packages/attrs/__pycache__/exceptions.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attrs/__pycache__/filters.cpython-312.pyc b/lib/python3.12/site-packages/attrs/__pycache__/filters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4166aeff22f595ca03357538348dbd882a2fbac Binary files /dev/null and b/lib/python3.12/site-packages/attrs/__pycache__/filters.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attrs/__pycache__/setters.cpython-312.pyc b/lib/python3.12/site-packages/attrs/__pycache__/setters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bae5484ef5770e8d798c2f8d20aa9773785edc99 Binary files /dev/null and b/lib/python3.12/site-packages/attrs/__pycache__/setters.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attrs/__pycache__/validators.cpython-312.pyc b/lib/python3.12/site-packages/attrs/__pycache__/validators.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e9e9d4899dc3c9b33fd9fe4846b8dd6f66bb95d Binary files /dev/null and b/lib/python3.12/site-packages/attrs/__pycache__/validators.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/attrs/converters.py b/lib/python3.12/site-packages/attrs/converters.py new file mode 100644 index 0000000000000000000000000000000000000000..7821f6c02cca81277d1ecc87b6bdafad886d8b70 --- /dev/null +++ b/lib/python3.12/site-packages/attrs/converters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.converters import * # noqa: F403 diff --git a/lib/python3.12/site-packages/attrs/exceptions.py b/lib/python3.12/site-packages/attrs/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..3323f9d2112c54b203763d45b455bd5abbe020f6 --- /dev/null +++ b/lib/python3.12/site-packages/attrs/exceptions.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.exceptions import * # noqa: F403 diff --git a/lib/python3.12/site-packages/attrs/filters.py b/lib/python3.12/site-packages/attrs/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..3080f48398e5ed8d3428ca3efeb7500633b0cb0f --- /dev/null +++ b/lib/python3.12/site-packages/attrs/filters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.filters import * # noqa: F403 diff --git a/lib/python3.12/site-packages/attrs/py.typed b/lib/python3.12/site-packages/attrs/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/lib/python3.12/site-packages/attrs/setters.py b/lib/python3.12/site-packages/attrs/setters.py new file mode 100644 index 0000000000000000000000000000000000000000..f3d73bb793dd49c138950961f41943bb26c57fde --- /dev/null +++ b/lib/python3.12/site-packages/attrs/setters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.setters import * # noqa: F403 diff --git a/lib/python3.12/site-packages/attrs/validators.py b/lib/python3.12/site-packages/attrs/validators.py new file mode 100644 index 0000000000000000000000000000000000000000..037e124f29f32d37c1642d159bf828de44f7c349 --- /dev/null +++ b/lib/python3.12/site-packages/attrs/validators.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.validators import * # noqa: F403 diff --git a/lib/python3.12/site-packages/torchaudio/__init__.py b/lib/python3.12/site-packages/torchaudio/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..90b411eae1f27ff5dc8adf4917c1ea8725cd2c9a --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/__init__.py @@ -0,0 +1,53 @@ +# Initialize extension and backend first +from . import _extension # noqa # usort: skip +from ._backend import ( # noqa # usort: skip + AudioMetaData, + get_audio_backend, + info, + list_audio_backends, + load, + save, + set_audio_backend, +) + +from . import ( # noqa: F401 + compliance, + datasets, + functional, + io, + kaldi_io, + models, + pipelines, + sox_effects, + transforms, + utils, +) + +# For BC +from . import backend # noqa # usort: skip + +try: + from .version import __version__, git_version # noqa: F401 +except ImportError: + pass + + +__all__ = [ + "AudioMetaData", + "load", + "info", + "save", + "io", + "compliance", + "datasets", + "functional", + "models", + "pipelines", + "kaldi_io", + "utils", + "sox_effects", + "transforms", + "list_audio_backends", + "get_audio_backend", + "set_audio_backend", +] diff --git a/lib/python3.12/site-packages/torchaudio/_internal/__init__.py b/lib/python3.12/site-packages/torchaudio/_internal/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..363e94f13bb5059ab6888af2fb60314699f1ab1e --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/_internal/__init__.py @@ -0,0 +1,10 @@ +try: + from .fb import download_url_to_file, load_state_dict_from_url +except ImportError: + from torch.hub import download_url_to_file, load_state_dict_from_url + + +__all__ = [ + "load_state_dict_from_url", + "download_url_to_file", +] diff --git a/lib/python3.12/site-packages/torchaudio/_internal/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/_internal/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3513c096eb2062bc0671a8aa62526601f273eead Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/_internal/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/_internal/__pycache__/module_utils.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/_internal/__pycache__/module_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..308b058efe881cfd2687a0427c9bfa2454b80110 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/_internal/__pycache__/module_utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/_internal/module_utils.py b/lib/python3.12/site-packages/torchaudio/_internal/module_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..bc484104bfd86460c57b2f773146ea1a14e47984 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/_internal/module_utils.py @@ -0,0 +1,113 @@ +import importlib.util +import os +import warnings +from functools import wraps +from typing import Optional + + +def eval_env(var, default): + """Check if environment varable has True-y value""" + if var not in os.environ: + return default + + val = os.environ.get(var, "0") + trues = ["1", "true", "TRUE", "on", "ON", "yes", "YES"] + falses = ["0", "false", "FALSE", "off", "OFF", "no", "NO"] + if val in trues: + return True + if val not in falses: + # fmt: off + raise RuntimeError( + f"Unexpected environment variable value `{var}={val}`. " + f"Expected one of {trues + falses}") + # fmt: on + return False + + +def is_module_available(*modules: str) -> bool: + r"""Returns if a top-level module with :attr:`name` exists *without** + importing it. This is generally safer than try-catch block around a + `import X`. It avoids third party libraries breaking assumptions of some of + our tests, e.g., setting multiprocessing start method when imported + (see librosa/#747, torchvision/#544). + """ + return all(importlib.util.find_spec(m) is not None for m in modules) + + +def requires_module(*modules: str): + """Decorate function to give error message if invoked without required optional modules. + + This decorator is to give better error message to users rather + than raising ``NameError: name 'module' is not defined`` at random places. + """ + missing = [m for m in modules if not is_module_available(m)] + + if not missing: + # fall through. If all the modules are available, no need to decorate + def decorator(func): + return func + + else: + req = f"module: {missing[0]}" if len(missing) == 1 else f"modules: {missing}" + + def decorator(func): + @wraps(func) + def wrapped(*args, **kwargs): + raise RuntimeError(f"{func.__module__}.{func.__name__} requires {req}") + + return wrapped + + return decorator + + +def deprecated(direction: str, version: Optional[str] = None, remove: bool = False): + """Decorator to add deprecation message + + Args: + direction (str): Migration steps to be given to users. + version (str or int): The version when the object will be removed + remove (bool): If enabled, append future removal message. + """ + + def decorator(func): + @wraps(func) + def wrapped(*args, **kwargs): + message = f"{func.__module__}.{func.__name__} has been deprecated. {direction}" + if remove: + message += f' It will be removed from {"future" if version is None else version} release. ' + warnings.warn(message, stacklevel=2) + return func(*args, **kwargs) + + message = "This function has been deprecated. " + if remove: + message += f'It will be removed from {"future" if version is None else version} release. ' + + wrapped.__doc__ = f"""DEPRECATED: {func.__doc__} + + .. warning:: + + {message} + {direction} + """ + + return wrapped + + return decorator + + +def fail_with_message(message): + """Generate decorator to give users message about missing TorchAudio extension.""" + + def decorator(func): + @wraps(func) + def wrapped(*args, **kwargs): + raise RuntimeError(f"{func.__module__}.{func.__name__} {message}") + + return wrapped + + return decorator + + +def no_op(func): + """Op-op decorator. Used in place of fail_with_message when a functionality that requires extension works fine.""" + return func diff --git a/lib/python3.12/site-packages/torchaudio/compliance/__init__.py b/lib/python3.12/site-packages/torchaudio/compliance/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..65579b4f01ba09695860717f1e6cd90d6e42b631 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/compliance/__init__.py @@ -0,0 +1,5 @@ +from . import kaldi + +__all__ = [ + "kaldi", +] diff --git a/lib/python3.12/site-packages/torchaudio/compliance/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/compliance/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79efddcb7fce3a4a21bd00e12ab9cd06154993db Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/compliance/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/compliance/__pycache__/kaldi.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/compliance/__pycache__/kaldi.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..41b43d34dc8ed71b6edf7b0c2e8627bb80d3613c Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/compliance/__pycache__/kaldi.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/compliance/kaldi.py b/lib/python3.12/site-packages/torchaudio/compliance/kaldi.py new file mode 100644 index 0000000000000000000000000000000000000000..98358f40b522facc0abdfbaceec45f5887e00e54 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/compliance/kaldi.py @@ -0,0 +1,813 @@ +import math +from typing import Tuple + +import torch +import torchaudio +from torch import Tensor + +__all__ = [ + "get_mel_banks", + "inverse_mel_scale", + "inverse_mel_scale_scalar", + "mel_scale", + "mel_scale_scalar", + "spectrogram", + "fbank", + "mfcc", + "vtln_warp_freq", + "vtln_warp_mel_freq", +] + +# numeric_limits::epsilon() 1.1920928955078125e-07 +EPSILON = torch.tensor(torch.finfo(torch.float).eps) +# 1 milliseconds = 0.001 seconds +MILLISECONDS_TO_SECONDS = 0.001 + +# window types +HAMMING = "hamming" +HANNING = "hanning" +POVEY = "povey" +RECTANGULAR = "rectangular" +BLACKMAN = "blackman" +WINDOWS = [HAMMING, HANNING, POVEY, RECTANGULAR, BLACKMAN] + + +def _get_epsilon(device, dtype): + return EPSILON.to(device=device, dtype=dtype) + + +def _next_power_of_2(x: int) -> int: + r"""Returns the smallest power of 2 that is greater than x""" + return 1 if x == 0 else 2 ** (x - 1).bit_length() + + +def _get_strided(waveform: Tensor, window_size: int, window_shift: int, snip_edges: bool) -> Tensor: + r"""Given a waveform (1D tensor of size ``num_samples``), it returns a 2D tensor (m, ``window_size``) + representing how the window is shifted along the waveform. Each row is a frame. + + Args: + waveform (Tensor): Tensor of size ``num_samples`` + window_size (int): Frame length + window_shift (int): Frame shift + snip_edges (bool): If True, end effects will be handled by outputting only frames that completely fit + in the file, and the number of frames depends on the frame_length. If False, the number of frames + depends only on the frame_shift, and we reflect the data at the ends. + + Returns: + Tensor: 2D tensor of size (m, ``window_size``) where each row is a frame + """ + assert waveform.dim() == 1 + num_samples = waveform.size(0) + strides = (window_shift * waveform.stride(0), waveform.stride(0)) + + if snip_edges: + if num_samples < window_size: + return torch.empty((0, 0), dtype=waveform.dtype, device=waveform.device) + else: + m = 1 + (num_samples - window_size) // window_shift + else: + reversed_waveform = torch.flip(waveform, [0]) + m = (num_samples + (window_shift // 2)) // window_shift + pad = window_size // 2 - window_shift // 2 + pad_right = reversed_waveform + if pad > 0: + # torch.nn.functional.pad returns [2,1,0,1,2] for 'reflect' + # but we want [2, 1, 0, 0, 1, 2] + pad_left = reversed_waveform[-pad:] + waveform = torch.cat((pad_left, waveform, pad_right), dim=0) + else: + # pad is negative so we want to trim the waveform at the front + waveform = torch.cat((waveform[-pad:], pad_right), dim=0) + + sizes = (m, window_size) + return waveform.as_strided(sizes, strides) + + +def _feature_window_function( + window_type: str, + window_size: int, + blackman_coeff: float, + device: torch.device, + dtype: int, +) -> Tensor: + r"""Returns a window function with the given type and size""" + if window_type == HANNING: + return torch.hann_window(window_size, periodic=False, device=device, dtype=dtype) + elif window_type == HAMMING: + return torch.hamming_window(window_size, periodic=False, alpha=0.54, beta=0.46, device=device, dtype=dtype) + elif window_type == POVEY: + # like hanning but goes to zero at edges + return torch.hann_window(window_size, periodic=False, device=device, dtype=dtype).pow(0.85) + elif window_type == RECTANGULAR: + return torch.ones(window_size, device=device, dtype=dtype) + elif window_type == BLACKMAN: + a = 2 * math.pi / (window_size - 1) + window_function = torch.arange(window_size, device=device, dtype=dtype) + # can't use torch.blackman_window as they use different coefficients + return ( + blackman_coeff + - 0.5 * torch.cos(a * window_function) + + (0.5 - blackman_coeff) * torch.cos(2 * a * window_function) + ).to(device=device, dtype=dtype) + else: + raise Exception("Invalid window type " + window_type) + + +def _get_log_energy(strided_input: Tensor, epsilon: Tensor, energy_floor: float) -> Tensor: + r"""Returns the log energy of size (m) for a strided_input (m,*)""" + device, dtype = strided_input.device, strided_input.dtype + log_energy = torch.max(strided_input.pow(2).sum(1), epsilon).log() # size (m) + if energy_floor == 0.0: + return log_energy + return torch.max(log_energy, torch.tensor(math.log(energy_floor), device=device, dtype=dtype)) + + +def _get_waveform_and_window_properties( + waveform: Tensor, + channel: int, + sample_frequency: float, + frame_shift: float, + frame_length: float, + round_to_power_of_two: bool, + preemphasis_coefficient: float, +) -> Tuple[Tensor, int, int, int]: + r"""Gets the waveform and window properties""" + channel = max(channel, 0) + assert channel < waveform.size(0), "Invalid channel {} for size {}".format(channel, waveform.size(0)) + waveform = waveform[channel, :] # size (n) + window_shift = int(sample_frequency * frame_shift * MILLISECONDS_TO_SECONDS) + window_size = int(sample_frequency * frame_length * MILLISECONDS_TO_SECONDS) + padded_window_size = _next_power_of_2(window_size) if round_to_power_of_two else window_size + + assert 2 <= window_size <= len(waveform), "choose a window size {} that is [2, {}]".format( + window_size, len(waveform) + ) + assert 0 < window_shift, "`window_shift` must be greater than 0" + assert padded_window_size % 2 == 0, ( + "the padded `window_size` must be divisible by two." " use `round_to_power_of_two` or change `frame_length`" + ) + assert 0.0 <= preemphasis_coefficient <= 1.0, "`preemphasis_coefficient` must be between [0,1]" + assert sample_frequency > 0, "`sample_frequency` must be greater than zero" + return waveform, window_shift, window_size, padded_window_size + + +def _get_window( + waveform: Tensor, + padded_window_size: int, + window_size: int, + window_shift: int, + window_type: str, + blackman_coeff: float, + snip_edges: bool, + raw_energy: bool, + energy_floor: float, + dither: float, + remove_dc_offset: bool, + preemphasis_coefficient: float, +) -> Tuple[Tensor, Tensor]: + r"""Gets a window and its log energy + + Returns: + (Tensor, Tensor): strided_input of size (m, ``padded_window_size``) and signal_log_energy of size (m) + """ + device, dtype = waveform.device, waveform.dtype + epsilon = _get_epsilon(device, dtype) + + # size (m, window_size) + strided_input = _get_strided(waveform, window_size, window_shift, snip_edges) + + if dither != 0.0: + rand_gauss = torch.randn(strided_input.shape, device=device, dtype=dtype) + strided_input = strided_input + rand_gauss * dither + + if remove_dc_offset: + # Subtract each row/frame by its mean + row_means = torch.mean(strided_input, dim=1).unsqueeze(1) # size (m, 1) + strided_input = strided_input - row_means + + if raw_energy: + # Compute the log energy of each row/frame before applying preemphasis and + # window function + signal_log_energy = _get_log_energy(strided_input, epsilon, energy_floor) # size (m) + + if preemphasis_coefficient != 0.0: + # strided_input[i,j] -= preemphasis_coefficient * strided_input[i, max(0, j-1)] for all i,j + offset_strided_input = torch.nn.functional.pad(strided_input.unsqueeze(0), (1, 0), mode="replicate").squeeze( + 0 + ) # size (m, window_size + 1) + strided_input = strided_input - preemphasis_coefficient * offset_strided_input[:, :-1] + + # Apply window_function to each row/frame + window_function = _feature_window_function(window_type, window_size, blackman_coeff, device, dtype).unsqueeze( + 0 + ) # size (1, window_size) + strided_input = strided_input * window_function # size (m, window_size) + + # Pad columns with zero until we reach size (m, padded_window_size) + if padded_window_size != window_size: + padding_right = padded_window_size - window_size + strided_input = torch.nn.functional.pad( + strided_input.unsqueeze(0), (0, padding_right), mode="constant", value=0 + ).squeeze(0) + + # Compute energy after window function (not the raw one) + if not raw_energy: + signal_log_energy = _get_log_energy(strided_input, epsilon, energy_floor) # size (m) + + return strided_input, signal_log_energy + + +def _subtract_column_mean(tensor: Tensor, subtract_mean: bool) -> Tensor: + # subtracts the column mean of the tensor size (m, n) if subtract_mean=True + # it returns size (m, n) + if subtract_mean: + col_means = torch.mean(tensor, dim=0).unsqueeze(0) + tensor = tensor - col_means + return tensor + + +def spectrogram( + waveform: Tensor, + blackman_coeff: float = 0.42, + channel: int = -1, + dither: float = 0.0, + energy_floor: float = 1.0, + frame_length: float = 25.0, + frame_shift: float = 10.0, + min_duration: float = 0.0, + preemphasis_coefficient: float = 0.97, + raw_energy: bool = True, + remove_dc_offset: bool = True, + round_to_power_of_two: bool = True, + sample_frequency: float = 16000.0, + snip_edges: bool = True, + subtract_mean: bool = False, + window_type: str = POVEY, +) -> Tensor: + r"""Create a spectrogram from a raw audio signal. This matches the input/output of Kaldi's + compute-spectrogram-feats. + + Args: + waveform (Tensor): Tensor of audio of size (c, n) where c is in the range [0,2) + blackman_coeff (float, optional): Constant coefficient for generalized Blackman window. (Default: ``0.42``) + channel (int, optional): Channel to extract (-1 -> expect mono, 0 -> left, 1 -> right) (Default: ``-1``) + dither (float, optional): Dithering constant (0.0 means no dither). If you turn this off, you should set + the energy_floor option, e.g. to 1.0 or 0.1 (Default: ``0.0``) + energy_floor (float, optional): Floor on energy (absolute, not relative) in Spectrogram computation. Caution: + this floor is applied to the zeroth component, representing the total signal energy. The floor on the + individual spectrogram elements is fixed at std::numeric_limits::epsilon(). (Default: ``1.0``) + frame_length (float, optional): Frame length in milliseconds (Default: ``25.0``) + frame_shift (float, optional): Frame shift in milliseconds (Default: ``10.0``) + min_duration (float, optional): Minimum duration of segments to process (in seconds). (Default: ``0.0``) + preemphasis_coefficient (float, optional): Coefficient for use in signal preemphasis (Default: ``0.97``) + raw_energy (bool, optional): If True, compute energy before preemphasis and windowing (Default: ``True``) + remove_dc_offset (bool, optional): Subtract mean from waveform on each frame (Default: ``True``) + round_to_power_of_two (bool, optional): If True, round window size to power of two by zero-padding input + to FFT. (Default: ``True``) + sample_frequency (float, optional): Waveform data sample frequency (must match the waveform file, if + specified there) (Default: ``16000.0``) + snip_edges (bool, optional): If True, end effects will be handled by outputting only frames that completely fit + in the file, and the number of frames depends on the frame_length. If False, the number of frames + depends only on the frame_shift, and we reflect the data at the ends. (Default: ``True``) + subtract_mean (bool, optional): Subtract mean of each feature file [CMS]; not recommended to do + it this way. (Default: ``False``) + window_type (str, optional): Type of window ('hamming'|'hanning'|'povey'|'rectangular'|'blackman') + (Default: ``'povey'``) + + Returns: + Tensor: A spectrogram identical to what Kaldi would output. The shape is + (m, ``padded_window_size // 2 + 1``) where m is calculated in _get_strided + """ + device, dtype = waveform.device, waveform.dtype + epsilon = _get_epsilon(device, dtype) + + waveform, window_shift, window_size, padded_window_size = _get_waveform_and_window_properties( + waveform, channel, sample_frequency, frame_shift, frame_length, round_to_power_of_two, preemphasis_coefficient + ) + + if len(waveform) < min_duration * sample_frequency: + # signal is too short + return torch.empty(0) + + strided_input, signal_log_energy = _get_window( + waveform, + padded_window_size, + window_size, + window_shift, + window_type, + blackman_coeff, + snip_edges, + raw_energy, + energy_floor, + dither, + remove_dc_offset, + preemphasis_coefficient, + ) + + # size (m, padded_window_size // 2 + 1, 2) + fft = torch.fft.rfft(strided_input) + + # Convert the FFT into a power spectrum + power_spectrum = torch.max(fft.abs().pow(2.0), epsilon).log() # size (m, padded_window_size // 2 + 1) + power_spectrum[:, 0] = signal_log_energy + + power_spectrum = _subtract_column_mean(power_spectrum, subtract_mean) + return power_spectrum + + +def inverse_mel_scale_scalar(mel_freq: float) -> float: + return 700.0 * (math.exp(mel_freq / 1127.0) - 1.0) + + +def inverse_mel_scale(mel_freq: Tensor) -> Tensor: + return 700.0 * ((mel_freq / 1127.0).exp() - 1.0) + + +def mel_scale_scalar(freq: float) -> float: + return 1127.0 * math.log(1.0 + freq / 700.0) + + +def mel_scale(freq: Tensor) -> Tensor: + return 1127.0 * (1.0 + freq / 700.0).log() + + +def vtln_warp_freq( + vtln_low_cutoff: float, + vtln_high_cutoff: float, + low_freq: float, + high_freq: float, + vtln_warp_factor: float, + freq: Tensor, +) -> Tensor: + r"""This computes a VTLN warping function that is not the same as HTK's one, + but has similar inputs (this function has the advantage of never producing + empty bins). + + This function computes a warp function F(freq), defined between low_freq + and high_freq inclusive, with the following properties: + F(low_freq) == low_freq + F(high_freq) == high_freq + The function is continuous and piecewise linear with two inflection + points. + The lower inflection point (measured in terms of the unwarped + frequency) is at frequency l, determined as described below. + The higher inflection point is at a frequency h, determined as + described below. + If l <= f <= h, then F(f) = f/vtln_warp_factor. + If the higher inflection point (measured in terms of the unwarped + frequency) is at h, then max(h, F(h)) == vtln_high_cutoff. + Since (by the last point) F(h) == h/vtln_warp_factor, then + max(h, h/vtln_warp_factor) == vtln_high_cutoff, so + h = vtln_high_cutoff / max(1, 1/vtln_warp_factor). + = vtln_high_cutoff * min(1, vtln_warp_factor). + If the lower inflection point (measured in terms of the unwarped + frequency) is at l, then min(l, F(l)) == vtln_low_cutoff + This implies that l = vtln_low_cutoff / min(1, 1/vtln_warp_factor) + = vtln_low_cutoff * max(1, vtln_warp_factor) + Args: + vtln_low_cutoff (float): Lower frequency cutoffs for VTLN + vtln_high_cutoff (float): Upper frequency cutoffs for VTLN + low_freq (float): Lower frequency cutoffs in mel computation + high_freq (float): Upper frequency cutoffs in mel computation + vtln_warp_factor (float): Vtln warp factor + freq (Tensor): given frequency in Hz + + Returns: + Tensor: Freq after vtln warp + """ + assert vtln_low_cutoff > low_freq, "be sure to set the vtln_low option higher than low_freq" + assert vtln_high_cutoff < high_freq, "be sure to set the vtln_high option lower than high_freq [or negative]" + l = vtln_low_cutoff * max(1.0, vtln_warp_factor) + h = vtln_high_cutoff * min(1.0, vtln_warp_factor) + scale = 1.0 / vtln_warp_factor + Fl = scale * l # F(l) + Fh = scale * h # F(h) + assert l > low_freq and h < high_freq + # slope of left part of the 3-piece linear function + scale_left = (Fl - low_freq) / (l - low_freq) + # [slope of center part is just "scale"] + + # slope of right part of the 3-piece linear function + scale_right = (high_freq - Fh) / (high_freq - h) + + res = torch.empty_like(freq) + + outside_low_high_freq = torch.lt(freq, low_freq) | torch.gt(freq, high_freq) # freq < low_freq || freq > high_freq + before_l = torch.lt(freq, l) # freq < l + before_h = torch.lt(freq, h) # freq < h + after_h = torch.ge(freq, h) # freq >= h + + # order of operations matter here (since there is overlapping frequency regions) + res[after_h] = high_freq + scale_right * (freq[after_h] - high_freq) + res[before_h] = scale * freq[before_h] + res[before_l] = low_freq + scale_left * (freq[before_l] - low_freq) + res[outside_low_high_freq] = freq[outside_low_high_freq] + + return res + + +def vtln_warp_mel_freq( + vtln_low_cutoff: float, + vtln_high_cutoff: float, + low_freq, + high_freq: float, + vtln_warp_factor: float, + mel_freq: Tensor, +) -> Tensor: + r""" + Args: + vtln_low_cutoff (float): Lower frequency cutoffs for VTLN + vtln_high_cutoff (float): Upper frequency cutoffs for VTLN + low_freq (float): Lower frequency cutoffs in mel computation + high_freq (float): Upper frequency cutoffs in mel computation + vtln_warp_factor (float): Vtln warp factor + mel_freq (Tensor): Given frequency in Mel + + Returns: + Tensor: ``mel_freq`` after vtln warp + """ + return mel_scale( + vtln_warp_freq( + vtln_low_cutoff, vtln_high_cutoff, low_freq, high_freq, vtln_warp_factor, inverse_mel_scale(mel_freq) + ) + ) + + +def get_mel_banks( + num_bins: int, + window_length_padded: int, + sample_freq: float, + low_freq: float, + high_freq: float, + vtln_low: float, + vtln_high: float, + vtln_warp_factor: float, +) -> Tuple[Tensor, Tensor]: + """ + Returns: + (Tensor, Tensor): The tuple consists of ``bins`` (which is + melbank of size (``num_bins``, ``num_fft_bins``)) and ``center_freqs`` (which is + center frequencies of bins of size (``num_bins``)). + """ + assert num_bins > 3, "Must have at least 3 mel bins" + assert window_length_padded % 2 == 0 + num_fft_bins = window_length_padded / 2 + nyquist = 0.5 * sample_freq + + if high_freq <= 0.0: + high_freq += nyquist + + assert ( + (0.0 <= low_freq < nyquist) and (0.0 < high_freq <= nyquist) and (low_freq < high_freq) + ), "Bad values in options: low-freq {} and high-freq {} vs. nyquist {}".format(low_freq, high_freq, nyquist) + + # fft-bin width [think of it as Nyquist-freq / half-window-length] + fft_bin_width = sample_freq / window_length_padded + mel_low_freq = mel_scale_scalar(low_freq) + mel_high_freq = mel_scale_scalar(high_freq) + + # divide by num_bins+1 in next line because of end-effects where the bins + # spread out to the sides. + mel_freq_delta = (mel_high_freq - mel_low_freq) / (num_bins + 1) + + if vtln_high < 0.0: + vtln_high += nyquist + + assert vtln_warp_factor == 1.0 or ( + (low_freq < vtln_low < high_freq) and (0.0 < vtln_high < high_freq) and (vtln_low < vtln_high) + ), "Bad values in options: vtln-low {} and vtln-high {}, versus " "low-freq {} and high-freq {}".format( + vtln_low, vtln_high, low_freq, high_freq + ) + + bin = torch.arange(num_bins).unsqueeze(1) + left_mel = mel_low_freq + bin * mel_freq_delta # size(num_bins, 1) + center_mel = mel_low_freq + (bin + 1.0) * mel_freq_delta # size(num_bins, 1) + right_mel = mel_low_freq + (bin + 2.0) * mel_freq_delta # size(num_bins, 1) + + if vtln_warp_factor != 1.0: + left_mel = vtln_warp_mel_freq(vtln_low, vtln_high, low_freq, high_freq, vtln_warp_factor, left_mel) + center_mel = vtln_warp_mel_freq(vtln_low, vtln_high, low_freq, high_freq, vtln_warp_factor, center_mel) + right_mel = vtln_warp_mel_freq(vtln_low, vtln_high, low_freq, high_freq, vtln_warp_factor, right_mel) + + center_freqs = inverse_mel_scale(center_mel) # size (num_bins) + # size(1, num_fft_bins) + mel = mel_scale(fft_bin_width * torch.arange(num_fft_bins)).unsqueeze(0) + + # size (num_bins, num_fft_bins) + up_slope = (mel - left_mel) / (center_mel - left_mel) + down_slope = (right_mel - mel) / (right_mel - center_mel) + + if vtln_warp_factor == 1.0: + # left_mel < center_mel < right_mel so we can min the two slopes and clamp negative values + bins = torch.max(torch.zeros(1), torch.min(up_slope, down_slope)) + else: + # warping can move the order of left_mel, center_mel, right_mel anywhere + bins = torch.zeros_like(up_slope) + up_idx = torch.gt(mel, left_mel) & torch.le(mel, center_mel) # left_mel < mel <= center_mel + down_idx = torch.gt(mel, center_mel) & torch.lt(mel, right_mel) # center_mel < mel < right_mel + bins[up_idx] = up_slope[up_idx] + bins[down_idx] = down_slope[down_idx] + + return bins, center_freqs + + +def fbank( + waveform: Tensor, + blackman_coeff: float = 0.42, + channel: int = -1, + dither: float = 0.0, + energy_floor: float = 1.0, + frame_length: float = 25.0, + frame_shift: float = 10.0, + high_freq: float = 0.0, + htk_compat: bool = False, + low_freq: float = 20.0, + min_duration: float = 0.0, + num_mel_bins: int = 23, + preemphasis_coefficient: float = 0.97, + raw_energy: bool = True, + remove_dc_offset: bool = True, + round_to_power_of_two: bool = True, + sample_frequency: float = 16000.0, + snip_edges: bool = True, + subtract_mean: bool = False, + use_energy: bool = False, + use_log_fbank: bool = True, + use_power: bool = True, + vtln_high: float = -500.0, + vtln_low: float = 100.0, + vtln_warp: float = 1.0, + window_type: str = POVEY, +) -> Tensor: + r"""Create a fbank from a raw audio signal. This matches the input/output of Kaldi's + compute-fbank-feats. + + Args: + waveform (Tensor): Tensor of audio of size (c, n) where c is in the range [0,2) + blackman_coeff (float, optional): Constant coefficient for generalized Blackman window. (Default: ``0.42``) + channel (int, optional): Channel to extract (-1 -> expect mono, 0 -> left, 1 -> right) (Default: ``-1``) + dither (float, optional): Dithering constant (0.0 means no dither). If you turn this off, you should set + the energy_floor option, e.g. to 1.0 or 0.1 (Default: ``0.0``) + energy_floor (float, optional): Floor on energy (absolute, not relative) in Spectrogram computation. Caution: + this floor is applied to the zeroth component, representing the total signal energy. The floor on the + individual spectrogram elements is fixed at std::numeric_limits::epsilon(). (Default: ``1.0``) + frame_length (float, optional): Frame length in milliseconds (Default: ``25.0``) + frame_shift (float, optional): Frame shift in milliseconds (Default: ``10.0``) + high_freq (float, optional): High cutoff frequency for mel bins (if <= 0, offset from Nyquist) + (Default: ``0.0``) + htk_compat (bool, optional): If true, put energy last. Warning: not sufficient to get HTK compatible features + (need to change other parameters). (Default: ``False``) + low_freq (float, optional): Low cutoff frequency for mel bins (Default: ``20.0``) + min_duration (float, optional): Minimum duration of segments to process (in seconds). (Default: ``0.0``) + num_mel_bins (int, optional): Number of triangular mel-frequency bins (Default: ``23``) + preemphasis_coefficient (float, optional): Coefficient for use in signal preemphasis (Default: ``0.97``) + raw_energy (bool, optional): If True, compute energy before preemphasis and windowing (Default: ``True``) + remove_dc_offset (bool, optional): Subtract mean from waveform on each frame (Default: ``True``) + round_to_power_of_two (bool, optional): If True, round window size to power of two by zero-padding input + to FFT. (Default: ``True``) + sample_frequency (float, optional): Waveform data sample frequency (must match the waveform file, if + specified there) (Default: ``16000.0``) + snip_edges (bool, optional): If True, end effects will be handled by outputting only frames that completely fit + in the file, and the number of frames depends on the frame_length. If False, the number of frames + depends only on the frame_shift, and we reflect the data at the ends. (Default: ``True``) + subtract_mean (bool, optional): Subtract mean of each feature file [CMS]; not recommended to do + it this way. (Default: ``False``) + use_energy (bool, optional): Add an extra dimension with energy to the FBANK output. (Default: ``False``) + use_log_fbank (bool, optional):If true, produce log-filterbank, else produce linear. (Default: ``True``) + use_power (bool, optional): If true, use power, else use magnitude. (Default: ``True``) + vtln_high (float, optional): High inflection point in piecewise linear VTLN warping function (if + negative, offset from high-mel-freq (Default: ``-500.0``) + vtln_low (float, optional): Low inflection point in piecewise linear VTLN warping function (Default: ``100.0``) + vtln_warp (float, optional): Vtln warp factor (only applicable if vtln_map not specified) (Default: ``1.0``) + window_type (str, optional): Type of window ('hamming'|'hanning'|'povey'|'rectangular'|'blackman') + (Default: ``'povey'``) + + Returns: + Tensor: A fbank identical to what Kaldi would output. The shape is (m, ``num_mel_bins + use_energy``) + where m is calculated in _get_strided + """ + device, dtype = waveform.device, waveform.dtype + + waveform, window_shift, window_size, padded_window_size = _get_waveform_and_window_properties( + waveform, channel, sample_frequency, frame_shift, frame_length, round_to_power_of_two, preemphasis_coefficient + ) + + if len(waveform) < min_duration * sample_frequency: + # signal is too short + return torch.empty(0, device=device, dtype=dtype) + + # strided_input, size (m, padded_window_size) and signal_log_energy, size (m) + strided_input, signal_log_energy = _get_window( + waveform, + padded_window_size, + window_size, + window_shift, + window_type, + blackman_coeff, + snip_edges, + raw_energy, + energy_floor, + dither, + remove_dc_offset, + preemphasis_coefficient, + ) + + # size (m, padded_window_size // 2 + 1) + spectrum = torch.fft.rfft(strided_input).abs() + if use_power: + spectrum = spectrum.pow(2.0) + + # size (num_mel_bins, padded_window_size // 2) + mel_energies, _ = get_mel_banks( + num_mel_bins, padded_window_size, sample_frequency, low_freq, high_freq, vtln_low, vtln_high, vtln_warp + ) + mel_energies = mel_energies.to(device=device, dtype=dtype) + + # pad right column with zeros and add dimension, size (num_mel_bins, padded_window_size // 2 + 1) + mel_energies = torch.nn.functional.pad(mel_energies, (0, 1), mode="constant", value=0) + + # sum with mel fiterbanks over the power spectrum, size (m, num_mel_bins) + mel_energies = torch.mm(spectrum, mel_energies.T) + if use_log_fbank: + # avoid log of zero (which should be prevented anyway by dithering) + mel_energies = torch.max(mel_energies, _get_epsilon(device, dtype)).log() + + # if use_energy then add it as the last column for htk_compat == true else first column + if use_energy: + signal_log_energy = signal_log_energy.unsqueeze(1) # size (m, 1) + # returns size (m, num_mel_bins + 1) + if htk_compat: + mel_energies = torch.cat((mel_energies, signal_log_energy), dim=1) + else: + mel_energies = torch.cat((signal_log_energy, mel_energies), dim=1) + + mel_energies = _subtract_column_mean(mel_energies, subtract_mean) + return mel_energies + + +def _get_dct_matrix(num_ceps: int, num_mel_bins: int) -> Tensor: + # returns a dct matrix of size (num_mel_bins, num_ceps) + # size (num_mel_bins, num_mel_bins) + dct_matrix = torchaudio.functional.create_dct(num_mel_bins, num_mel_bins, "ortho") + # kaldi expects the first cepstral to be weighted sum of factor sqrt(1/num_mel_bins) + # this would be the first column in the dct_matrix for torchaudio as it expects a + # right multiply (which would be the first column of the kaldi's dct_matrix as kaldi + # expects a left multiply e.g. dct_matrix * vector). + dct_matrix[:, 0] = math.sqrt(1 / float(num_mel_bins)) + dct_matrix = dct_matrix[:, :num_ceps] + return dct_matrix + + +def _get_lifter_coeffs(num_ceps: int, cepstral_lifter: float) -> Tensor: + # returns size (num_ceps) + # Compute liftering coefficients (scaling on cepstral coeffs) + # coeffs are numbered slightly differently from HTK: the zeroth index is C0, which is not affected. + i = torch.arange(num_ceps) + return 1.0 + 0.5 * cepstral_lifter * torch.sin(math.pi * i / cepstral_lifter) + + +def mfcc( + waveform: Tensor, + blackman_coeff: float = 0.42, + cepstral_lifter: float = 22.0, + channel: int = -1, + dither: float = 0.0, + energy_floor: float = 1.0, + frame_length: float = 25.0, + frame_shift: float = 10.0, + high_freq: float = 0.0, + htk_compat: bool = False, + low_freq: float = 20.0, + num_ceps: int = 13, + min_duration: float = 0.0, + num_mel_bins: int = 23, + preemphasis_coefficient: float = 0.97, + raw_energy: bool = True, + remove_dc_offset: bool = True, + round_to_power_of_two: bool = True, + sample_frequency: float = 16000.0, + snip_edges: bool = True, + subtract_mean: bool = False, + use_energy: bool = False, + vtln_high: float = -500.0, + vtln_low: float = 100.0, + vtln_warp: float = 1.0, + window_type: str = POVEY, +) -> Tensor: + r"""Create a mfcc from a raw audio signal. This matches the input/output of Kaldi's + compute-mfcc-feats. + + Args: + waveform (Tensor): Tensor of audio of size (c, n) where c is in the range [0,2) + blackman_coeff (float, optional): Constant coefficient for generalized Blackman window. (Default: ``0.42``) + cepstral_lifter (float, optional): Constant that controls scaling of MFCCs (Default: ``22.0``) + channel (int, optional): Channel to extract (-1 -> expect mono, 0 -> left, 1 -> right) (Default: ``-1``) + dither (float, optional): Dithering constant (0.0 means no dither). If you turn this off, you should set + the energy_floor option, e.g. to 1.0 or 0.1 (Default: ``0.0``) + energy_floor (float, optional): Floor on energy (absolute, not relative) in Spectrogram computation. Caution: + this floor is applied to the zeroth component, representing the total signal energy. The floor on the + individual spectrogram elements is fixed at std::numeric_limits::epsilon(). (Default: ``1.0``) + frame_length (float, optional): Frame length in milliseconds (Default: ``25.0``) + frame_shift (float, optional): Frame shift in milliseconds (Default: ``10.0``) + high_freq (float, optional): High cutoff frequency for mel bins (if <= 0, offset from Nyquist) + (Default: ``0.0``) + htk_compat (bool, optional): If true, put energy last. Warning: not sufficient to get HTK compatible + features (need to change other parameters). (Default: ``False``) + low_freq (float, optional): Low cutoff frequency for mel bins (Default: ``20.0``) + num_ceps (int, optional): Number of cepstra in MFCC computation (including C0) (Default: ``13``) + min_duration (float, optional): Minimum duration of segments to process (in seconds). (Default: ``0.0``) + num_mel_bins (int, optional): Number of triangular mel-frequency bins (Default: ``23``) + preemphasis_coefficient (float, optional): Coefficient for use in signal preemphasis (Default: ``0.97``) + raw_energy (bool, optional): If True, compute energy before preemphasis and windowing (Default: ``True``) + remove_dc_offset (bool, optional): Subtract mean from waveform on each frame (Default: ``True``) + round_to_power_of_two (bool, optional): If True, round window size to power of two by zero-padding input + to FFT. (Default: ``True``) + sample_frequency (float, optional): Waveform data sample frequency (must match the waveform file, if + specified there) (Default: ``16000.0``) + snip_edges (bool, optional): If True, end effects will be handled by outputting only frames that completely fit + in the file, and the number of frames depends on the frame_length. If False, the number of frames + depends only on the frame_shift, and we reflect the data at the ends. (Default: ``True``) + subtract_mean (bool, optional): Subtract mean of each feature file [CMS]; not recommended to do + it this way. (Default: ``False``) + use_energy (bool, optional): Add an extra dimension with energy to the FBANK output. (Default: ``False``) + vtln_high (float, optional): High inflection point in piecewise linear VTLN warping function (if + negative, offset from high-mel-freq (Default: ``-500.0``) + vtln_low (float, optional): Low inflection point in piecewise linear VTLN warping function (Default: ``100.0``) + vtln_warp (float, optional): Vtln warp factor (only applicable if vtln_map not specified) (Default: ``1.0``) + window_type (str, optional): Type of window ('hamming'|'hanning'|'povey'|'rectangular'|'blackman') + (Default: ``"povey"``) + + Returns: + Tensor: A mfcc identical to what Kaldi would output. The shape is (m, ``num_ceps``) + where m is calculated in _get_strided + """ + assert num_ceps <= num_mel_bins, "num_ceps cannot be larger than num_mel_bins: %d vs %d" % (num_ceps, num_mel_bins) + + device, dtype = waveform.device, waveform.dtype + + # The mel_energies should not be squared (use_power=True), not have mean subtracted + # (subtract_mean=False), and use log (use_log_fbank=True). + # size (m, num_mel_bins + use_energy) + feature = fbank( + waveform=waveform, + blackman_coeff=blackman_coeff, + channel=channel, + dither=dither, + energy_floor=energy_floor, + frame_length=frame_length, + frame_shift=frame_shift, + high_freq=high_freq, + htk_compat=htk_compat, + low_freq=low_freq, + min_duration=min_duration, + num_mel_bins=num_mel_bins, + preemphasis_coefficient=preemphasis_coefficient, + raw_energy=raw_energy, + remove_dc_offset=remove_dc_offset, + round_to_power_of_two=round_to_power_of_two, + sample_frequency=sample_frequency, + snip_edges=snip_edges, + subtract_mean=False, + use_energy=use_energy, + use_log_fbank=True, + use_power=True, + vtln_high=vtln_high, + vtln_low=vtln_low, + vtln_warp=vtln_warp, + window_type=window_type, + ) + + if use_energy: + # size (m) + signal_log_energy = feature[:, num_mel_bins if htk_compat else 0] + # offset is 0 if htk_compat==True else 1 + mel_offset = int(not htk_compat) + feature = feature[:, mel_offset : (num_mel_bins + mel_offset)] + + # size (num_mel_bins, num_ceps) + dct_matrix = _get_dct_matrix(num_ceps, num_mel_bins).to(dtype=dtype, device=device) + + # size (m, num_ceps) + feature = feature.matmul(dct_matrix) + + if cepstral_lifter != 0.0: + # size (1, num_ceps) + lifter_coeffs = _get_lifter_coeffs(num_ceps, cepstral_lifter).unsqueeze(0) + feature *= lifter_coeffs.to(device=device, dtype=dtype) + + # if use_energy then replace the last column for htk_compat == true else first column + if use_energy: + feature[:, 0] = signal_log_energy + + if htk_compat: + energy = feature[:, 0].unsqueeze(1) # size (m, 1) + feature = feature[:, 1:] # size (m, num_ceps - 1) + if not use_energy: + # scale on C0 (actually removing a scale we previously added that's + # part of one common definition of the cosine transform.) + energy *= math.sqrt(2) + + feature = torch.cat((feature, energy), dim=1) + + feature = _subtract_column_mean(feature, subtract_mean) + return feature diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__init__.py b/lib/python3.12/site-packages/torchaudio/datasets/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..609cb14fdcc38c48270acd5803f4bfe603c39e71 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/__init__.py @@ -0,0 +1,47 @@ +from .cmuarctic import CMUARCTIC +from .cmudict import CMUDict +from .commonvoice import COMMONVOICE +from .dr_vctk import DR_VCTK +from .fluentcommands import FluentSpeechCommands +from .gtzan import GTZAN +from .iemocap import IEMOCAP +from .librilight_limited import LibriLightLimited +from .librimix import LibriMix +from .librispeech import LIBRISPEECH +from .librispeech_biasing import LibriSpeechBiasing +from .libritts import LIBRITTS +from .ljspeech import LJSPEECH +from .musdb_hq import MUSDB_HQ +from .quesst14 import QUESST14 +from .snips import Snips +from .speechcommands import SPEECHCOMMANDS +from .tedlium import TEDLIUM +from .vctk import VCTK_092 +from .voxceleb1 import VoxCeleb1Identification, VoxCeleb1Verification +from .yesno import YESNO + + +__all__ = [ + "COMMONVOICE", + "LIBRISPEECH", + "LibriSpeechBiasing", + "LibriLightLimited", + "SPEECHCOMMANDS", + "VCTK_092", + "DR_VCTK", + "YESNO", + "LJSPEECH", + "GTZAN", + "CMUARCTIC", + "CMUDict", + "LibriMix", + "LIBRITTS", + "TEDLIUM", + "QUESST14", + "MUSDB_HQ", + "FluentSpeechCommands", + "VoxCeleb1Identification", + "VoxCeleb1Verification", + "IEMOCAP", + "Snips", +] diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb0505109b9e956fa4d1b2e6984fa19a5b442a94 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/cmuarctic.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/cmuarctic.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5e09c9c5fdd53a53606cae9d0b54f3b63f210060 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/cmuarctic.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/commonvoice.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/commonvoice.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c17637bc2f8bbeb81250c11ffee6fad58318010 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/commonvoice.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/dr_vctk.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/dr_vctk.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3b24fa7b049fe52328d9c48729336bed0503357 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/dr_vctk.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/fluentcommands.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/fluentcommands.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a45ce541c45840d898684bb4f19bbac054e6bd6 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/fluentcommands.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/iemocap.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/iemocap.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a2b09382b819164ea07e03c8f1ed66df6e2aa4a Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/iemocap.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librilight_limited.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librilight_limited.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d704152025d2d867751c78622c36cc9b9bd8ecee Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librilight_limited.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librimix.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librimix.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..382e5298ad419001817059b79f6f5619488408b3 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librimix.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librispeech.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librispeech.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a2557565152b363b6f87b16b3c84b4a9d273464 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librispeech.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librispeech_biasing.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librispeech_biasing.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39bfd065c2a756b843c66f172f0554d55193908f Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/librispeech_biasing.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/libritts.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/libritts.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0afc99b40a2600840a193c80bb673e282c925fe Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/libritts.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/quesst14.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/quesst14.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fdbb379fc39bb6ec2879ab911e78bb475687a886 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/quesst14.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/snips.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/snips.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e1fddcef0f25f45ab8c1b09e282273a3a48f9e5c Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/snips.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/speechcommands.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/speechcommands.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b309b3b60469cc6f75353778aad13569caef140 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/speechcommands.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/tedlium.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/tedlium.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0c50c6bac3416f93627d456a2d36fb1389287d0 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/tedlium.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/utils.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60d1a65f3024718ba8e4e9736262d23a73e4ddea Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/utils.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/vctk.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/vctk.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c4324b4cba75b57c20859fb664bef7d430d1787 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/vctk.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/voxceleb1.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/voxceleb1.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5103922b0b25310d77e112489d767f391fd4205d Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/voxceleb1.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/yesno.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/yesno.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e6393522d697aea4bf6a6f13ea9663ed1f1eec2 Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/datasets/__pycache__/yesno.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/datasets/cmuarctic.py b/lib/python3.12/site-packages/torchaudio/datasets/cmuarctic.py new file mode 100644 index 0000000000000000000000000000000000000000..96f498f00f04a2f1b6d7d3e33510eafcb9ffe6bc --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/cmuarctic.py @@ -0,0 +1,157 @@ +import csv +import os +from pathlib import Path +from typing import Tuple, Union + +import torchaudio +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_tar + +URL = "aew" +FOLDER_IN_ARCHIVE = "ARCTIC" +_CHECKSUMS = { + "http://festvox.org/cmu_arctic/packed/cmu_us_aew_arctic.tar.bz2": "645cb33c0f0b2ce41384fdd8d3db2c3f5fc15c1e688baeb74d2e08cab18ab406", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_ahw_arctic.tar.bz2": "024664adeb892809d646a3efd043625b46b5bfa3e6189b3500b2d0d59dfab06c", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_aup_arctic.tar.bz2": "2c55bc3050caa996758869126ad10cf42e1441212111db034b3a45189c18b6fc", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_awb_arctic.tar.bz2": "d74a950c9739a65f7bfc4dfa6187f2730fa03de5b8eb3f2da97a51b74df64d3c", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_axb_arctic.tar.bz2": "dd65c3d2907d1ee52f86e44f578319159e60f4bf722a9142be01161d84e330ff", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_bdl_arctic.tar.bz2": "26b91aaf48b2799b2956792b4632c2f926cd0542f402b5452d5adecb60942904", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_clb_arctic.tar.bz2": "3f16dc3f3b97955ea22623efb33b444341013fc660677b2e170efdcc959fa7c6", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_eey_arctic.tar.bz2": "8a0ee4e5acbd4b2f61a4fb947c1730ab3adcc9dc50b195981d99391d29928e8a", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_fem_arctic.tar.bz2": "3fcff629412b57233589cdb058f730594a62c4f3a75c20de14afe06621ef45e2", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_gka_arctic.tar.bz2": "dc82e7967cbd5eddbed33074b0699128dbd4482b41711916d58103707e38c67f", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_jmk_arctic.tar.bz2": "3a37c0e1dfc91e734fdbc88b562d9e2ebca621772402cdc693bbc9b09b211d73", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_ksp_arctic.tar.bz2": "8029cafce8296f9bed3022c44ef1e7953332b6bf6943c14b929f468122532717", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_ljm_arctic.tar.bz2": "b23993765cbf2b9e7bbc3c85b6c56eaf292ac81ee4bb887b638a24d104f921a0", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_lnh_arctic.tar.bz2": "4faf34d71aa7112813252fb20c5433e2fdd9a9de55a00701ffcbf05f24a5991a", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_rms_arctic.tar.bz2": "c6dc11235629c58441c071a7ba8a2d067903dfefbaabc4056d87da35b72ecda4", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_rxr_arctic.tar.bz2": "1fa4271c393e5998d200e56c102ff46fcfea169aaa2148ad9e9469616fbfdd9b", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_slp_arctic.tar.bz2": "54345ed55e45c23d419e9a823eef427f1cc93c83a710735ec667d068c916abf1", # noqa: E501 + "http://festvox.org/cmu_arctic/packed/cmu_us_slt_arctic.tar.bz2": "7c173297916acf3cc7fcab2713be4c60b27312316765a90934651d367226b4ea", # noqa: E501 +} + + +def load_cmuarctic_item(line: str, path: str, folder_audio: str, ext_audio: str) -> Tuple[Tensor, int, str, str]: + + utterance_id, transcript = line[0].strip().split(" ", 2)[1:] + + # Remove space, double quote, and single parenthesis from transcript + transcript = transcript[1:-3] + + file_audio = os.path.join(path, folder_audio, utterance_id + ext_audio) + + # Load audio + waveform, sample_rate = torchaudio.load(file_audio) + + return (waveform, sample_rate, transcript, utterance_id.split("_")[1]) + + +class CMUARCTIC(Dataset): + """*CMU ARCTIC* :cite:`Kominek03cmuarctic` dataset. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + url (str, optional): + The URL to download the dataset from or the type of the dataset to download. + (default: ``"aew"``) + Allowed type values are ``"aew"``, ``"ahw"``, ``"aup"``, ``"awb"``, ``"axb"``, ``"bdl"``, + ``"clb"``, ``"eey"``, ``"fem"``, ``"gka"``, ``"jmk"``, ``"ksp"``, ``"ljm"``, ``"lnh"``, + ``"rms"``, ``"rxr"``, ``"slp"`` or ``"slt"``. + folder_in_archive (str, optional): + The top-level directory of the dataset. (default: ``"ARCTIC"``) + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + """ + + _file_text = "txt.done.data" + _folder_text = "etc" + _ext_audio = ".wav" + _folder_audio = "wav" + + def __init__( + self, root: Union[str, Path], url: str = URL, folder_in_archive: str = FOLDER_IN_ARCHIVE, download: bool = False + ) -> None: + + if url in [ + "aew", + "ahw", + "aup", + "awb", + "axb", + "bdl", + "clb", + "eey", + "fem", + "gka", + "jmk", + "ksp", + "ljm", + "lnh", + "rms", + "rxr", + "slp", + "slt", + ]: + + url = "cmu_us_" + url + "_arctic" + ext_archive = ".tar.bz2" + base_url = "http://www.festvox.org/cmu_arctic/packed/" + + url = os.path.join(base_url, url + ext_archive) + + # Get string representation of 'root' in case Path object is passed + root = os.fspath(root) + + basename = os.path.basename(url) + root = os.path.join(root, folder_in_archive) + if not os.path.isdir(root): + os.mkdir(root) + archive = os.path.join(root, basename) + + basename = basename.split(".")[0] + + self._path = os.path.join(root, basename) + + if download: + if not os.path.isdir(self._path): + if not os.path.isfile(archive): + checksum = _CHECKSUMS.get(url, None) + download_url_to_file(url, archive, hash_prefix=checksum) + _extract_tar(archive) + else: + if not os.path.exists(self._path): + raise RuntimeError( + f"The path {self._path} doesn't exist. " + "Please check the ``root`` path or set `download=True` to download it" + ) + self._text = os.path.join(self._path, self._folder_text, self._file_text) + + with open(self._text, "r") as text: + walker = csv.reader(text, delimiter="\n") + self._walker = list(walker) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + Transcript + str: + Utterance ID + """ + line = self._walker[n] + return load_cmuarctic_item(line, self._path, self._folder_audio, self._ext_audio) + + def __len__(self) -> int: + return len(self._walker) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/cmudict.py b/lib/python3.12/site-packages/torchaudio/datasets/cmudict.py new file mode 100644 index 0000000000000000000000000000000000000000..d1038f48badde6f5db589691c5aee2ddf1f1d5de --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/cmudict.py @@ -0,0 +1,186 @@ +import os +import re +from pathlib import Path +from typing import Iterable, List, Tuple, Union + +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file + + +_CHECKSUMS = { + "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b": "209a8b4cd265013e96f4658632a9878103b0c5abf62b50d4ef3ae1be226b29e4", # noqa: E501 + "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols": "408ccaae803641c6d7b626b6299949320c2dbca96b2220fd3fb17887b023b027", # noqa: E501 +} +_PUNCTUATIONS = { + "!EXCLAMATION-POINT", + '"CLOSE-QUOTE', + '"DOUBLE-QUOTE', + '"END-OF-QUOTE', + '"END-QUOTE', + '"IN-QUOTES', + '"QUOTE', + '"UNQUOTE', + "#HASH-MARK", + "#POUND-SIGN", + "#SHARP-SIGN", + "%PERCENT", + "&ERSAND", + "'END-INNER-QUOTE", + "'END-QUOTE", + "'INNER-QUOTE", + "'QUOTE", + "'SINGLE-QUOTE", + "(BEGIN-PARENS", + "(IN-PARENTHESES", + "(LEFT-PAREN", + "(OPEN-PARENTHESES", + "(PAREN", + "(PARENS", + "(PARENTHESES", + ")CLOSE-PAREN", + ")CLOSE-PARENTHESES", + ")END-PAREN", + ")END-PARENS", + ")END-PARENTHESES", + ")END-THE-PAREN", + ")PAREN", + ")PARENS", + ")RIGHT-PAREN", + ")UN-PARENTHESES", + "+PLUS", + ",COMMA", + "--DASH", + "-DASH", + "-HYPHEN", + "...ELLIPSIS", + ".DECIMAL", + ".DOT", + ".FULL-STOP", + ".PERIOD", + ".POINT", + "/SLASH", + ":COLON", + ";SEMI-COLON", + ";SEMI-COLON(1)", + "?QUESTION-MARK", + "{BRACE", + "{LEFT-BRACE", + "{OPEN-BRACE", + "}CLOSE-BRACE", + "}RIGHT-BRACE", +} + + +def _parse_dictionary(lines: Iterable[str], exclude_punctuations: bool) -> List[str]: + _alt_re = re.compile(r"\([0-9]+\)") + cmudict: List[Tuple[str, List[str]]] = [] + for line in lines: + if not line or line.startswith(";;;"): # ignore comments + continue + + word, phones = line.strip().split(" ") + if word in _PUNCTUATIONS: + if exclude_punctuations: + continue + # !EXCLAMATION-POINT -> ! + # --DASH -> -- + # ...ELLIPSIS -> ... + if word.startswith("..."): + word = "..." + elif word.startswith("--"): + word = "--" + else: + word = word[0] + + # if a word have multiple pronunciations, there will be (number) appended to it + # for example, DATAPOINTS and DATAPOINTS(1), + # the regular expression `_alt_re` removes the '(1)' and change the word DATAPOINTS(1) to DATAPOINTS + word = re.sub(_alt_re, "", word) + phones = phones.split(" ") + cmudict.append((word, phones)) + + return cmudict + + +class CMUDict(Dataset): + """*CMU Pronouncing Dictionary* :cite:`cmudict` (CMUDict) dataset. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + exclude_punctuations (bool, optional): + When enabled, exclude the pronounciation of punctuations, such as + `!EXCLAMATION-POINT` and `#HASH-MARK`. + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + url (str, optional): + The URL to download the dictionary from. + (default: ``"http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b"``) + url_symbols (str, optional): + The URL to download the list of symbols from. + (default: ``"http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols"``) + """ + + def __init__( + self, + root: Union[str, Path], + exclude_punctuations: bool = True, + *, + download: bool = False, + url: str = "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b", + url_symbols: str = "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b.symbols", + ) -> None: + + self.exclude_punctuations = exclude_punctuations + + self._root_path = Path(root) + if not os.path.isdir(self._root_path): + raise RuntimeError(f"The root directory does not exist; {root}") + + dict_file = self._root_path / os.path.basename(url) + symbol_file = self._root_path / os.path.basename(url_symbols) + if not os.path.exists(dict_file): + if not download: + raise RuntimeError( + "The dictionary file is not found in the following location. " + f"Set `download=True` to download it. {dict_file}" + ) + checksum = _CHECKSUMS.get(url, None) + download_url_to_file(url, dict_file, checksum) + if not os.path.exists(symbol_file): + if not download: + raise RuntimeError( + "The symbol file is not found in the following location. " + f"Set `download=True` to download it. {symbol_file}" + ) + checksum = _CHECKSUMS.get(url_symbols, None) + download_url_to_file(url_symbols, symbol_file, checksum) + + with open(symbol_file, "r") as text: + self._symbols = [line.strip() for line in text.readlines()] + + with open(dict_file, "r", encoding="latin-1") as text: + self._dictionary = _parse_dictionary(text.readlines(), exclude_punctuations=self.exclude_punctuations) + + def __getitem__(self, n: int) -> Tuple[str, List[str]]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded. + + Returns: + Tuple of a word and its phonemes + + str: + Word + List[str]: + Phonemes + """ + return self._dictionary[n] + + def __len__(self) -> int: + return len(self._dictionary) + + @property + def symbols(self) -> List[str]: + """list[str]: A list of phonemes symbols, such as ``"AA"``, ``"AE"``, ``"AH"``.""" + return self._symbols.copy() diff --git a/lib/python3.12/site-packages/torchaudio/datasets/commonvoice.py b/lib/python3.12/site-packages/torchaudio/datasets/commonvoice.py new file mode 100644 index 0000000000000000000000000000000000000000..db0e035c6116487a87efcffaeea31a19212be458 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/commonvoice.py @@ -0,0 +1,86 @@ +import csv +import os +from pathlib import Path +from typing import Dict, List, Tuple, Union + +import torchaudio +from torch import Tensor +from torch.utils.data import Dataset + + +def load_commonvoice_item( + line: List[str], header: List[str], path: str, folder_audio: str, ext_audio: str +) -> Tuple[Tensor, int, Dict[str, str]]: + # Each line as the following data: + # client_id, path, sentence, up_votes, down_votes, age, gender, accent + + if header[1] != "path": + raise ValueError(f"expect `header[1]` to be 'path', but got {header[1]}") + fileid = line[1] + filename = os.path.join(path, folder_audio, fileid) + if not filename.endswith(ext_audio): + filename += ext_audio + waveform, sample_rate = torchaudio.load(filename) + + dic = dict(zip(header, line)) + + return waveform, sample_rate, dic + + +class COMMONVOICE(Dataset): + """*CommonVoice* :cite:`ardila2020common` dataset. + + Args: + root (str or Path): Path to the directory where the dataset is located. + (Where the ``tsv`` file is present.) + tsv (str, optional): + The name of the tsv file used to construct the metadata, such as + ``"train.tsv"``, ``"test.tsv"``, ``"dev.tsv"``, ``"invalidated.tsv"``, + ``"validated.tsv"`` and ``"other.tsv"``. (default: ``"train.tsv"``) + """ + + _ext_txt = ".txt" + _ext_audio = ".mp3" + _folder_audio = "clips" + + def __init__(self, root: Union[str, Path], tsv: str = "train.tsv") -> None: + + # Get string representation of 'root' in case Path object is passed + self._path = os.fspath(root) + self._tsv = os.path.join(self._path, tsv) + + with open(self._tsv, "r") as tsv_: + walker = csv.reader(tsv_, delimiter="\t") + self._header = next(walker) + self._walker = list(walker) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, Dict[str, str]]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + Dict[str, str]: + Dictionary containing the following items from the corresponding TSV file; + + * ``"client_id"`` + * ``"path"`` + * ``"sentence"`` + * ``"up_votes"`` + * ``"down_votes"`` + * ``"age"`` + * ``"gender"`` + * ``"accent"`` + """ + line = self._walker[n] + return load_commonvoice_item(line, self._header, self._path, self._folder_audio, self._ext_audio) + + def __len__(self) -> int: + return len(self._walker) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/dr_vctk.py b/lib/python3.12/site-packages/torchaudio/datasets/dr_vctk.py new file mode 100644 index 0000000000000000000000000000000000000000..a634b968949480738eefef926d25b05846f0b67d --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/dr_vctk.py @@ -0,0 +1,121 @@ +from pathlib import Path +from typing import Dict, Tuple, Union + +import torchaudio +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_zip + + +_URL = "https://datashare.ed.ac.uk/bitstream/handle/10283/3038/DR-VCTK.zip" +_CHECKSUM = "781f12f4406ed36ed27ae3bce55da47ba176e2d8bae67319e389e07b2c9bd769" +_SUPPORTED_SUBSETS = {"train", "test"} + + +class DR_VCTK(Dataset): + """*Device Recorded VCTK (Small subset version)* :cite:`Sarfjoo2018DeviceRV` dataset. + + Args: + root (str or Path): Root directory where the dataset's top level directory is found. + subset (str): The subset to use. Can be one of ``"train"`` and ``"test"``. (default: ``"train"``). + download (bool): + Whether to download the dataset if it is not found at root path. (default: ``False``). + url (str): The URL to download the dataset from. + (default: ``"https://datashare.ed.ac.uk/bitstream/handle/10283/3038/DR-VCTK.zip"``) + """ + + def __init__( + self, + root: Union[str, Path], + subset: str = "train", + *, + download: bool = False, + url: str = _URL, + ) -> None: + if subset not in _SUPPORTED_SUBSETS: + raise RuntimeError( + f"The subset '{subset}' does not match any of the supported subsets: {_SUPPORTED_SUBSETS}" + ) + + root = Path(root).expanduser() + archive = root / "DR-VCTK.zip" + + self._subset = subset + self._path = root / "DR-VCTK" / "DR-VCTK" + self._clean_audio_dir = self._path / f"clean_{self._subset}set_wav_16k" + self._noisy_audio_dir = self._path / f"device-recorded_{self._subset}set_wav_16k" + self._config_filepath = self._path / "configurations" / f"{self._subset}_ch_log.txt" + + if not self._path.is_dir(): + if not archive.is_file(): + if not download: + raise RuntimeError("Dataset not found. Please use `download=True` to download it.") + download_url_to_file(url, archive, hash_prefix=_CHECKSUM) + _extract_zip(archive, root) + + self._config = self._load_config(self._config_filepath) + self._filename_list = sorted(self._config) + + def _load_config(self, filepath: str) -> Dict[str, Tuple[str, int]]: + # Skip header + skip_rows = 2 if self._subset == "train" else 1 + + config = {} + with open(filepath) as f: + for i, line in enumerate(f): + if i < skip_rows or not line: + continue + filename, source, channel_id = line.strip().split("\t") + config[filename] = (source, int(channel_id)) + return config + + def _load_dr_vctk_item(self, filename: str) -> Tuple[Tensor, int, Tensor, int, str, str, str, int]: + speaker_id, utterance_id = filename.split(".")[0].split("_") + source, channel_id = self._config[filename] + file_clean_audio = self._clean_audio_dir / filename + file_noisy_audio = self._noisy_audio_dir / filename + waveform_clean, sample_rate_clean = torchaudio.load(file_clean_audio) + waveform_noisy, sample_rate_noisy = torchaudio.load(file_noisy_audio) + return ( + waveform_clean, + sample_rate_clean, + waveform_noisy, + sample_rate_noisy, + speaker_id, + utterance_id, + source, + channel_id, + ) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, Tensor, int, str, str, str, int]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Clean waveform + int: + Sample rate of the clean waveform + Tensor: + Noisy waveform + int: + Sample rate of the noisy waveform + str: + Speaker ID + str: + Utterance ID + str: + Source + int: + Channel ID + """ + filename = self._filename_list[n] + return self._load_dr_vctk_item(filename) + + def __len__(self) -> int: + return len(self._filename_list) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/fluentcommands.py b/lib/python3.12/site-packages/torchaudio/datasets/fluentcommands.py new file mode 100644 index 0000000000000000000000000000000000000000..5cdee398d6e31a5e622321d1f73177606d9c8640 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/fluentcommands.py @@ -0,0 +1,108 @@ +import csv +import os +from pathlib import Path +from typing import Tuple, Union + +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio.datasets.utils import _load_waveform + +SAMPLE_RATE = 16000 + + +class FluentSpeechCommands(Dataset): + """*Fluent Speech Commands* :cite:`fluent` dataset + + Args: + root (str of Path): Path to the directory where the dataset is found. + subset (str, optional): subset of the dataset to use. + Options: [``"train"``, ``"valid"``, ``"test"``]. + (Default: ``"train"``) + """ + + def __init__(self, root: Union[str, Path], subset: str = "train"): + if subset not in ["train", "valid", "test"]: + raise ValueError("`subset` must be one of ['train', 'valid', 'test']") + + root = os.fspath(root) + self._path = os.path.join(root, "fluent_speech_commands_dataset") + + if not os.path.isdir(self._path): + raise RuntimeError("Dataset not found.") + + subset_path = os.path.join(self._path, "data", f"{subset}_data.csv") + with open(subset_path) as subset_csv: + subset_reader = csv.reader(subset_csv) + data = list(subset_reader) + + self.header = data[0] + self.data = data[1:] + + def get_metadata(self, n: int) -> Tuple[str, int, str, int, str, str, str, str]: + """Get metadata for the n-th sample from the dataset. Returns filepath instead of waveform, + but otherwise returns the same fields as :py:func:`__getitem__`. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + str: + Path to audio + int: + Sample rate + str: + File name + int: + Speaker ID + str: + Transcription + str: + Action + str: + Object + str: + Location + """ + sample = self.data[n] + + file_name = sample[self.header.index("path")].split("/")[-1] + file_name = file_name.split(".")[0] + speaker_id, transcription, action, obj, location = sample[2:] + file_path = os.path.join("wavs", "speakers", speaker_id, f"{file_name}.wav") + + return file_path, SAMPLE_RATE, file_name, speaker_id, transcription, action, obj, location + + def __len__(self) -> int: + return len(self.data) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str, int, str, str, str, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + File name + int: + Speaker ID + str: + Transcription + str: + Action + str: + Object + str: + Location + """ + metadata = self.get_metadata(n) + waveform = _load_waveform(self._path, metadata[0], metadata[1]) + return (waveform,) + metadata[1:] diff --git a/lib/python3.12/site-packages/torchaudio/datasets/gtzan.py b/lib/python3.12/site-packages/torchaudio/datasets/gtzan.py new file mode 100644 index 0000000000000000000000000000000000000000..347e7e71831770f42d7fdaf0b3c63a09409f659d --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/gtzan.py @@ -0,0 +1,1118 @@ +import os +from pathlib import Path +from typing import Optional, Tuple, Union + +import torchaudio +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_tar + +# The following lists prefixed with `filtered_` provide a filtered split +# that: +# +# a. Mitigate a known issue with GTZAN (duplication) +# +# b. Provide a standard split for testing it against other +# methods (e.g. the one in jordipons/sklearn-audio-transfer-learning). +# +# Those are used when GTZAN is initialised with the `filtered` keyword. +# The split was taken from (github) jordipons/sklearn-audio-transfer-learning. + +gtzan_genres = [ + "blues", + "classical", + "country", + "disco", + "hiphop", + "jazz", + "metal", + "pop", + "reggae", + "rock", +] + +filtered_test = [ + "blues.00012", + "blues.00013", + "blues.00014", + "blues.00015", + "blues.00016", + "blues.00017", + "blues.00018", + "blues.00019", + "blues.00020", + "blues.00021", + "blues.00022", + "blues.00023", + "blues.00024", + "blues.00025", + "blues.00026", + "blues.00027", + "blues.00028", + "blues.00061", + "blues.00062", + "blues.00063", + "blues.00064", + "blues.00065", + "blues.00066", + "blues.00067", + "blues.00068", + "blues.00069", + "blues.00070", + "blues.00071", + "blues.00072", + "blues.00098", + "blues.00099", + "classical.00011", + "classical.00012", + "classical.00013", + "classical.00014", + "classical.00015", + "classical.00016", + "classical.00017", + "classical.00018", + "classical.00019", + "classical.00020", + "classical.00021", + "classical.00022", + "classical.00023", + "classical.00024", + "classical.00025", + "classical.00026", + "classical.00027", + "classical.00028", + "classical.00029", + "classical.00034", + "classical.00035", + "classical.00036", + "classical.00037", + "classical.00038", + "classical.00039", + "classical.00040", + "classical.00041", + "classical.00049", + "classical.00077", + "classical.00078", + "classical.00079", + "country.00030", + "country.00031", + "country.00032", + "country.00033", + "country.00034", + "country.00035", + "country.00036", + "country.00037", + "country.00038", + "country.00039", + "country.00040", + "country.00043", + "country.00044", + "country.00046", + "country.00047", + "country.00048", + "country.00050", + "country.00051", + "country.00053", + "country.00054", + "country.00055", + "country.00056", + "country.00057", + "country.00058", + "country.00059", + "country.00060", + "country.00061", + "country.00062", + "country.00063", + "country.00064", + "disco.00001", + "disco.00021", + "disco.00058", + "disco.00062", + "disco.00063", + "disco.00064", + "disco.00065", + "disco.00066", + "disco.00069", + "disco.00076", + "disco.00077", + "disco.00078", + "disco.00079", + "disco.00080", + "disco.00081", + "disco.00082", + "disco.00083", + "disco.00084", + "disco.00085", + "disco.00086", + "disco.00087", + "disco.00088", + "disco.00091", + "disco.00092", + "disco.00093", + "disco.00094", + "disco.00096", + "disco.00097", + "disco.00099", + "hiphop.00000", + "hiphop.00026", + "hiphop.00027", + "hiphop.00030", + "hiphop.00040", + "hiphop.00043", + "hiphop.00044", + "hiphop.00045", + "hiphop.00051", + "hiphop.00052", + "hiphop.00053", + "hiphop.00054", + "hiphop.00062", + "hiphop.00063", + "hiphop.00064", + "hiphop.00065", + "hiphop.00066", + "hiphop.00067", + "hiphop.00068", + "hiphop.00069", + "hiphop.00070", + "hiphop.00071", + "hiphop.00072", + "hiphop.00073", + "hiphop.00074", + "hiphop.00075", + "hiphop.00099", + "jazz.00073", + "jazz.00074", + "jazz.00075", + "jazz.00076", + "jazz.00077", + "jazz.00078", + "jazz.00079", + "jazz.00080", + "jazz.00081", + "jazz.00082", + "jazz.00083", + "jazz.00084", + "jazz.00085", + "jazz.00086", + "jazz.00087", + "jazz.00088", + "jazz.00089", + "jazz.00090", + "jazz.00091", + "jazz.00092", + "jazz.00093", + "jazz.00094", + "jazz.00095", + "jazz.00096", + "jazz.00097", + "jazz.00098", + "jazz.00099", + "metal.00012", + "metal.00013", + "metal.00014", + "metal.00015", + "metal.00022", + "metal.00023", + "metal.00025", + "metal.00026", + "metal.00027", + "metal.00028", + "metal.00029", + "metal.00030", + "metal.00031", + "metal.00032", + "metal.00033", + "metal.00038", + "metal.00039", + "metal.00067", + "metal.00070", + "metal.00073", + "metal.00074", + "metal.00075", + "metal.00078", + "metal.00083", + "metal.00085", + "metal.00087", + "metal.00088", + "pop.00000", + "pop.00001", + "pop.00013", + "pop.00014", + "pop.00043", + "pop.00063", + "pop.00064", + "pop.00065", + "pop.00066", + "pop.00069", + "pop.00070", + "pop.00071", + "pop.00072", + "pop.00073", + "pop.00074", + "pop.00075", + "pop.00076", + "pop.00077", + "pop.00078", + "pop.00079", + "pop.00082", + "pop.00088", + "pop.00089", + "pop.00090", + "pop.00091", + "pop.00092", + "pop.00093", + "pop.00094", + "pop.00095", + "pop.00096", + "reggae.00034", + "reggae.00035", + "reggae.00036", + "reggae.00037", + "reggae.00038", + "reggae.00039", + "reggae.00040", + "reggae.00046", + "reggae.00047", + "reggae.00048", + "reggae.00052", + "reggae.00053", + "reggae.00064", + "reggae.00065", + "reggae.00066", + "reggae.00067", + "reggae.00068", + "reggae.00071", + "reggae.00079", + "reggae.00082", + "reggae.00083", + "reggae.00084", + "reggae.00087", + "reggae.00088", + "reggae.00089", + "reggae.00090", + "rock.00010", + "rock.00011", + "rock.00012", + "rock.00013", + "rock.00014", + "rock.00015", + "rock.00027", + "rock.00028", + "rock.00029", + "rock.00030", + "rock.00031", + "rock.00032", + "rock.00033", + "rock.00034", + "rock.00035", + "rock.00036", + "rock.00037", + "rock.00039", + "rock.00040", + "rock.00041", + "rock.00042", + "rock.00043", + "rock.00044", + "rock.00045", + "rock.00046", + "rock.00047", + "rock.00048", + "rock.00086", + "rock.00087", + "rock.00088", + "rock.00089", + "rock.00090", +] + +filtered_train = [ + "blues.00029", + "blues.00030", + "blues.00031", + "blues.00032", + "blues.00033", + "blues.00034", + "blues.00035", + "blues.00036", + "blues.00037", + "blues.00038", + "blues.00039", + "blues.00040", + "blues.00041", + "blues.00042", + "blues.00043", + "blues.00044", + "blues.00045", + "blues.00046", + "blues.00047", + "blues.00048", + "blues.00049", + "blues.00073", + "blues.00074", + "blues.00075", + "blues.00076", + "blues.00077", + "blues.00078", + "blues.00079", + "blues.00080", + "blues.00081", + "blues.00082", + "blues.00083", + "blues.00084", + "blues.00085", + "blues.00086", + "blues.00087", + "blues.00088", + "blues.00089", + "blues.00090", + "blues.00091", + "blues.00092", + "blues.00093", + "blues.00094", + "blues.00095", + "blues.00096", + "blues.00097", + "classical.00030", + "classical.00031", + "classical.00032", + "classical.00033", + "classical.00043", + "classical.00044", + "classical.00045", + "classical.00046", + "classical.00047", + "classical.00048", + "classical.00050", + "classical.00051", + "classical.00052", + "classical.00053", + "classical.00054", + "classical.00055", + "classical.00056", + "classical.00057", + "classical.00058", + "classical.00059", + "classical.00060", + "classical.00061", + "classical.00062", + "classical.00063", + "classical.00064", + "classical.00065", + "classical.00066", + "classical.00067", + "classical.00080", + "classical.00081", + "classical.00082", + "classical.00083", + "classical.00084", + "classical.00085", + "classical.00086", + "classical.00087", + "classical.00088", + "classical.00089", + "classical.00090", + "classical.00091", + "classical.00092", + "classical.00093", + "classical.00094", + "classical.00095", + "classical.00096", + "classical.00097", + "classical.00098", + "classical.00099", + "country.00019", + "country.00020", + "country.00021", + "country.00022", + "country.00023", + "country.00024", + "country.00025", + "country.00026", + "country.00028", + "country.00029", + "country.00065", + "country.00066", + "country.00067", + "country.00068", + "country.00069", + "country.00070", + "country.00071", + "country.00072", + "country.00073", + "country.00074", + "country.00075", + "country.00076", + "country.00077", + "country.00078", + "country.00079", + "country.00080", + "country.00081", + "country.00082", + "country.00083", + "country.00084", + "country.00085", + "country.00086", + "country.00087", + "country.00088", + "country.00089", + "country.00090", + "country.00091", + "country.00092", + "country.00093", + "country.00094", + "country.00095", + "country.00096", + "country.00097", + "country.00098", + "country.00099", + "disco.00005", + "disco.00015", + "disco.00016", + "disco.00017", + "disco.00018", + "disco.00019", + "disco.00020", + "disco.00022", + "disco.00023", + "disco.00024", + "disco.00025", + "disco.00026", + "disco.00027", + "disco.00028", + "disco.00029", + "disco.00030", + "disco.00031", + "disco.00032", + "disco.00033", + "disco.00034", + "disco.00035", + "disco.00036", + "disco.00037", + "disco.00039", + "disco.00040", + "disco.00041", + "disco.00042", + "disco.00043", + "disco.00044", + "disco.00045", + "disco.00047", + "disco.00049", + "disco.00053", + "disco.00054", + "disco.00056", + "disco.00057", + "disco.00059", + "disco.00061", + "disco.00070", + "disco.00073", + "disco.00074", + "disco.00089", + "hiphop.00002", + "hiphop.00003", + "hiphop.00004", + "hiphop.00005", + "hiphop.00006", + "hiphop.00007", + "hiphop.00008", + "hiphop.00009", + "hiphop.00010", + "hiphop.00011", + "hiphop.00012", + "hiphop.00013", + "hiphop.00014", + "hiphop.00015", + "hiphop.00016", + "hiphop.00017", + "hiphop.00018", + "hiphop.00019", + "hiphop.00020", + "hiphop.00021", + "hiphop.00022", + "hiphop.00023", + "hiphop.00024", + "hiphop.00025", + "hiphop.00028", + "hiphop.00029", + "hiphop.00031", + "hiphop.00032", + "hiphop.00033", + "hiphop.00034", + "hiphop.00035", + "hiphop.00036", + "hiphop.00037", + "hiphop.00038", + "hiphop.00041", + "hiphop.00042", + "hiphop.00055", + "hiphop.00056", + "hiphop.00057", + "hiphop.00058", + "hiphop.00059", + "hiphop.00060", + "hiphop.00061", + "hiphop.00077", + "hiphop.00078", + "hiphop.00079", + "hiphop.00080", + "jazz.00000", + "jazz.00001", + "jazz.00011", + "jazz.00012", + "jazz.00013", + "jazz.00014", + "jazz.00015", + "jazz.00016", + "jazz.00017", + "jazz.00018", + "jazz.00019", + "jazz.00020", + "jazz.00021", + "jazz.00022", + "jazz.00023", + "jazz.00024", + "jazz.00041", + "jazz.00047", + "jazz.00048", + "jazz.00049", + "jazz.00050", + "jazz.00051", + "jazz.00052", + "jazz.00053", + "jazz.00054", + "jazz.00055", + "jazz.00056", + "jazz.00057", + "jazz.00058", + "jazz.00059", + "jazz.00060", + "jazz.00061", + "jazz.00062", + "jazz.00063", + "jazz.00064", + "jazz.00065", + "jazz.00066", + "jazz.00067", + "jazz.00068", + "jazz.00069", + "jazz.00070", + "jazz.00071", + "jazz.00072", + "metal.00002", + "metal.00003", + "metal.00005", + "metal.00021", + "metal.00024", + "metal.00035", + "metal.00046", + "metal.00047", + "metal.00048", + "metal.00049", + "metal.00050", + "metal.00051", + "metal.00052", + "metal.00053", + "metal.00054", + "metal.00055", + "metal.00056", + "metal.00057", + "metal.00059", + "metal.00060", + "metal.00061", + "metal.00062", + "metal.00063", + "metal.00064", + "metal.00065", + "metal.00066", + "metal.00069", + "metal.00071", + "metal.00072", + "metal.00079", + "metal.00080", + "metal.00084", + "metal.00086", + "metal.00089", + "metal.00090", + "metal.00091", + "metal.00092", + "metal.00093", + "metal.00094", + "metal.00095", + "metal.00096", + "metal.00097", + "metal.00098", + "metal.00099", + "pop.00002", + "pop.00003", + "pop.00004", + "pop.00005", + "pop.00006", + "pop.00007", + "pop.00008", + "pop.00009", + "pop.00011", + "pop.00012", + "pop.00016", + "pop.00017", + "pop.00018", + "pop.00019", + "pop.00020", + "pop.00023", + "pop.00024", + "pop.00025", + "pop.00026", + "pop.00027", + "pop.00028", + "pop.00029", + "pop.00031", + "pop.00032", + "pop.00033", + "pop.00034", + "pop.00035", + "pop.00036", + "pop.00038", + "pop.00039", + "pop.00040", + "pop.00041", + "pop.00042", + "pop.00044", + "pop.00046", + "pop.00049", + "pop.00050", + "pop.00080", + "pop.00097", + "pop.00098", + "pop.00099", + "reggae.00000", + "reggae.00001", + "reggae.00002", + "reggae.00004", + "reggae.00006", + "reggae.00009", + "reggae.00011", + "reggae.00012", + "reggae.00014", + "reggae.00015", + "reggae.00016", + "reggae.00017", + "reggae.00018", + "reggae.00019", + "reggae.00020", + "reggae.00021", + "reggae.00022", + "reggae.00023", + "reggae.00024", + "reggae.00025", + "reggae.00026", + "reggae.00027", + "reggae.00028", + "reggae.00029", + "reggae.00030", + "reggae.00031", + "reggae.00032", + "reggae.00042", + "reggae.00043", + "reggae.00044", + "reggae.00045", + "reggae.00049", + "reggae.00050", + "reggae.00051", + "reggae.00054", + "reggae.00055", + "reggae.00056", + "reggae.00057", + "reggae.00058", + "reggae.00059", + "reggae.00060", + "reggae.00063", + "reggae.00069", + "rock.00000", + "rock.00001", + "rock.00002", + "rock.00003", + "rock.00004", + "rock.00005", + "rock.00006", + "rock.00007", + "rock.00008", + "rock.00009", + "rock.00016", + "rock.00017", + "rock.00018", + "rock.00019", + "rock.00020", + "rock.00021", + "rock.00022", + "rock.00023", + "rock.00024", + "rock.00025", + "rock.00026", + "rock.00057", + "rock.00058", + "rock.00059", + "rock.00060", + "rock.00061", + "rock.00062", + "rock.00063", + "rock.00064", + "rock.00065", + "rock.00066", + "rock.00067", + "rock.00068", + "rock.00069", + "rock.00070", + "rock.00091", + "rock.00092", + "rock.00093", + "rock.00094", + "rock.00095", + "rock.00096", + "rock.00097", + "rock.00098", + "rock.00099", +] + +filtered_valid = [ + "blues.00000", + "blues.00001", + "blues.00002", + "blues.00003", + "blues.00004", + "blues.00005", + "blues.00006", + "blues.00007", + "blues.00008", + "blues.00009", + "blues.00010", + "blues.00011", + "blues.00050", + "blues.00051", + "blues.00052", + "blues.00053", + "blues.00054", + "blues.00055", + "blues.00056", + "blues.00057", + "blues.00058", + "blues.00059", + "blues.00060", + "classical.00000", + "classical.00001", + "classical.00002", + "classical.00003", + "classical.00004", + "classical.00005", + "classical.00006", + "classical.00007", + "classical.00008", + "classical.00009", + "classical.00010", + "classical.00068", + "classical.00069", + "classical.00070", + "classical.00071", + "classical.00072", + "classical.00073", + "classical.00074", + "classical.00075", + "classical.00076", + "country.00000", + "country.00001", + "country.00002", + "country.00003", + "country.00004", + "country.00005", + "country.00006", + "country.00007", + "country.00009", + "country.00010", + "country.00011", + "country.00012", + "country.00013", + "country.00014", + "country.00015", + "country.00016", + "country.00017", + "country.00018", + "country.00027", + "country.00041", + "country.00042", + "country.00045", + "country.00049", + "disco.00000", + "disco.00002", + "disco.00003", + "disco.00004", + "disco.00006", + "disco.00007", + "disco.00008", + "disco.00009", + "disco.00010", + "disco.00011", + "disco.00012", + "disco.00013", + "disco.00014", + "disco.00046", + "disco.00048", + "disco.00052", + "disco.00067", + "disco.00068", + "disco.00072", + "disco.00075", + "disco.00090", + "disco.00095", + "hiphop.00081", + "hiphop.00082", + "hiphop.00083", + "hiphop.00084", + "hiphop.00085", + "hiphop.00086", + "hiphop.00087", + "hiphop.00088", + "hiphop.00089", + "hiphop.00090", + "hiphop.00091", + "hiphop.00092", + "hiphop.00093", + "hiphop.00094", + "hiphop.00095", + "hiphop.00096", + "hiphop.00097", + "hiphop.00098", + "jazz.00002", + "jazz.00003", + "jazz.00004", + "jazz.00005", + "jazz.00006", + "jazz.00007", + "jazz.00008", + "jazz.00009", + "jazz.00010", + "jazz.00025", + "jazz.00026", + "jazz.00027", + "jazz.00028", + "jazz.00029", + "jazz.00030", + "jazz.00031", + "jazz.00032", + "metal.00000", + "metal.00001", + "metal.00006", + "metal.00007", + "metal.00008", + "metal.00009", + "metal.00010", + "metal.00011", + "metal.00016", + "metal.00017", + "metal.00018", + "metal.00019", + "metal.00020", + "metal.00036", + "metal.00037", + "metal.00068", + "metal.00076", + "metal.00077", + "metal.00081", + "metal.00082", + "pop.00010", + "pop.00053", + "pop.00055", + "pop.00058", + "pop.00059", + "pop.00060", + "pop.00061", + "pop.00062", + "pop.00081", + "pop.00083", + "pop.00084", + "pop.00085", + "pop.00086", + "reggae.00061", + "reggae.00062", + "reggae.00070", + "reggae.00072", + "reggae.00074", + "reggae.00076", + "reggae.00077", + "reggae.00078", + "reggae.00085", + "reggae.00092", + "reggae.00093", + "reggae.00094", + "reggae.00095", + "reggae.00096", + "reggae.00097", + "reggae.00098", + "reggae.00099", + "rock.00038", + "rock.00049", + "rock.00050", + "rock.00051", + "rock.00052", + "rock.00053", + "rock.00054", + "rock.00055", + "rock.00056", + "rock.00071", + "rock.00072", + "rock.00073", + "rock.00074", + "rock.00075", + "rock.00076", + "rock.00077", + "rock.00078", + "rock.00079", + "rock.00080", + "rock.00081", + "rock.00082", + "rock.00083", + "rock.00084", + "rock.00085", +] + + +URL = "http://opihi.cs.uvic.ca/sound/genres.tar.gz" +FOLDER_IN_ARCHIVE = "genres" +_CHECKSUMS = { + "http://opihi.cs.uvic.ca/sound/genres.tar.gz": "24347e0223d2ba798e0a558c4c172d9d4a19c00bb7963fe055d183dadb4ef2c6" +} + + +def load_gtzan_item(fileid: str, path: str, ext_audio: str) -> Tuple[Tensor, str]: + """ + Loads a file from the dataset and returns the raw waveform + as a Torch Tensor, its sample rate as an integer, and its + genre as a string. + """ + # Filenames are of the form label.id, e.g. blues.00078 + label, _ = fileid.split(".") + + # Read wav + file_audio = os.path.join(path, label, fileid + ext_audio) + waveform, sample_rate = torchaudio.load(file_audio) + + return waveform, sample_rate, label + + +class GTZAN(Dataset): + """*GTZAN* :cite:`tzanetakis_essl_cook_2001` dataset. + + Note: + Please see http://marsyas.info/downloads/datasets.html if you are planning to use + this dataset to publish results. + + Note: + As of October 2022, the download link is not currently working. Setting ``download=True`` + in GTZAN dataset will result in a URL connection error. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + url (str, optional): The URL to download the dataset from. + (default: ``"http://opihi.cs.uvic.ca/sound/genres.tar.gz"``) + folder_in_archive (str, optional): The top-level directory of the dataset. + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + subset (str or None, optional): Which subset of the dataset to use. + One of ``"training"``, ``"validation"``, ``"testing"`` or ``None``. + If ``None``, the entire dataset is used. (default: ``None``). + """ + + _ext_audio = ".wav" + + def __init__( + self, + root: Union[str, Path], + url: str = URL, + folder_in_archive: str = FOLDER_IN_ARCHIVE, + download: bool = False, + subset: Optional[str] = None, + ) -> None: + + # super(GTZAN, self).__init__() + + # Get string representation of 'root' in case Path object is passed + root = os.fspath(root) + + self.root = root + self.url = url + self.folder_in_archive = folder_in_archive + self.download = download + self.subset = subset + + if subset is not None and subset not in ["training", "validation", "testing"]: + raise ValueError("When `subset` is not None, it must be one of ['training', 'validation', 'testing'].") + + archive = os.path.basename(url) + archive = os.path.join(root, archive) + self._path = os.path.join(root, folder_in_archive) + + if download: + if not os.path.isdir(self._path): + if not os.path.isfile(archive): + checksum = _CHECKSUMS.get(url, None) + download_url_to_file(url, archive, hash_prefix=checksum) + _extract_tar(archive) + + if not os.path.isdir(self._path): + raise RuntimeError("Dataset not found. Please use `download=True` to download it.") + + if self.subset is None: + # Check every subdirectory under dataset root + # which has the same name as the genres in + # GTZAN (e.g. `root_dir'/blues/, `root_dir'/rock, etc.) + # This lets users remove or move around song files, + # useful when e.g. they want to use only some of the files + # in a genre or want to label other files with a different + # genre. + self._walker = [] + + root = os.path.expanduser(self._path) + + for directory in gtzan_genres: + fulldir = os.path.join(root, directory) + + if not os.path.exists(fulldir): + continue + + songs_in_genre = os.listdir(fulldir) + songs_in_genre.sort() + for fname in songs_in_genre: + name, ext = os.path.splitext(fname) + if ext.lower() == ".wav" and "." in name: + # Check whether the file is of the form + # `gtzan_genre`.`5 digit number`.wav + genre, num = name.split(".") + if genre in gtzan_genres and len(num) == 5 and num.isdigit(): + self._walker.append(name) + else: + if self.subset == "training": + self._walker = filtered_train + elif self.subset == "validation": + self._walker = filtered_valid + elif self.subset == "testing": + self._walker = filtered_test + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + Label + """ + fileid = self._walker[n] + item = load_gtzan_item(fileid, self._path, self._ext_audio) + waveform, sample_rate, label = item + return waveform, sample_rate, label + + def __len__(self) -> int: + return len(self._walker) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/iemocap.py b/lib/python3.12/site-packages/torchaudio/datasets/iemocap.py new file mode 100644 index 0000000000000000000000000000000000000000..224300a84f5ec3ae217f030783c825fc3db56c8a --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/iemocap.py @@ -0,0 +1,147 @@ +import os +import re +from pathlib import Path +from typing import Optional, Tuple, Union + +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio.datasets.utils import _load_waveform + + +_SAMPLE_RATE = 16000 + + +def _get_wavs_paths(data_dir): + wav_dir = data_dir / "sentences" / "wav" + wav_paths = sorted(str(p) for p in wav_dir.glob("*/*.wav")) + relative_paths = [] + for wav_path in wav_paths: + start = wav_path.find("Session") + wav_path = wav_path[start:] + relative_paths.append(wav_path) + return relative_paths + + +class IEMOCAP(Dataset): + """*IEMOCAP* :cite:`iemocap` dataset. + + Args: + root (str or Path): Root directory where the dataset's top level directory is found + sessions (Tuple[int]): Tuple of sessions (1-5) to use. (Default: ``(1, 2, 3, 4, 5)``) + utterance_type (str or None, optional): Which type(s) of utterances to include in the dataset. + Options: ("scripted", "improvised", ``None``). If ``None``, both scripted and improvised + data are used. + """ + + def __init__( + self, + root: Union[str, Path], + sessions: Tuple[str] = (1, 2, 3, 4, 5), + utterance_type: Optional[str] = None, + ): + root = Path(root) + self._path = root / "IEMOCAP" + + if not os.path.isdir(self._path): + raise RuntimeError("Dataset not found.") + + if utterance_type not in ["scripted", "improvised", None]: + raise ValueError("utterance_type must be one of ['scripted', 'improvised', or None]") + + all_data = [] + self.data = [] + self.mapping = {} + + for session in sessions: + session_name = f"Session{session}" + session_dir = self._path / session_name + + # get wav paths + wav_paths = _get_wavs_paths(session_dir) + for wav_path in wav_paths: + wav_stem = str(Path(wav_path).stem) + all_data.append(wav_stem) + + # add labels + label_dir = session_dir / "dialog" / "EmoEvaluation" + query = "*.txt" + if utterance_type == "scripted": + query = "*script*.txt" + elif utterance_type == "improvised": + query = "*impro*.txt" + label_paths = label_dir.glob(query) + + for label_path in label_paths: + with open(label_path, "r") as f: + for line in f: + if not line.startswith("["): + continue + line = re.split("[\t\n]", line) + wav_stem = line[1] + label = line[2] + if wav_stem not in all_data: + continue + if label not in ["neu", "hap", "ang", "sad", "exc", "fru"]: + continue + self.mapping[wav_stem] = {} + self.mapping[wav_stem]["label"] = label + + for wav_path in wav_paths: + wav_stem = str(Path(wav_path).stem) + if wav_stem in self.mapping: + self.data.append(wav_stem) + self.mapping[wav_stem]["path"] = wav_path + + def get_metadata(self, n: int) -> Tuple[str, int, str, str, str]: + """Get metadata for the n-th sample from the dataset. Returns filepath instead of waveform, + but otherwise returns the same fields as :py:meth:`__getitem__`. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + str: + Path to audio + int: + Sample rate + str: + File name + str: + Label (one of ``"neu"``, ``"hap"``, ``"ang"``, ``"sad"``, ``"exc"``, ``"fru"``) + str: + Speaker + """ + wav_stem = self.data[n] + wav_path = self.mapping[wav_stem]["path"] + label = self.mapping[wav_stem]["label"] + speaker = wav_stem.split("_")[0] + return (wav_path, _SAMPLE_RATE, wav_stem, label, speaker) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str, str, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + File name + str: + Label (one of ``"neu"``, ``"hap"``, ``"ang"``, ``"sad"``, ``"exc"``, ``"fru"``) + str: + Speaker + """ + metadata = self.get_metadata(n) + waveform = _load_waveform(self._path, metadata[0], metadata[1]) + return (waveform,) + metadata[1:] + + def __len__(self): + return len(self.data) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/librilight_limited.py b/lib/python3.12/site-packages/torchaudio/datasets/librilight_limited.py new file mode 100644 index 0000000000000000000000000000000000000000..f0cb3100f7c4ad2e488c20bdfaac3833e0a136dd --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/librilight_limited.py @@ -0,0 +1,111 @@ +import os +from pathlib import Path +from typing import List, Tuple, Union + +import torchaudio +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.librispeech import _get_librispeech_metadata +from torchaudio.datasets.utils import _extract_tar + + +_ARCHIVE_NAME = "librispeech_finetuning" +_URL = "https://dl.fbaipublicfiles.com/librilight/data/librispeech_finetuning.tgz" +_CHECKSUM = "5d1efdc777b548194d7e09ba89126e2188026df9fd57aa57eb14408d2b2342af" +_SUBSET_MAP = {"10min": ["1h/0"], "1h": ["1h/*"], "10h": ["1h/*", "9h"]} + + +def _get_fileids_paths(path: Path, folders: List[str], _ext_audio: str) -> List[Tuple[str, str]]: + """Get the file names and the corresponding file paths without `speaker_id` + and `chapter_id` directories. + The format of path is like: + {root}/{_ARCHIVE_NAME}/1h/[0-5]/[clean, other] or + {root}/{_ARCHIVE_NAME}/9h/[clean, other] + + Args: + path (Path): Root path to the dataset. + folders (List[str]): Folders that contain the desired audio files. + _ext_audio (str): Extension of audio files. + + Returns: + List[Tuple[str, str]]: + List of tuples where the first element is the relative path to the audio file. + The format of relative path is like: + 1h/[0-5]/[clean, other] or 9h/[clean, other] + The second element is the file name without audio extension. + """ + + path = Path(path) + files_paths = [] + for folder in folders: + paths = [p.relative_to(path) for p in path.glob(f"{folder}/*/*/*/*{_ext_audio}")] + files_paths += [(str(p.parent.parent.parent), str(p.stem)) for p in paths] # get subset folder and file name + files_paths.sort(key=lambda x: x[0] + x[1]) + return files_paths + + +class LibriLightLimited(Dataset): + """Subset of Libri-light :cite:`librilight` dataset, + which was used in HuBERT :cite:`hsu2021hubert` for supervised fine-tuning. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + subset (str, optional): The subset to use. Options: [``"10min"``, ``"1h"``, ``"10h"``] + (Default: ``"10min"``). + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + """ + + _ext_txt = ".trans.txt" + _ext_audio = ".flac" + + def __init__( + self, + root: Union[str, Path], + subset: str = "10min", + download: bool = False, + ) -> None: + if subset not in _SUBSET_MAP: + raise ValueError(f"`subset` must be one of {_SUBSET_MAP.keys()}. Found: {subset}") + folders = _SUBSET_MAP[subset] + + root = os.fspath(root) + self._path = os.path.join(root, _ARCHIVE_NAME) + archive = os.path.join(root, f"{_ARCHIVE_NAME}.tgz") + if not os.path.isdir(self._path): + if not download: + raise RuntimeError("Dataset not found. Please use `download=True` to download") + if not os.path.isfile(archive): + download_url_to_file(_URL, archive, hash_prefix=_CHECKSUM) + _extract_tar(archive) + self._fileids_paths = _get_fileids_paths(self._path, folders, self._ext_audio) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str, int, int, int]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + Transcript + int: + Speaker ID + int: + Chapter ID + int: + Utterance ID + """ + file_path, fileid = self._fileids_paths[n] + metadata = _get_librispeech_metadata(fileid, self._path, file_path, self._ext_audio, self._ext_txt) + waveform, _ = torchaudio.load(os.path.join(self._path, metadata[0])) + return (waveform,) + metadata[1:] + + def __len__(self) -> int: + return len(self._fileids_paths) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/librimix.py b/lib/python3.12/site-packages/torchaudio/datasets/librimix.py new file mode 100644 index 0000000000000000000000000000000000000000..2c6c6f18600ab35f037dda11f9f5bc32c8a5cbf5 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/librimix.py @@ -0,0 +1,133 @@ +import os +from pathlib import Path +from typing import List, Tuple, Union + +import torch +from torch.utils.data import Dataset +from torchaudio.datasets.utils import _load_waveform + +_TASKS_TO_MIXTURE = { + "sep_clean": "mix_clean", + "enh_single": "mix_single", + "enh_both": "mix_both", + "sep_noisy": "mix_both", +} + + +class LibriMix(Dataset): + r"""*LibriMix* :cite:`cosentino2020librimix` dataset. + + Args: + root (str or Path): The path where the directory ``Libri2Mix`` or + ``Libri3Mix`` is stored. Not the path of those directories. + subset (str, optional): The subset to use. Options: [``"train-360"``, ``"train-100"``, + ``"dev"``, and ``"test"``] (Default: ``"train-360"``). + num_speakers (int, optional): The number of speakers, which determines the directories + to traverse. The Dataset will traverse ``s1`` to ``sN`` directories to collect + N source audios. (Default: 2) + sample_rate (int, optional): Sample rate of audio files. The ``sample_rate`` determines + which subdirectory the audio are fetched. If any of the audio has a different sample + rate, raises ``ValueError``. Options: [8000, 16000] (Default: 8000) + task (str, optional): The task of LibriMix. + Options: [``"enh_single"``, ``"enh_both"``, ``"sep_clean"``, ``"sep_noisy"``] + (Default: ``"sep_clean"``) + mode (str, optional): The mode when creating the mixture. If set to ``"min"``, the lengths of mixture + and sources are the minimum length of all sources. If set to ``"max"``, the lengths of mixture and + sources are zero padded to the maximum length of all sources. + Options: [``"min"``, ``"max"``] + (Default: ``"min"``) + + Note: + The LibriMix dataset needs to be manually generated. Please check https://github.com/JorisCos/LibriMix + """ + + def __init__( + self, + root: Union[str, Path], + subset: str = "train-360", + num_speakers: int = 2, + sample_rate: int = 8000, + task: str = "sep_clean", + mode: str = "min", + ): + self.root = Path(root) / f"Libri{num_speakers}Mix" + if not os.path.exists(self.root): + raise RuntimeError( + f"The path {self.root} doesn't exist. " + "Please check the ``root`` path and ``num_speakers`` or download the dataset manually." + ) + if mode not in ["max", "min"]: + raise ValueError(f'Expect ``mode`` to be one in ["min", "max"]. Found {mode}.') + if sample_rate == 8000: + mix_dir = self.root / "wav8k" / mode / subset + elif sample_rate == 16000: + mix_dir = self.root / "wav16k" / mode / subset + else: + raise ValueError(f"Unsupported sample rate. Found {sample_rate}.") + self.sample_rate = sample_rate + self.task = task + + self.mix_dir = mix_dir / _TASKS_TO_MIXTURE[task] + if task == "enh_both": + self.src_dirs = [(mix_dir / "mix_clean")] + else: + self.src_dirs = [(mix_dir / f"s{i+1}") for i in range(num_speakers)] + + self.files = [p.name for p in self.mix_dir.glob("*.wav")] + self.files.sort() + + def _load_sample(self, key) -> Tuple[int, torch.Tensor, List[torch.Tensor]]: + metadata = self.get_metadata(key) + mixed = _load_waveform(self.root, metadata[1], metadata[0]) + srcs = [] + for i, path_ in enumerate(metadata[2]): + src = _load_waveform(self.root, path_, metadata[0]) + if mixed.shape != src.shape: + raise ValueError(f"Different waveform shapes. mixed: {mixed.shape}, src[{i}]: {src.shape}") + srcs.append(src) + return self.sample_rate, mixed, srcs + + def get_metadata(self, key: int) -> Tuple[int, str, List[str]]: + """Get metadata for the n-th sample from the dataset. + + Args: + key (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + int: + Sample rate + str: + Path to mixed audio + List of str: + List of paths to source audios + """ + filename = self.files[key] + mixed_path = os.path.relpath(self.mix_dir / filename, self.root) + srcs_paths = [] + for dir_ in self.src_dirs: + src = os.path.relpath(dir_ / filename, self.root) + srcs_paths.append(src) + return self.sample_rate, mixed_path, srcs_paths + + def __len__(self) -> int: + return len(self.files) + + def __getitem__(self, key: int) -> Tuple[int, torch.Tensor, List[torch.Tensor]]: + """Load the n-th sample from the dataset. + + Args: + key (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + int: + Sample rate + Tensor: + Mixture waveform + List of Tensors: + List of source waveforms + """ + return self._load_sample(key) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/librispeech.py b/lib/python3.12/site-packages/torchaudio/datasets/librispeech.py new file mode 100644 index 0000000000000000000000000000000000000000..7cf05dbecb5cce24c91e3bbcf232935e1f6d8cd9 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/librispeech.py @@ -0,0 +1,174 @@ +import os +from pathlib import Path +from typing import Tuple, Union + +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_tar, _load_waveform + +URL = "train-clean-100" +FOLDER_IN_ARCHIVE = "LibriSpeech" +SAMPLE_RATE = 16000 +_DATA_SUBSETS = [ + "dev-clean", + "dev-other", + "test-clean", + "test-other", + "train-clean-100", + "train-clean-360", + "train-other-500", +] +_CHECKSUMS = { + "http://www.openslr.org/resources/12/dev-clean.tar.gz": "76f87d090650617fca0cac8f88b9416e0ebf80350acb97b343a85fa903728ab3", # noqa: E501 + "http://www.openslr.org/resources/12/dev-other.tar.gz": "12661c48e8c3fe1de2c1caa4c3e135193bfb1811584f11f569dd12645aa84365", # noqa: E501 + "http://www.openslr.org/resources/12/test-clean.tar.gz": "39fde525e59672dc6d1551919b1478f724438a95aa55f874b576be21967e6c23", # noqa: E501 + "http://www.openslr.org/resources/12/test-other.tar.gz": "d09c181bba5cf717b3dee7d4d592af11a3ee3a09e08ae025c5506f6ebe961c29", # noqa: E501 + "http://www.openslr.org/resources/12/train-clean-100.tar.gz": "d4ddd1d5a6ab303066f14971d768ee43278a5f2a0aa43dc716b0e64ecbbbf6e2", # noqa: E501 + "http://www.openslr.org/resources/12/train-clean-360.tar.gz": "146a56496217e96c14334a160df97fffedd6e0a04e66b9c5af0d40be3c792ecf", # noqa: E501 + "http://www.openslr.org/resources/12/train-other-500.tar.gz": "ddb22f27f96ec163645d53215559df6aa36515f26e01dd70798188350adcb6d2", # noqa: E501 +} + + +def _download_librispeech(root, url): + base_url = "http://www.openslr.org/resources/12/" + ext_archive = ".tar.gz" + + filename = url + ext_archive + archive = os.path.join(root, filename) + download_url = os.path.join(base_url, filename) + if not os.path.isfile(archive): + checksum = _CHECKSUMS.get(download_url, None) + download_url_to_file(download_url, archive, hash_prefix=checksum) + _extract_tar(archive) + + +def _get_librispeech_metadata( + fileid: str, root: str, folder: str, ext_audio: str, ext_txt: str +) -> Tuple[str, int, str, int, int, int]: + speaker_id, chapter_id, utterance_id = fileid.split("-") + + # Get audio path and sample rate + fileid_audio = f"{speaker_id}-{chapter_id}-{utterance_id}" + filepath = os.path.join(folder, speaker_id, chapter_id, f"{fileid_audio}{ext_audio}") + + # Load text + file_text = f"{speaker_id}-{chapter_id}{ext_txt}" + file_text = os.path.join(root, folder, speaker_id, chapter_id, file_text) + with open(file_text) as ft: + for line in ft: + fileid_text, transcript = line.strip().split(" ", 1) + if fileid_audio == fileid_text: + break + else: + # Translation not found + raise FileNotFoundError(f"Translation not found for {fileid_audio}") + + return ( + filepath, + SAMPLE_RATE, + transcript, + int(speaker_id), + int(chapter_id), + int(utterance_id), + ) + + +class LIBRISPEECH(Dataset): + """*LibriSpeech* :cite:`7178964` dataset. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + url (str, optional): The URL to download the dataset from, + or the type of the dataset to dowload. + Allowed type values are ``"dev-clean"``, ``"dev-other"``, ``"test-clean"``, + ``"test-other"``, ``"train-clean-100"``, ``"train-clean-360"`` and + ``"train-other-500"``. (default: ``"train-clean-100"``) + folder_in_archive (str, optional): + The top-level directory of the dataset. (default: ``"LibriSpeech"``) + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + """ + + _ext_txt = ".trans.txt" + _ext_audio = ".flac" + + def __init__( + self, + root: Union[str, Path], + url: str = URL, + folder_in_archive: str = FOLDER_IN_ARCHIVE, + download: bool = False, + ) -> None: + self._url = url + if url not in _DATA_SUBSETS: + raise ValueError(f"Invalid url '{url}' given; please provide one of {_DATA_SUBSETS}.") + + root = os.fspath(root) + self._archive = os.path.join(root, folder_in_archive) + self._path = os.path.join(root, folder_in_archive, url) + + if not os.path.isdir(self._path): + if download: + _download_librispeech(root, url) + else: + raise RuntimeError( + f"Dataset not found at {self._path}. Please set `download=True` to download the dataset." + ) + + self._walker = sorted(str(p.stem) for p in Path(self._path).glob("*/*/*" + self._ext_audio)) + + def get_metadata(self, n: int) -> Tuple[str, int, str, int, int, int]: + """Get metadata for the n-th sample from the dataset. Returns filepath instead of waveform, + but otherwise returns the same fields as :py:func:`__getitem__`. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + str: + Path to audio + int: + Sample rate + str: + Transcript + int: + Speaker ID + int: + Chapter ID + int: + Utterance ID + """ + fileid = self._walker[n] + return _get_librispeech_metadata(fileid, self._archive, self._url, self._ext_audio, self._ext_txt) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str, int, int, int]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + Transcript + int: + Speaker ID + int: + Chapter ID + int: + Utterance ID + """ + metadata = self.get_metadata(n) + waveform = _load_waveform(self._archive, metadata[0], metadata[1]) + return (waveform,) + metadata[1:] + + def __len__(self) -> int: + return len(self._walker) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/librispeech_biasing.py b/lib/python3.12/site-packages/torchaudio/datasets/librispeech_biasing.py new file mode 100644 index 0000000000000000000000000000000000000000..bd518cf2b69094728f8693fe2cb8a2a535bd7d3c --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/librispeech_biasing.py @@ -0,0 +1,189 @@ +import os +from pathlib import Path +from typing import List, Tuple, Union + +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_tar, _load_waveform + +URL = "train-clean-100" +FOLDER_IN_ARCHIVE = "LibriSpeech" +SAMPLE_RATE = 16000 +_DATA_SUBSETS = [ + "dev-clean", + "dev-other", + "test-clean", + "test-other", + "train-clean-100", + "train-clean-360", + "train-other-500", +] +_CHECKSUMS = { + "http://www.openslr.org/resources/12/dev-clean.tar.gz": "76f87d090650617fca0cac8f88b9416e0ebf80350acb97b343a85fa903728ab3", # noqa: E501 + "http://www.openslr.org/resources/12/dev-other.tar.gz": "12661c48e8c3fe1de2c1caa4c3e135193bfb1811584f11f569dd12645aa84365", # noqa: E501 + "http://www.openslr.org/resources/12/test-clean.tar.gz": "39fde525e59672dc6d1551919b1478f724438a95aa55f874b576be21967e6c23", # noqa: E501 + "http://www.openslr.org/resources/12/test-other.tar.gz": "d09c181bba5cf717b3dee7d4d592af11a3ee3a09e08ae025c5506f6ebe961c29", # noqa: E501 + "http://www.openslr.org/resources/12/train-clean-100.tar.gz": "d4ddd1d5a6ab303066f14971d768ee43278a5f2a0aa43dc716b0e64ecbbbf6e2", # noqa: E501 + "http://www.openslr.org/resources/12/train-clean-360.tar.gz": "146a56496217e96c14334a160df97fffedd6e0a04e66b9c5af0d40be3c792ecf", # noqa: E501 + "http://www.openslr.org/resources/12/train-other-500.tar.gz": "ddb22f27f96ec163645d53215559df6aa36515f26e01dd70798188350adcb6d2", # noqa: E501 +} + + +def _download_librispeech(root, url): + base_url = "http://www.openslr.org/resources/12/" + ext_archive = ".tar.gz" + + filename = url + ext_archive + archive = os.path.join(root, filename) + download_url = os.path.join(base_url, filename) + if not os.path.isfile(archive): + checksum = _CHECKSUMS.get(download_url, None) + download_url_to_file(download_url, archive, hash_prefix=checksum) + _extract_tar(archive) + + +def _get_librispeech_metadata( + fileid: str, root: str, folder: str, ext_audio: str, ext_txt: str, blist: List[str] +) -> Tuple[str, int, str, int, int, int]: + blist = blist or [] + speaker_id, chapter_id, utterance_id = fileid.split("-") + + # Get audio path and sample rate + fileid_audio = f"{speaker_id}-{chapter_id}-{utterance_id}" + filepath = os.path.join(folder, speaker_id, chapter_id, f"{fileid_audio}{ext_audio}") + + # Load text + file_text = f"{speaker_id}-{chapter_id}{ext_txt}" + file_text = os.path.join(root, folder, speaker_id, chapter_id, file_text) + uttblist = [] + with open(file_text) as ft: + for line in ft: + fileid_text, transcript = line.strip().split(" ", 1) + if fileid_audio == fileid_text: + # get utterance biasing list + for word in transcript.split(): + if word in blist and word not in uttblist: + uttblist.append(word) + break + else: + # Translation not found + raise FileNotFoundError(f"Translation not found for {fileid_audio}") + + return ( + filepath, + SAMPLE_RATE, + transcript, + int(speaker_id), + int(chapter_id), + int(utterance_id), + uttblist, + ) + + +class LibriSpeechBiasing(Dataset): + """*LibriSpeech* :cite:`7178964` dataset with prefix-tree construction and biasing support. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + url (str, optional): The URL to download the dataset from, + or the type of the dataset to dowload. + Allowed type values are ``"dev-clean"``, ``"dev-other"``, ``"test-clean"``, + ``"test-other"``, ``"train-clean-100"``, ``"train-clean-360"`` and + ``"train-other-500"``. (default: ``"train-clean-100"``) + folder_in_archive (str, optional): + The top-level directory of the dataset. (default: ``"LibriSpeech"``) + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + blist (list, optional): + The list of biasing words (default: ``[]``). + """ + + _ext_txt = ".trans.txt" + _ext_audio = ".flac" + + def __init__( + self, + root: Union[str, Path], + url: str = URL, + folder_in_archive: str = FOLDER_IN_ARCHIVE, + download: bool = False, + blist: List[str] = None, + ) -> None: + self._url = url + if url not in _DATA_SUBSETS: + raise ValueError(f"Invalid url '{url}' given; please provide one of {_DATA_SUBSETS}.") + + root = os.fspath(root) + self._archive = os.path.join(root, folder_in_archive) + self._path = os.path.join(root, folder_in_archive, url) + + if not os.path.isdir(self._path): + if download: + _download_librispeech(root, url) + else: + raise RuntimeError( + f"Dataset not found at {self._path}. Please set `download=True` to download the dataset." + ) + + self._walker = sorted(str(p.stem) for p in Path(self._path).glob("*/*/*" + self._ext_audio)) + self.blist = blist + + def get_metadata(self, n: int) -> Tuple[str, int, str, int, int, int]: + """Get metadata for the n-th sample from the dataset. Returns filepath instead of waveform, + but otherwise returns the same fields as :py:func:`__getitem__`. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + str: + Path to audio + int: + Sample rate + str: + Transcript + int: + Speaker ID + int: + Chapter ID + int: + Utterance ID + list: + List of biasing words in the utterance + """ + fileid = self._walker[n] + return _get_librispeech_metadata(fileid, self._archive, self._url, self._ext_audio, self._ext_txt, self.blist) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str, int, int, int]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + Transcript + int: + Speaker ID + int: + Chapter ID + int: + Utterance ID + list: + List of biasing words in the utterance + """ + metadata = self.get_metadata(n) + waveform = _load_waveform(self._archive, metadata[0], metadata[1]) + return (waveform,) + metadata[1:] + + def __len__(self) -> int: + return len(self._walker) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/libritts.py b/lib/python3.12/site-packages/torchaudio/datasets/libritts.py new file mode 100644 index 0000000000000000000000000000000000000000..829ce9572920c31ec7a4b393379f779a7df14ea9 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/libritts.py @@ -0,0 +1,168 @@ +import os +from pathlib import Path +from typing import Tuple, Union + +import torchaudio +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_tar + +URL = "train-clean-100" +FOLDER_IN_ARCHIVE = "LibriTTS" +_CHECKSUMS = { + "http://www.openslr.org/resources/60/dev-clean.tar.gz": "da0864e1bd26debed35da8a869dd5c04dfc27682921936de7cff9c8a254dbe1a", # noqa: E501 + "http://www.openslr.org/resources/60/dev-other.tar.gz": "d413eda26f3a152ac7c9cf3658ef85504dfb1b625296e5fa83727f5186cca79c", # noqa: E501 + "http://www.openslr.org/resources/60/test-clean.tar.gz": "234ea5b25859102a87024a4b9b86641f5b5aaaf1197335c95090cde04fe9a4f5", # noqa: E501 + "http://www.openslr.org/resources/60/test-other.tar.gz": "33a5342094f3bba7ccc2e0500b9e72d558f72eb99328ac8debe1d9080402f10d", # noqa: E501 + "http://www.openslr.org/resources/60/train-clean-100.tar.gz": "c5608bf1ef74bb621935382b8399c5cdd51cd3ee47cec51f00f885a64c6c7f6b", # noqa: E501 + "http://www.openslr.org/resources/60/train-clean-360.tar.gz": "ce7cff44dcac46009d18379f37ef36551123a1dc4e5c8e4eb73ae57260de4886", # noqa: E501 + "http://www.openslr.org/resources/60/train-other-500.tar.gz": "e35f7e34deeb2e2bdfe4403d88c8fdd5fbf64865cae41f027a185a6965f0a5df", # noqa: E501 +} + + +def load_libritts_item( + fileid: str, + path: str, + ext_audio: str, + ext_original_txt: str, + ext_normalized_txt: str, +) -> Tuple[Tensor, int, str, str, int, int, str]: + speaker_id, chapter_id, segment_id, utterance_id = fileid.split("_") + utterance_id = fileid + + normalized_text = utterance_id + ext_normalized_txt + normalized_text = os.path.join(path, speaker_id, chapter_id, normalized_text) + + original_text = utterance_id + ext_original_txt + original_text = os.path.join(path, speaker_id, chapter_id, original_text) + + file_audio = utterance_id + ext_audio + file_audio = os.path.join(path, speaker_id, chapter_id, file_audio) + + # Load audio + waveform, sample_rate = torchaudio.load(file_audio) + + # Load original text + with open(original_text) as ft: + original_text = ft.readline() + + # Load normalized text + with open(normalized_text, "r") as ft: + normalized_text = ft.readline() + + return ( + waveform, + sample_rate, + original_text, + normalized_text, + int(speaker_id), + int(chapter_id), + utterance_id, + ) + + +class LIBRITTS(Dataset): + """*LibriTTS* :cite:`Zen2019LibriTTSAC` dataset. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + url (str, optional): The URL to download the dataset from, + or the type of the dataset to dowload. + Allowed type values are ``"dev-clean"``, ``"dev-other"``, ``"test-clean"``, + ``"test-other"``, ``"train-clean-100"``, ``"train-clean-360"`` and + ``"train-other-500"``. (default: ``"train-clean-100"``) + folder_in_archive (str, optional): + The top-level directory of the dataset. (default: ``"LibriTTS"``) + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + """ + + _ext_original_txt = ".original.txt" + _ext_normalized_txt = ".normalized.txt" + _ext_audio = ".wav" + + def __init__( + self, + root: Union[str, Path], + url: str = URL, + folder_in_archive: str = FOLDER_IN_ARCHIVE, + download: bool = False, + ) -> None: + + if url in [ + "dev-clean", + "dev-other", + "test-clean", + "test-other", + "train-clean-100", + "train-clean-360", + "train-other-500", + ]: + + ext_archive = ".tar.gz" + base_url = "http://www.openslr.org/resources/60/" + + url = os.path.join(base_url, url + ext_archive) + + # Get string representation of 'root' in case Path object is passed + root = os.fspath(root) + + basename = os.path.basename(url) + archive = os.path.join(root, basename) + + basename = basename.split(".")[0] + folder_in_archive = os.path.join(folder_in_archive, basename) + + self._path = os.path.join(root, folder_in_archive) + + if download: + if not os.path.isdir(self._path): + if not os.path.isfile(archive): + checksum = _CHECKSUMS.get(url, None) + download_url_to_file(url, archive, hash_prefix=checksum) + _extract_tar(archive) + else: + if not os.path.exists(self._path): + raise RuntimeError( + f"The path {self._path} doesn't exist. " + "Please check the ``root`` path or set `download=True` to download it" + ) + + self._walker = sorted(str(p.stem) for p in Path(self._path).glob("*/*/*" + self._ext_audio)) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str, str, int, int, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + Original text + str: + Normalized text + int: + Speaker ID + int: + Chapter ID + str: + Utterance ID + """ + fileid = self._walker[n] + return load_libritts_item( + fileid, + self._path, + self._ext_audio, + self._ext_original_txt, + self._ext_normalized_txt, + ) + + def __len__(self) -> int: + return len(self._walker) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/ljspeech.py b/lib/python3.12/site-packages/torchaudio/datasets/ljspeech.py new file mode 100644 index 0000000000000000000000000000000000000000..9cdaeeb0f3e67a29fc57e9d0e9ed3056d98c24df --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/ljspeech.py @@ -0,0 +1,107 @@ +import csv +import os +from pathlib import Path +from typing import Tuple, Union + +import torchaudio +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_tar + + +_RELEASE_CONFIGS = { + "release1": { + "folder_in_archive": "wavs", + "url": "https://data.keithito.com/data/speech/LJSpeech-1.1.tar.bz2", + "checksum": "be1a30453f28eb8dd26af4101ae40cbf2c50413b1bb21936cbcdc6fae3de8aa5", + } +} + + +class LJSPEECH(Dataset): + """*LJSpeech-1.1* :cite:`ljspeech17` dataset. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + url (str, optional): The URL to download the dataset from. + (default: ``"https://data.keithito.com/data/speech/LJSpeech-1.1.tar.bz2"``) + folder_in_archive (str, optional): + The top-level directory of the dataset. (default: ``"wavs"``) + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + """ + + def __init__( + self, + root: Union[str, Path], + url: str = _RELEASE_CONFIGS["release1"]["url"], + folder_in_archive: str = _RELEASE_CONFIGS["release1"]["folder_in_archive"], + download: bool = False, + ) -> None: + + self._parse_filesystem(root, url, folder_in_archive, download) + + def _parse_filesystem(self, root: str, url: str, folder_in_archive: str, download: bool) -> None: + root = Path(root) + + basename = os.path.basename(url) + archive = root / basename + + basename = Path(basename.split(".tar.bz2")[0]) + folder_in_archive = basename / folder_in_archive + + self._path = root / folder_in_archive + self._metadata_path = root / basename / "metadata.csv" + + if download: + if not os.path.isdir(self._path): + if not os.path.isfile(archive): + checksum = _RELEASE_CONFIGS["release1"]["checksum"] + download_url_to_file(url, archive, hash_prefix=checksum) + _extract_tar(archive) + else: + if not os.path.exists(self._path): + raise RuntimeError( + f"The path {self._path} doesn't exist. " + "Please check the ``root`` path or set `download=True` to download it" + ) + + with open(self._metadata_path, "r", newline="") as metadata: + flist = csv.reader(metadata, delimiter="|", quoting=csv.QUOTE_NONE) + self._flist = list(flist) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + Transcript + str: + Normalized Transcript + """ + line = self._flist[n] + fileid, transcript, normalized_transcript = line + fileid_audio = self._path / (fileid + ".wav") + + # Load audio + waveform, sample_rate = torchaudio.load(fileid_audio) + + return ( + waveform, + sample_rate, + transcript, + normalized_transcript, + ) + + def __len__(self) -> int: + return len(self._flist) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/musdb_hq.py b/lib/python3.12/site-packages/torchaudio/datasets/musdb_hq.py new file mode 100644 index 0000000000000000000000000000000000000000..dd4bc9f340f3fde076ea31a683a7b41b7b3741d7 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/musdb_hq.py @@ -0,0 +1,139 @@ +import os +from pathlib import Path +from typing import List, Optional, Tuple, Union + +import torch +import torchaudio +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_zip + +_URL = "https://zenodo.org/record/3338373/files/musdb18hq.zip" +_CHECKSUM = "baac80d0483c61d74b2e5f3be75fa557eec52898339e6aa45c1fa48833c5d21d" +_EXT = ".wav" +_SAMPLE_RATE = 44100 +_VALIDATION_SET = [ + "Actions - One Minute Smile", + "Clara Berry And Wooldog - Waltz For My Victims", + "Johnny Lokke - Promises & Lies", + "Patrick Talbot - A Reason To Leave", + "Triviul - Angelsaint", + "Alexander Ross - Goodbye Bolero", + "Fergessen - Nos Palpitants", + "Leaf - Summerghost", + "Skelpolu - Human Mistakes", + "Young Griffo - Pennies", + "ANiMAL - Rockshow", + "James May - On The Line", + "Meaxic - Take A Step", + "Traffic Experiment - Sirens", +] + + +class MUSDB_HQ(Dataset): + """*MUSDB_HQ* :cite:`MUSDB18HQ` dataset. + + Args: + root (str or Path): Root directory where the dataset's top level directory is found + subset (str): Subset of the dataset to use. Options: [``"train"``, ``"test"``]. + sources (List[str] or None, optional): Sources extract data from. + List can contain the following options: [``"bass"``, ``"drums"``, ``"other"``, ``"mixture"``, ``"vocals"``]. + If ``None``, dataset consists of tracks except mixture. + (default: ``None``) + split (str or None, optional): Whether to split training set into train and validation set. + If ``None``, no splitting occurs. If ``train`` or ``validation``, returns respective set. + (default: ``None``) + download (bool, optional): Whether to download the dataset if it is not found at root path. + (default: ``False``) + """ + + def __init__( + self, + root: Union[str, Path], + subset: str, + sources: Optional[List[str]] = None, + split: Optional[str] = None, + download: bool = False, + ) -> None: + self.sources = ["bass", "drums", "other", "vocals"] if not sources else sources + self.split = split + + basename = os.path.basename(_URL) + archive = os.path.join(root, basename) + basename = basename.rsplit(".", 2)[0] + + if subset not in ["test", "train"]: + raise ValueError("`subset` must be one of ['test', 'train']") + if self.split is not None and self.split not in ["train", "validation"]: + raise ValueError("`split` must be one of ['train', 'validation']") + base_path = os.path.join(root, basename) + self._path = os.path.join(base_path, subset) + if not os.path.isdir(self._path): + if not os.path.isfile(archive): + if not download: + raise RuntimeError("Dataset not found. Please use `download=True` to download") + download_url_to_file(_URL, archive, hash_prefix=_CHECKSUM) + os.makedirs(base_path, exist_ok=True) + _extract_zip(archive, base_path) + + self.names = self._collect_songs() + + def _get_track(self, name, source): + return Path(self._path) / name / f"{source}{_EXT}" + + def _load_sample(self, n: int) -> Tuple[torch.Tensor, int, int, str]: + name = self.names[n] + wavs = [] + num_frames = None + for source in self.sources: + track = self._get_track(name, source) + wav, sr = torchaudio.load(str(track)) + if sr != _SAMPLE_RATE: + raise ValueError(f"expected sample rate {_SAMPLE_RATE}, but got {sr}") + if num_frames is None: + num_frames = wav.shape[-1] + else: + if wav.shape[-1] != num_frames: + raise ValueError("num_frames do not match across sources") + wavs.append(wav) + + stacked = torch.stack(wavs) + + return stacked, _SAMPLE_RATE, num_frames, name + + def _collect_songs(self): + if self.split == "validation": + return _VALIDATION_SET + path = Path(self._path) + names = [] + for root, folders, _ in os.walk(path, followlinks=True): + root = Path(root) + if root.name.startswith(".") or folders or root == path: + continue + name = str(root.relative_to(path)) + if self.split and name in _VALIDATION_SET: + continue + names.append(name) + return sorted(names) + + def __getitem__(self, n: int) -> Tuple[torch.Tensor, int, int, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + int: + Num frames + str: + Track name + """ + return self._load_sample(n) + + def __len__(self) -> int: + return len(self.names) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/quesst14.py b/lib/python3.12/site-packages/torchaudio/datasets/quesst14.py new file mode 100644 index 0000000000000000000000000000000000000000..064423c4494850f2ad8f43fb00a956be21fcb95e --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/quesst14.py @@ -0,0 +1,136 @@ +import os +import re +from pathlib import Path +from typing import Optional, Tuple, Union + +import torch +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_tar, _load_waveform + + +URL = "https://speech.fit.vutbr.cz/files/quesst14Database.tgz" +SAMPLE_RATE = 8000 +_CHECKSUM = "4f869e06bc066bbe9c5dde31dbd3909a0870d70291110ebbb38878dcbc2fc5e4" +_LANGUAGES = [ + "albanian", + "basque", + "czech", + "nnenglish", + "romanian", + "slovak", +] + + +class QUESST14(Dataset): + """*QUESST14* :cite:`Mir2015QUESST2014EQ` dataset. + + Args: + root (str or Path): Root directory where the dataset's top level directory is found + subset (str): Subset of the dataset to use. Options: [``"docs"``, ``"dev"``, ``"eval"``]. + language (str or None, optional): Language to get dataset for. + Options: [``None``, ``albanian``, ``basque``, ``czech``, ``nnenglish``, ``romanian``, ``slovak``]. + If ``None``, dataset consists of all languages. (default: ``"nnenglish"``) + download (bool, optional): Whether to download the dataset if it is not found at root path. + (default: ``False``) + """ + + def __init__( + self, + root: Union[str, Path], + subset: str, + language: Optional[str] = "nnenglish", + download: bool = False, + ) -> None: + if subset not in ["docs", "dev", "eval"]: + raise ValueError("`subset` must be one of ['docs', 'dev', 'eval']") + + if language is not None and language not in _LANGUAGES: + raise ValueError(f"`language` must be None or one of {str(_LANGUAGES)}") + + # Get string representation of 'root' + root = os.fspath(root) + + basename = os.path.basename(URL) + archive = os.path.join(root, basename) + + basename = basename.rsplit(".", 2)[0] + self._path = os.path.join(root, basename) + + if not os.path.isdir(self._path): + if not os.path.isfile(archive): + if not download: + raise RuntimeError("Dataset not found. Please use `download=True` to download") + download_url_to_file(URL, archive, hash_prefix=_CHECKSUM) + _extract_tar(archive, root) + + if subset == "docs": + self.data = filter_audio_paths(self._path, language, "language_key_utterances.lst") + elif subset == "dev": + self.data = filter_audio_paths(self._path, language, "language_key_dev.lst") + elif subset == "eval": + self.data = filter_audio_paths(self._path, language, "language_key_eval.lst") + + def get_metadata(self, n: int) -> Tuple[str, int, str]: + """Get metadata for the n-th sample from the dataset. Returns filepath instead of waveform, + but otherwise returns the same fields as :py:func:`__getitem__`. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + str: + Path to audio + int: + Sample rate + str: + File name + """ + audio_path = self.data[n] + relpath = os.path.relpath(audio_path, self._path) + return relpath, SAMPLE_RATE, audio_path.with_suffix("").name + + def __getitem__(self, n: int) -> Tuple[torch.Tensor, int, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + File name + """ + metadata = self.get_metadata(n) + waveform = _load_waveform(self._path, metadata[0], metadata[1]) + return (waveform,) + metadata[1:] + + def __len__(self) -> int: + return len(self.data) + + +def filter_audio_paths( + path: str, + language: str, + lst_name: str, +): + """Extract audio paths for the given language.""" + audio_paths = [] + + path = Path(path) + with open(path / "scoring" / lst_name) as f: + for line in f: + audio_path, lang = line.strip().split() + if language is not None and lang != language: + continue + audio_path = re.sub(r"^.*?\/", "", audio_path) + audio_paths.append(path / audio_path) + + return audio_paths diff --git a/lib/python3.12/site-packages/torchaudio/datasets/snips.py b/lib/python3.12/site-packages/torchaudio/datasets/snips.py new file mode 100644 index 0000000000000000000000000000000000000000..6b15d677f7fa1f9c1baccad7625a6fa14c73d70f --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/snips.py @@ -0,0 +1,157 @@ +import os +from pathlib import Path +from typing import List, Optional, Tuple, Union + +import torch +from torch.utils.data import Dataset +from torchaudio.datasets.utils import _load_waveform + + +_SAMPLE_RATE = 16000 +_SPEAKERS = [ + "Aditi", + "Amy", + "Brian", + "Emma", + "Geraint", + "Ivy", + "Joanna", + "Joey", + "Justin", + "Kendra", + "Kimberly", + "Matthew", + "Nicole", + "Raveena", + "Russell", + "Salli", +] + + +def _load_labels(file: Path, subset: str): + """Load transcirpt, iob, and intent labels for all utterances. + + Args: + file (Path): The path to the label file. + subset (str): Subset of the dataset to use. Options: [``"train"``, ``"valid"``, ``"test"``]. + + Returns: + Dictionary of labels, where the key is the filename of the audio, + and the label is a Tuple of transcript, Inside–outside–beginning (IOB) label, and intention label. + """ + labels = {} + with open(file, "r") as f: + for line in f: + line = line.strip().split(" ") + index = line[0] + trans, iob_intent = " ".join(line[1:]).split("\t") + trans = " ".join(trans.split(" ")[1:-1]) + iob = " ".join(iob_intent.split(" ")[1:-1]) + intent = iob_intent.split(" ")[-1] + if subset in index: + labels[index] = (trans, iob, intent) + return labels + + +class Snips(Dataset): + """*Snips* :cite:`coucke2018snips` dataset. + + Args: + root (str or Path): Root directory where the dataset's top level directory is found. + subset (str): Subset of the dataset to use. Options: [``"train"``, ``"valid"``, ``"test"``]. + speakers (List[str] or None, optional): The speaker list to include in the dataset. If ``None``, + include all speakers in the subset. (Default: ``None``) + audio_format (str, optional): The extension of the audios. Options: [``"mp3"``, ``"wav"``]. + (Default: ``"mp3"``) + """ + + _trans_file = "all.iob.snips.txt" + + def __init__( + self, + root: Union[str, Path], + subset: str, + speakers: Optional[List[str]] = None, + audio_format: str = "mp3", + ) -> None: + if subset not in ["train", "valid", "test"]: + raise ValueError('`subset` must be one of ["train", "valid", "test"].') + if audio_format not in ["mp3", "wav"]: + raise ValueError('`audio_format` must be one of ["mp3", "wav].') + + root = Path(root) + self._path = root / "SNIPS" + self.audio_path = self._path / subset + if speakers is None: + speakers = _SPEAKERS + + if not os.path.isdir(self._path): + raise RuntimeError("Dataset not found.") + + self.audio_paths = self.audio_path.glob(f"*.{audio_format}") + self.data = [] + for audio_path in sorted(self.audio_paths): + audio_name = str(audio_path.name) + speaker = audio_name.split("-")[0] + if speaker in speakers: + self.data.append(audio_path) + transcript_path = self._path / self._trans_file + self.labels = _load_labels(transcript_path, subset) + + def get_metadata(self, n: int) -> Tuple[str, int, str, str, str]: + """Get metadata for the n-th sample from the dataset. Returns filepath instead of waveform, + but otherwise returns the same fields as :py:func:`__getitem__`. + + Args: + n (int): The index of the sample to be loaded. + + Returns: + Tuple of the following items: + + str: + Path to audio + int: + Sample rate + str: + File name + str: + Transcription of audio + str: + Inside–outside–beginning (IOB) label of transcription + str: + Intention label of the audio. + """ + audio_path = self.data[n] + relpath = os.path.relpath(audio_path, self._path) + file_name = audio_path.with_suffix("").name + transcript, iob, intent = self.labels[file_name] + return relpath, _SAMPLE_RATE, file_name, transcript, iob, intent + + def __getitem__(self, n: int) -> Tuple[torch.Tensor, int, str, str, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items: + + Tensor: + Waveform + int: + Sample rate + str: + File name + str: + Transcription of audio + str: + Inside–outside–beginning (IOB) label of transcription + str: + Intention label of the audio. + """ + metadata = self.get_metadata(n) + waveform = _load_waveform(self._path, metadata[0], metadata[1]) + return (waveform,) + metadata[1:] + + def __len__(self) -> int: + return len(self.data) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/speechcommands.py b/lib/python3.12/site-packages/torchaudio/datasets/speechcommands.py new file mode 100644 index 0000000000000000000000000000000000000000..1945fc75c18b474404b733e43d50156f3c3d6652 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/speechcommands.py @@ -0,0 +1,183 @@ +import os +from pathlib import Path +from typing import Optional, Tuple, Union + +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_tar, _load_waveform + +FOLDER_IN_ARCHIVE = "SpeechCommands" +URL = "speech_commands_v0.02" +HASH_DIVIDER = "_nohash_" +EXCEPT_FOLDER = "_background_noise_" +SAMPLE_RATE = 16000 +_CHECKSUMS = { + "http://download.tensorflow.org/data/speech_commands_v0.01.tar.gz": "743935421bb51cccdb6bdd152e04c5c70274e935c82119ad7faeec31780d811d", # noqa: E501 + "http://download.tensorflow.org/data/speech_commands_v0.02.tar.gz": "af14739ee7dc311471de98f5f9d2c9191b18aedfe957f4a6ff791c709868ff58", # noqa: E501 +} + + +def _load_list(root, *filenames): + output = [] + for filename in filenames: + filepath = os.path.join(root, filename) + with open(filepath) as fileobj: + output += [os.path.normpath(os.path.join(root, line.strip())) for line in fileobj] + return output + + +def _get_speechcommands_metadata(filepath: str, path: str) -> Tuple[str, int, str, str, int]: + relpath = os.path.relpath(filepath, path) + reldir, filename = os.path.split(relpath) + _, label = os.path.split(reldir) + # Besides the officially supported split method for datasets defined by "validation_list.txt" + # and "testing_list.txt" over "speech_commands_v0.0x.tar.gz" archives, an alternative split + # method referred to in paragraph 2-3 of Section 7.1, references 13 and 14 of the original + # paper, and the checksums file from the tensorflow_datasets package [1] is also supported. + # Some filenames in those "speech_commands_test_set_v0.0x.tar.gz" archives have the form + # "xxx.wav.wav", so file extensions twice needs to be stripped twice. + # [1] https://github.com/tensorflow/datasets/blob/master/tensorflow_datasets/url_checksums/speech_commands.txt + speaker, _ = os.path.splitext(filename) + speaker, _ = os.path.splitext(speaker) + + speaker_id, utterance_number = speaker.split(HASH_DIVIDER) + utterance_number = int(utterance_number) + + return relpath, SAMPLE_RATE, label, speaker_id, utterance_number + + +class SPEECHCOMMANDS(Dataset): + """*Speech Commands* :cite:`speechcommandsv2` dataset. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + url (str, optional): The URL to download the dataset from, + or the type of the dataset to dowload. + Allowed type values are ``"speech_commands_v0.01"`` and ``"speech_commands_v0.02"`` + (default: ``"speech_commands_v0.02"``) + folder_in_archive (str, optional): + The top-level directory of the dataset. (default: ``"SpeechCommands"``) + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + subset (str or None, optional): + Select a subset of the dataset [None, "training", "validation", "testing"]. None means + the whole dataset. "validation" and "testing" are defined in "validation_list.txt" and + "testing_list.txt", respectively, and "training" is the rest. Details for the files + "validation_list.txt" and "testing_list.txt" are explained in the README of the dataset + and in the introduction of Section 7 of the original paper and its reference 12. The + original paper can be found `here `_. (Default: ``None``) + """ + + def __init__( + self, + root: Union[str, Path], + url: str = URL, + folder_in_archive: str = FOLDER_IN_ARCHIVE, + download: bool = False, + subset: Optional[str] = None, + ) -> None: + + if subset is not None and subset not in ["training", "validation", "testing"]: + raise ValueError("When `subset` is not None, it must be one of ['training', 'validation', 'testing'].") + + if url in [ + "speech_commands_v0.01", + "speech_commands_v0.02", + ]: + base_url = "http://download.tensorflow.org/data/" + ext_archive = ".tar.gz" + + url = os.path.join(base_url, url + ext_archive) + + # Get string representation of 'root' in case Path object is passed + root = os.fspath(root) + self._archive = os.path.join(root, folder_in_archive) + + basename = os.path.basename(url) + archive = os.path.join(root, basename) + + basename = basename.rsplit(".", 2)[0] + folder_in_archive = os.path.join(folder_in_archive, basename) + + self._path = os.path.join(root, folder_in_archive) + + if download: + if not os.path.isdir(self._path): + if not os.path.isfile(archive): + checksum = _CHECKSUMS.get(url, None) + download_url_to_file(url, archive, hash_prefix=checksum) + _extract_tar(archive, self._path) + else: + if not os.path.exists(self._path): + raise RuntimeError( + f"The path {self._path} doesn't exist. " + "Please check the ``root`` path or set `download=True` to download it" + ) + + if subset == "validation": + self._walker = _load_list(self._path, "validation_list.txt") + elif subset == "testing": + self._walker = _load_list(self._path, "testing_list.txt") + elif subset == "training": + excludes = set(_load_list(self._path, "validation_list.txt", "testing_list.txt")) + walker = sorted(str(p) for p in Path(self._path).glob("*/*.wav")) + self._walker = [ + w + for w in walker + if HASH_DIVIDER in w and EXCEPT_FOLDER not in w and os.path.normpath(w) not in excludes + ] + else: + walker = sorted(str(p) for p in Path(self._path).glob("*/*.wav")) + self._walker = [w for w in walker if HASH_DIVIDER in w and EXCEPT_FOLDER not in w] + + def get_metadata(self, n: int) -> Tuple[str, int, str, str, int]: + """Get metadata for the n-th sample from the dataset. Returns filepath instead of waveform, + but otherwise returns the same fields as :py:func:`__getitem__`. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + str: + Path to the audio + int: + Sample rate + str: + Label + str: + Speaker ID + int: + Utterance number + """ + fileid = self._walker[n] + return _get_speechcommands_metadata(fileid, self._archive) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str, str, int]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + Label + str: + Speaker ID + int: + Utterance number + """ + metadata = self.get_metadata(n) + waveform = _load_waveform(self._archive, metadata[0], metadata[1]) + return (waveform,) + metadata[1:] + + def __len__(self) -> int: + return len(self._walker) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/tedlium.py b/lib/python3.12/site-packages/torchaudio/datasets/tedlium.py new file mode 100644 index 0000000000000000000000000000000000000000..7e7d22195a772d18770f6db3253d83672743c81c --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/tedlium.py @@ -0,0 +1,218 @@ +import os +from pathlib import Path +from typing import Tuple, Union + +import torchaudio +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_tar + + +_RELEASE_CONFIGS = { + "release1": { + "folder_in_archive": "TEDLIUM_release1", + "url": "http://www.openslr.org/resources/7/TEDLIUM_release1.tar.gz", + "checksum": "30301975fd8c5cac4040c261c0852f57cfa8adbbad2ce78e77e4986957445f27", + "data_path": "", + "subset": "train", + "supported_subsets": ["train", "test", "dev"], + "dict": "TEDLIUM.150K.dic", + }, + "release2": { + "folder_in_archive": "TEDLIUM_release2", + "url": "http://www.openslr.org/resources/19/TEDLIUM_release2.tar.gz", + "checksum": "93281b5fcaaae5c88671c9d000b443cb3c7ea3499ad12010b3934ca41a7b9c58", + "data_path": "", + "subset": "train", + "supported_subsets": ["train", "test", "dev"], + "dict": "TEDLIUM.152k.dic", + }, + "release3": { + "folder_in_archive": "TEDLIUM_release-3", + "url": "http://www.openslr.org/resources/51/TEDLIUM_release-3.tgz", + "checksum": "ad1e454d14d1ad550bc2564c462d87c7a7ec83d4dc2b9210f22ab4973b9eccdb", + "data_path": "data/", + "subset": "train", + "supported_subsets": ["train", "test", "dev"], + "dict": "TEDLIUM.152k.dic", + }, +} + + +class TEDLIUM(Dataset): + """*Tedlium* :cite:`rousseau2012tedlium` dataset (releases 1,2 and 3). + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + release (str, optional): Release version. + Allowed values are ``"release1"``, ``"release2"`` or ``"release3"``. + (default: ``"release1"``). + subset (str, optional): The subset of dataset to use. Valid options are ``"train"``, ``"dev"``, + and ``"test"``. Defaults to ``"train"``. + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + audio_ext (str, optional): extension for audio file (default: ``".sph"``) + """ + + def __init__( + self, + root: Union[str, Path], + release: str = "release1", + subset: str = "train", + download: bool = False, + audio_ext: str = ".sph", + ) -> None: + self._ext_audio = audio_ext + if release in _RELEASE_CONFIGS.keys(): + folder_in_archive = _RELEASE_CONFIGS[release]["folder_in_archive"] + url = _RELEASE_CONFIGS[release]["url"] + subset = subset if subset else _RELEASE_CONFIGS[release]["subset"] + else: + # Raise warning + raise RuntimeError( + "The release {} does not match any of the supported tedlium releases{} ".format( + release, + _RELEASE_CONFIGS.keys(), + ) + ) + if subset not in _RELEASE_CONFIGS[release]["supported_subsets"]: + # Raise warning + raise RuntimeError( + "The subset {} does not match any of the supported tedlium subsets{} ".format( + subset, + _RELEASE_CONFIGS[release]["supported_subsets"], + ) + ) + + # Get string representation of 'root' in case Path object is passed + root = os.fspath(root) + + basename = os.path.basename(url) + archive = os.path.join(root, basename) + + basename = basename.split(".")[0] + + if release == "release3": + if subset == "train": + self._path = os.path.join(root, folder_in_archive, _RELEASE_CONFIGS[release]["data_path"]) + else: + self._path = os.path.join(root, folder_in_archive, "legacy", subset) + else: + self._path = os.path.join(root, folder_in_archive, _RELEASE_CONFIGS[release]["data_path"], subset) + + if download: + if not os.path.isdir(self._path): + if not os.path.isfile(archive): + checksum = _RELEASE_CONFIGS[release]["checksum"] + download_url_to_file(url, archive, hash_prefix=checksum) + _extract_tar(archive) + else: + if not os.path.exists(self._path): + raise RuntimeError( + f"The path {self._path} doesn't exist. " + "Please check the ``root`` path or set `download=True` to download it" + ) + + # Create list for all samples + self._filelist = [] + stm_path = os.path.join(self._path, "stm") + for file in sorted(os.listdir(stm_path)): + if file.endswith(".stm"): + stm_path = os.path.join(self._path, "stm", file) + with open(stm_path) as f: + l = len(f.readlines()) + file = file.replace(".stm", "") + self._filelist.extend((file, line) for line in range(l)) + # Create dict path for later read + self._dict_path = os.path.join(root, folder_in_archive, _RELEASE_CONFIGS[release]["dict"]) + self._phoneme_dict = None + + def _load_tedlium_item(self, fileid: str, line: int, path: str) -> Tuple[Tensor, int, str, int, int, int]: + """Loads a TEDLIUM dataset sample given a file name and corresponding sentence name. + + Args: + fileid (str): File id to identify both text and audio files corresponding to the sample + line (int): Line identifier for the sample inside the text file + path (str): Dataset root path + + Returns: + (Tensor, int, str, int, int, int): + ``(waveform, sample_rate, transcript, talk_id, speaker_id, identifier)`` + """ + transcript_path = os.path.join(path, "stm", fileid) + with open(transcript_path + ".stm") as f: + transcript = f.readlines()[line] + talk_id, _, speaker_id, start_time, end_time, identifier, transcript = transcript.split(" ", 6) + + wave_path = os.path.join(path, "sph", fileid) + waveform, sample_rate = self._load_audio(wave_path + self._ext_audio, start_time=start_time, end_time=end_time) + + return (waveform, sample_rate, transcript, talk_id, speaker_id, identifier) + + def _load_audio(self, path: str, start_time: float, end_time: float, sample_rate: int = 16000) -> [Tensor, int]: + """Default load function used in TEDLIUM dataset, you can overwrite this function to customize functionality + and load individual sentences from a full ted audio talk file. + + Args: + path (str): Path to audio file + start_time (int): Time in seconds where the sample sentence stars + end_time (int): Time in seconds where the sample sentence finishes + sample_rate (float, optional): Sampling rate + + Returns: + [Tensor, int]: Audio tensor representation and sample rate + """ + start_time = int(float(start_time) * sample_rate) + end_time = int(float(end_time) * sample_rate) + + kwargs = {"frame_offset": start_time, "num_frames": end_time - start_time} + + return torchaudio.load(path, **kwargs) + + def __getitem__(self, n: int) -> Tuple[Tensor, int, str, int, int, int]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + Transcript + int: + Talk ID + int: + Speaker ID + int: + Identifier + """ + fileid, line = self._filelist[n] + return self._load_tedlium_item(fileid, line, self._path) + + def __len__(self) -> int: + """TEDLIUM dataset custom function overwritting len default behaviour. + + Returns: + int: TEDLIUM dataset length + """ + return len(self._filelist) + + @property + def phoneme_dict(self): + """dict[str, tuple[str]]: Phonemes. Mapping from word to tuple of phonemes. + Note that some words have empty phonemes. + """ + # Read phoneme dictionary + if not self._phoneme_dict: + self._phoneme_dict = {} + with open(self._dict_path, "r", encoding="utf-8") as f: + for line in f.readlines(): + content = line.strip().split() + self._phoneme_dict[content[0]] = tuple(content[1:]) # content[1:] can be empty list + return self._phoneme_dict.copy() diff --git a/lib/python3.12/site-packages/torchaudio/datasets/utils.py b/lib/python3.12/site-packages/torchaudio/datasets/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b4599f83aae535d5c4126b5d0bab4ed325f494f3 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/utils.py @@ -0,0 +1,54 @@ +import logging +import os +import tarfile +import zipfile +from typing import Any, List, Optional + +import torchaudio + +_LG = logging.getLogger(__name__) + + +def _extract_tar(from_path: str, to_path: Optional[str] = None, overwrite: bool = False) -> List[str]: + if to_path is None: + to_path = os.path.dirname(from_path) + with tarfile.open(from_path, "r") as tar: + files = [] + for file_ in tar: # type: Any + file_path = os.path.join(to_path, file_.name) + if file_.isfile(): + files.append(file_path) + if os.path.exists(file_path): + _LG.info("%s already extracted.", file_path) + if not overwrite: + continue + tar.extract(file_, to_path) + return files + + +def _extract_zip(from_path: str, to_path: Optional[str] = None, overwrite: bool = False) -> List[str]: + if to_path is None: + to_path = os.path.dirname(from_path) + + with zipfile.ZipFile(from_path, "r") as zfile: + files = zfile.namelist() + for file_ in files: + file_path = os.path.join(to_path, file_) + if os.path.exists(file_path): + _LG.info("%s already extracted.", file_path) + if not overwrite: + continue + zfile.extract(file_, to_path) + return files + + +def _load_waveform( + root: str, + filename: str, + exp_sample_rate: int, +): + path = os.path.join(root, filename) + waveform, sample_rate = torchaudio.load(path) + if exp_sample_rate != sample_rate: + raise ValueError(f"sample rate should be {exp_sample_rate}, but got {sample_rate}") + return waveform diff --git a/lib/python3.12/site-packages/torchaudio/datasets/vctk.py b/lib/python3.12/site-packages/torchaudio/datasets/vctk.py new file mode 100644 index 0000000000000000000000000000000000000000..3195b9b4276b643e934baadc26c872fc690383df --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/vctk.py @@ -0,0 +1,143 @@ +import os +from typing import Tuple + +import torchaudio +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_zip + +URL = "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip" +_CHECKSUMS = { + "https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip": "f96258be9fdc2cbff6559541aae7ea4f59df3fcaf5cf963aae5ca647357e359c" # noqa: E501 +} + + +SampleType = Tuple[Tensor, int, str, str, str] + + +class VCTK_092(Dataset): + """*VCTK 0.92* :cite:`yamagishi2019vctk` dataset + + Args: + root (str): Root directory where the dataset's top level directory is found. + mic_id (str, optional): Microphone ID. Either ``"mic1"`` or ``"mic2"``. (default: ``"mic2"``) + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + url (str, optional): The URL to download the dataset from. + (default: ``"https://datashare.is.ed.ac.uk/bitstream/handle/10283/3443/VCTK-Corpus-0.92.zip"``) + audio_ext (str, optional): Custom audio extension if dataset is converted to non-default audio format. + + Note: + * All the speeches from speaker ``p315`` will be skipped due to the lack of the corresponding text files. + * All the speeches from ``p280`` will be skipped for ``mic_id="mic2"`` due to the lack of the audio files. + * Some of the speeches from speaker ``p362`` will be skipped due to the lack of the audio files. + * See Also: https://datashare.is.ed.ac.uk/handle/10283/3443 + """ + + def __init__( + self, + root: str, + mic_id: str = "mic2", + download: bool = False, + url: str = URL, + audio_ext=".flac", + ): + if mic_id not in ["mic1", "mic2"]: + raise RuntimeError(f'`mic_id` has to be either "mic1" or "mic2". Found: {mic_id}') + + archive = os.path.join(root, "VCTK-Corpus-0.92.zip") + + self._path = os.path.join(root, "VCTK-Corpus-0.92") + self._txt_dir = os.path.join(self._path, "txt") + self._audio_dir = os.path.join(self._path, "wav48_silence_trimmed") + self._mic_id = mic_id + self._audio_ext = audio_ext + + if download: + if not os.path.isdir(self._path): + if not os.path.isfile(archive): + checksum = _CHECKSUMS.get(url, None) + download_url_to_file(url, archive, hash_prefix=checksum) + _extract_zip(archive, self._path) + + if not os.path.isdir(self._path): + raise RuntimeError("Dataset not found. Please use `download=True` to download it.") + + # Extracting speaker IDs from the folder structure + self._speaker_ids = sorted(os.listdir(self._txt_dir)) + self._sample_ids = [] + + """ + Due to some insufficient data complexity in the 0.92 version of this dataset, + we start traversing the audio folder structure in accordance with the text folder. + As some of the audio files are missing of either ``mic_1`` or ``mic_2`` but the + text is present for the same, we first check for the existence of the audio file + before adding it to the ``sample_ids`` list. + + Once the ``audio_ids`` are loaded into memory we can quickly access the list for + different parameters required by the user. + """ + for speaker_id in self._speaker_ids: + if speaker_id == "p280" and mic_id == "mic2": + continue + utterance_dir = os.path.join(self._txt_dir, speaker_id) + for utterance_file in sorted(f for f in os.listdir(utterance_dir) if f.endswith(".txt")): + utterance_id = os.path.splitext(utterance_file)[0] + audio_path_mic = os.path.join( + self._audio_dir, + speaker_id, + f"{utterance_id}_{mic_id}{self._audio_ext}", + ) + if speaker_id == "p362" and not os.path.isfile(audio_path_mic): + continue + self._sample_ids.append(utterance_id.split("_")) + + def _load_text(self, file_path) -> str: + with open(file_path) as file_path: + return file_path.readlines()[0] + + def _load_audio(self, file_path) -> Tuple[Tensor, int]: + return torchaudio.load(file_path) + + def _load_sample(self, speaker_id: str, utterance_id: str, mic_id: str) -> SampleType: + transcript_path = os.path.join(self._txt_dir, speaker_id, f"{speaker_id}_{utterance_id}.txt") + audio_path = os.path.join( + self._audio_dir, + speaker_id, + f"{speaker_id}_{utterance_id}_{mic_id}{self._audio_ext}", + ) + + # Reading text + transcript = self._load_text(transcript_path) + + # Reading FLAC + waveform, sample_rate = self._load_audio(audio_path) + + return (waveform, sample_rate, transcript, speaker_id, utterance_id) + + def __getitem__(self, n: int) -> SampleType: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + str: + Transcript + str: + Speaker ID + std: + Utterance ID + """ + speaker_id, utterance_id = self._sample_ids[n] + return self._load_sample(speaker_id, utterance_id, self._mic_id) + + def __len__(self) -> int: + return len(self._sample_ids) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/voxceleb1.py b/lib/python3.12/site-packages/torchaudio/datasets/voxceleb1.py new file mode 100644 index 0000000000000000000000000000000000000000..5112fff0898a88adb1d2c33acf9bdd905ca883f3 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/voxceleb1.py @@ -0,0 +1,309 @@ +import os +from pathlib import Path +from typing import List, Tuple, Union + +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_zip, _load_waveform + + +SAMPLE_RATE = 16000 +_ARCHIVE_CONFIGS = { + "dev": { + "archive_name": "vox1_dev_wav.zip", + "urls": [ + "https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_dev_wav_partaa", + "https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_dev_wav_partab", + "https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_dev_wav_partac", + "https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_dev_wav_partad", + ], + "checksums": [ + "21ec6ca843659ebc2fdbe04b530baa4f191ad4b0971912672d92c158f32226a0", + "311d21e0c8cbf33573a4fce6c80e5a279d80736274b381c394319fc557159a04", + "92b64465f2b2a3dc0e4196ae8dd6828cbe9ddd1f089419a11e4cbfe2e1750df0", + "00e6190c770b27f27d2a3dd26ee15596b17066b715ac111906861a7d09a211a5", + ], + }, + "test": { + "archive_name": "vox1_test_wav.zip", + "url": "https://thor.robots.ox.ac.uk/~vgg/data/voxceleb/vox1a/vox1_test_wav.zip", + "checksum": "8de57f347fe22b2c24526e9f444f689ecf5096fc2a92018cf420ff6b5b15eaea", + }, +} +_IDEN_SPLIT_URL = "https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/iden_split.txt" +_VERI_TEST_URL = "https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/veri_test.txt" + + +def _download_extract_wavs(root: str): + for archive in ["dev", "test"]: + archive_name = _ARCHIVE_CONFIGS[archive]["archive_name"] + archive_path = os.path.join(root, archive_name) + # The zip file of dev data is splited to 4 chunks. + # Download and combine them into one file before extraction. + if archive == "dev": + urls = _ARCHIVE_CONFIGS[archive]["urls"] + checksums = _ARCHIVE_CONFIGS[archive]["checksums"] + with open(archive_path, "wb") as f: + for url, checksum in zip(urls, checksums): + file_path = os.path.join(root, os.path.basename(url)) + download_url_to_file(url, file_path, hash_prefix=checksum) + with open(file_path, "rb") as f_split: + f.write(f_split.read()) + else: + url = _ARCHIVE_CONFIGS[archive]["url"] + checksum = _ARCHIVE_CONFIGS[archive]["checksum"] + download_url_to_file(url, archive_path, hash_prefix=checksum) + _extract_zip(archive_path) + + +def _get_flist(root: str, file_path: str, subset: str) -> List[str]: + f_list = [] + if subset == "train": + index = 1 + elif subset == "dev": + index = 2 + else: + index = 3 + with open(file_path, "r") as f: + for line in f: + id, path = line.split() + if int(id) == index: + f_list.append(path) + return sorted(f_list) + + +def _get_paired_flist(root: str, veri_test_path: str): + f_list = [] + with open(veri_test_path, "r") as f: + for line in f: + label, path1, path2 = line.split() + f_list.append((label, path1, path2)) + return f_list + + +def _get_file_id(file_path: str, _ext_audio: str): + speaker_id, youtube_id, utterance_id = file_path.split("/")[-3:] + utterance_id = utterance_id.replace(_ext_audio, "") + file_id = "-".join([speaker_id, youtube_id, utterance_id]) + return file_id + + +class VoxCeleb1(Dataset): + """*VoxCeleb1* :cite:`nagrani2017voxceleb` dataset. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + download (bool, optional): + Whether to download the dataset if it is not found at root path. (Default: ``False``). + """ + + _ext_audio = ".wav" + + def __init__(self, root: Union[str, Path], download: bool = False) -> None: + # Get string representation of 'root' in case Path object is passed + root = os.fspath(root) + self._path = os.path.join(root, "wav") + if not os.path.isdir(self._path): + if not download: + raise RuntimeError( + f"Dataset not found at {self._path}. Please set `download=True` to download the dataset." + ) + _download_extract_wavs(root) + + def get_metadata(self, n: int): + raise NotImplementedError + + def __getitem__(self, n: int): + raise NotImplementedError + + def __len__(self) -> int: + raise NotImplementedError + + +class VoxCeleb1Identification(VoxCeleb1): + """*VoxCeleb1* :cite:`nagrani2017voxceleb` dataset for speaker identification task. + + Each data sample contains the waveform, sample rate, speaker id, and the file id. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + subset (str, optional): Subset of the dataset to use. Options: ["train", "dev", "test"]. (Default: ``"train"``) + meta_url (str, optional): The url of meta file that contains the list of subset labels and file paths. + The format of each row is ``subset file_path". For example: ``1 id10006/nLEBBc9oIFs/00003.wav``. + ``1``, ``2``, ``3`` mean ``train``, ``dev``, and ``test`` subest, respectively. + (Default: ``"https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/iden_split.txt"``) + download (bool, optional): + Whether to download the dataset if it is not found at root path. (Default: ``False``). + + Note: + The file structure of `VoxCeleb1Identification` dataset is as follows: + + └─ root/ + + └─ wav/ + + └─ speaker_id folders + + Users who pre-downloaded the ``"vox1_dev_wav.zip"`` and ``"vox1_test_wav.zip"`` files need to move + the extracted files into the same ``root`` directory. + """ + + def __init__( + self, root: Union[str, Path], subset: str = "train", meta_url: str = _IDEN_SPLIT_URL, download: bool = False + ) -> None: + super().__init__(root, download) + if subset not in ["train", "dev", "test"]: + raise ValueError("`subset` must be one of ['train', 'dev', 'test']") + # download the iden_split.txt to get the train, dev, test lists. + meta_list_path = os.path.join(root, os.path.basename(meta_url)) + if not os.path.exists(meta_list_path): + download_url_to_file(meta_url, meta_list_path) + self._flist = _get_flist(self._path, meta_list_path, subset) + + def get_metadata(self, n: int) -> Tuple[str, int, int, str]: + """Get metadata for the n-th sample from the dataset. Returns filepath instead of waveform, + but otherwise returns the same fields as :py:func:`__getitem__`. + + Args: + n (int): The index of the sample + + Returns: + Tuple of the following items; + + str: + Path to audio + int: + Sample rate + int: + Speaker ID + str: + File ID + """ + file_path = self._flist[n] + file_id = _get_file_id(file_path, self._ext_audio) + speaker_id = file_id.split("-")[0] + speaker_id = int(speaker_id[3:]) + return file_path, SAMPLE_RATE, speaker_id, file_id + + def __getitem__(self, n: int) -> Tuple[Tensor, int, int, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + int: + Speaker ID + str: + File ID + """ + metadata = self.get_metadata(n) + waveform = _load_waveform(self._path, metadata[0], metadata[1]) + return (waveform,) + metadata[1:] + + def __len__(self) -> int: + return len(self._flist) + + +class VoxCeleb1Verification(VoxCeleb1): + """*VoxCeleb1* :cite:`nagrani2017voxceleb` dataset for speaker verification task. + + Each data sample contains a pair of waveforms, sample rate, the label indicating if they are + from the same speaker, and the file ids. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + meta_url (str, optional): The url of meta file that contains a list of utterance pairs + and the corresponding labels. The format of each row is ``label file_path1 file_path2". + For example: ``1 id10270/x6uYqmx31kE/00001.wav id10270/8jEAjG6SegY/00008.wav``. + ``1`` means the two utterances are from the same speaker, ``0`` means not. + (Default: ``"https://www.robots.ox.ac.uk/~vgg/data/voxceleb/meta/veri_test.txt"``) + download (bool, optional): + Whether to download the dataset if it is not found at root path. (Default: ``False``). + + Note: + The file structure of `VoxCeleb1Verification` dataset is as follows: + + └─ root/ + + └─ wav/ + + └─ speaker_id folders + + Users who pre-downloaded the ``"vox1_dev_wav.zip"`` and ``"vox1_test_wav.zip"`` files need to move + the extracted files into the same ``root`` directory. + """ + + def __init__(self, root: Union[str, Path], meta_url: str = _VERI_TEST_URL, download: bool = False) -> None: + super().__init__(root, download) + # download the veri_test.txt to get the list of training pairs and labels. + meta_list_path = os.path.join(root, os.path.basename(meta_url)) + if not os.path.exists(meta_list_path): + download_url_to_file(meta_url, meta_list_path) + self._flist = _get_paired_flist(self._path, meta_list_path) + + def get_metadata(self, n: int) -> Tuple[str, str, int, int, str, str]: + """Get metadata for the n-th sample from the dataset. Returns filepaths instead of waveforms, + but otherwise returns the same fields as :py:func:`__getitem__`. + + Args: + n (int): The index of the sample + + Returns: + Tuple of the following items; + + str: + Path to audio file of speaker 1 + str: + Path to audio file of speaker 2 + int: + Sample rate + int: + Label + str: + File ID of speaker 1 + str: + File ID of speaker 2 + """ + label, file_path_spk1, file_path_spk2 = self._flist[n] + label = int(label) + file_id_spk1 = _get_file_id(file_path_spk1, self._ext_audio) + file_id_spk2 = _get_file_id(file_path_spk2, self._ext_audio) + return file_path_spk1, file_path_spk2, SAMPLE_RATE, label, file_id_spk1, file_id_spk2 + + def __getitem__(self, n: int) -> Tuple[Tensor, Tensor, int, int, str, str]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded. + + Returns: + Tuple of the following items; + + Tensor: + Waveform of speaker 1 + Tensor: + Waveform of speaker 2 + int: + Sample rate + int: + Label + str: + File ID of speaker 1 + str: + File ID of speaker 2 + """ + metadata = self.get_metadata(n) + waveform_spk1 = _load_waveform(self._path, metadata[0], metadata[2]) + waveform_spk2 = _load_waveform(self._path, metadata[1], metadata[2]) + return (waveform_spk1, waveform_spk2) + metadata[2:] + + def __len__(self) -> int: + return len(self._flist) diff --git a/lib/python3.12/site-packages/torchaudio/datasets/yesno.py b/lib/python3.12/site-packages/torchaudio/datasets/yesno.py new file mode 100644 index 0000000000000000000000000000000000000000..baad08f1593a49af5f95658e8d4b67be6d3deeb9 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/datasets/yesno.py @@ -0,0 +1,89 @@ +import os +from pathlib import Path +from typing import List, Tuple, Union + +import torchaudio +from torch import Tensor +from torch.utils.data import Dataset +from torchaudio._internal import download_url_to_file +from torchaudio.datasets.utils import _extract_tar + + +_RELEASE_CONFIGS = { + "release1": { + "folder_in_archive": "waves_yesno", + "url": "http://www.openslr.org/resources/1/waves_yesno.tar.gz", + "checksum": "c3f49e0cca421f96b75b41640749167b52118f232498667ca7a5f9416aef8e73", + } +} + + +class YESNO(Dataset): + """*YesNo* :cite:`YesNo` dataset. + + Args: + root (str or Path): Path to the directory where the dataset is found or downloaded. + url (str, optional): The URL to download the dataset from. + (default: ``"http://www.openslr.org/resources/1/waves_yesno.tar.gz"``) + folder_in_archive (str, optional): + The top-level directory of the dataset. (default: ``"waves_yesno"``) + download (bool, optional): + Whether to download the dataset if it is not found at root path. (default: ``False``). + """ + + def __init__( + self, + root: Union[str, Path], + url: str = _RELEASE_CONFIGS["release1"]["url"], + folder_in_archive: str = _RELEASE_CONFIGS["release1"]["folder_in_archive"], + download: bool = False, + ) -> None: + + self._parse_filesystem(root, url, folder_in_archive, download) + + def _parse_filesystem(self, root: str, url: str, folder_in_archive: str, download: bool) -> None: + root = Path(root) + archive = os.path.basename(url) + archive = root / archive + + self._path = root / folder_in_archive + if download: + if not os.path.isdir(self._path): + if not os.path.isfile(archive): + checksum = _RELEASE_CONFIGS["release1"]["checksum"] + download_url_to_file(url, archive, hash_prefix=checksum) + _extract_tar(archive) + + if not os.path.isdir(self._path): + raise RuntimeError("Dataset not found. Please use `download=True` to download it.") + + self._walker = sorted(str(p.stem) for p in Path(self._path).glob("*.wav")) + + def _load_item(self, fileid: str, path: str): + labels = [int(c) for c in fileid.split("_")] + file_audio = os.path.join(path, fileid + ".wav") + waveform, sample_rate = torchaudio.load(file_audio) + return waveform, sample_rate, labels + + def __getitem__(self, n: int) -> Tuple[Tensor, int, List[int]]: + """Load the n-th sample from the dataset. + + Args: + n (int): The index of the sample to be loaded + + Returns: + Tuple of the following items; + + Tensor: + Waveform + int: + Sample rate + List[int]: + labels + """ + fileid = self._walker[n] + item = self._load_item(fileid, self._path) + return item + + def __len__(self) -> int: + return len(self._walker) diff --git a/lib/python3.12/site-packages/torchaudio/kaldi_io.py b/lib/python3.12/site-packages/torchaudio/kaldi_io.py new file mode 100644 index 0000000000000000000000000000000000000000..4d372429dcfd23fdcbe8cd0f38abef1086d8a5eb --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/kaldi_io.py @@ -0,0 +1,144 @@ +# To use this file, the dependency (https://github.com/vesis84/kaldi-io-for-python) +# needs to be installed. This is a light wrapper around kaldi_io that returns +# torch.Tensors. +from typing import Any, Callable, Iterable, Tuple + +import torch +from torch import Tensor +from torchaudio._internal import module_utils as _mod_utils + +if _mod_utils.is_module_available("numpy"): + import numpy as np + + +__all__ = [ + "read_vec_int_ark", + "read_vec_flt_scp", + "read_vec_flt_ark", + "read_mat_scp", + "read_mat_ark", +] + + +def _convert_method_output_to_tensor( + file_or_fd: Any, fn: Callable, convert_contiguous: bool = False +) -> Iterable[Tuple[str, Tensor]]: + r"""Takes a method invokes it. The output is converted to a tensor. + + Args: + file_or_fd (str/FileDescriptor): File name or file descriptor + fn (Callable): Function that has the signature (file name/descriptor) and converts it to + Iterable[Tuple[str, Tensor]]. + convert_contiguous (bool, optional): Determines whether the array should be converted into a + contiguous layout. (Default: ``False``) + + Returns: + Iterable[Tuple[str, Tensor]]: The string is the key and the tensor is vec/mat + """ + for key, np_arr in fn(file_or_fd): + if convert_contiguous: + np_arr = np.ascontiguousarray(np_arr) + yield key, torch.from_numpy(np_arr) + + +@_mod_utils.requires_module("kaldi_io", "numpy") +def read_vec_int_ark(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: + r"""Create generator of (key,vector) tuples, which reads from the ark file/stream. + + Args: + file_or_fd (str/FileDescriptor): ark, gzipped ark, pipe or opened file descriptor + + Returns: + Iterable[Tuple[str, Tensor]]: The string is the key and the tensor is the vector read from file + + Example + >>> # read ark to a 'dictionary' + >>> d = { u:d for u,d in torchaudio.kaldi_io.read_vec_int_ark(file) } + """ + + import kaldi_io + + # Requires convert_contiguous to be True because elements from int32 vector are + # sorted in tuples: (sizeof(int32), value) so strides are (5,) instead of (4,) which will throw an error + # in from_numpy as it expects strides to be a multiple of 4 (int32). + return _convert_method_output_to_tensor(file_or_fd, kaldi_io.read_vec_int_ark, convert_contiguous=True) + + +@_mod_utils.requires_module("kaldi_io", "numpy") +def read_vec_flt_scp(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: + r"""Create generator of (key,vector) tuples, read according to Kaldi scp. + + Args: + file_or_fd (str/FileDescriptor): scp, gzipped scp, pipe or opened file descriptor + + Returns: + Iterable[Tuple[str, Tensor]]: The string is the key and the tensor is the vector read from file + + Example + >>> # read scp to a 'dictionary' + >>> # d = { u:d for u,d in torchaudio.kaldi_io.read_vec_flt_scp(file) } + """ + + import kaldi_io + + return _convert_method_output_to_tensor(file_or_fd, kaldi_io.read_vec_flt_scp) + + +@_mod_utils.requires_module("kaldi_io", "numpy") +def read_vec_flt_ark(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: + r"""Create generator of (key,vector) tuples, which reads from the ark file/stream. + + Args: + file_or_fd (str/FileDescriptor): ark, gzipped ark, pipe or opened file descriptor + + Returns: + Iterable[Tuple[str, Tensor]]: The string is the key and the tensor is the vector read from file + + Example + >>> # read ark to a 'dictionary' + >>> d = { u:d for u,d in torchaudio.kaldi_io.read_vec_flt_ark(file) } + """ + + import kaldi_io + + return _convert_method_output_to_tensor(file_or_fd, kaldi_io.read_vec_flt_ark) + + +@_mod_utils.requires_module("kaldi_io", "numpy") +def read_mat_scp(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: + r"""Create generator of (key,matrix) tuples, read according to Kaldi scp. + + Args: + file_or_fd (str/FileDescriptor): scp, gzipped scp, pipe or opened file descriptor + + Returns: + Iterable[Tuple[str, Tensor]]: The string is the key and the tensor is the matrix read from file + + Example + >>> # read scp to a 'dictionary' + >>> d = { u:d for u,d in torchaudio.kaldi_io.read_mat_scp(file) } + """ + + import kaldi_io + + return _convert_method_output_to_tensor(file_or_fd, kaldi_io.read_mat_scp) + + +@_mod_utils.requires_module("kaldi_io", "numpy") +def read_mat_ark(file_or_fd: Any) -> Iterable[Tuple[str, Tensor]]: + r"""Create generator of (key,matrix) tuples, which reads from the ark file/stream. + + Args: + file_or_fd (str/FileDescriptor): ark, gzipped ark, pipe or opened file descriptor + + Returns: + Iterable[Tuple[str, Tensor]]: The string is the key and the tensor is the matrix read from file + + Example + >>> # read ark to a 'dictionary' + >>> d = { u:d for u,d in torchaudio.kaldi_io.read_mat_ark(file) } + """ + + import kaldi_io + + return _convert_method_output_to_tensor(file_or_fd, kaldi_io.read_mat_ark) diff --git a/lib/python3.12/site-packages/torchaudio/sox_effects/__init__.py b/lib/python3.12/site-packages/torchaudio/sox_effects/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..93c63cae1ded8a37226a76eec89796eb4902eafc --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/sox_effects/__init__.py @@ -0,0 +1,10 @@ +from .sox_effects import apply_effects_file, apply_effects_tensor, effect_names, init_sox_effects, shutdown_sox_effects + + +__all__ = [ + "init_sox_effects", + "shutdown_sox_effects", + "effect_names", + "apply_effects_tensor", + "apply_effects_file", +] diff --git a/lib/python3.12/site-packages/torchaudio/sox_effects/__pycache__/__init__.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/sox_effects/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb37e3aa5c091ac79173b80c404df78120bff50e Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/sox_effects/__pycache__/__init__.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/sox_effects/__pycache__/sox_effects.cpython-312.pyc b/lib/python3.12/site-packages/torchaudio/sox_effects/__pycache__/sox_effects.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba0c3ee0165fb90a0ce3984e0faaf31d267cf76a Binary files /dev/null and b/lib/python3.12/site-packages/torchaudio/sox_effects/__pycache__/sox_effects.cpython-312.pyc differ diff --git a/lib/python3.12/site-packages/torchaudio/sox_effects/sox_effects.py b/lib/python3.12/site-packages/torchaudio/sox_effects/sox_effects.py new file mode 100644 index 0000000000000000000000000000000000000000..3d64d465acada0911e86af2f74d2b58dba9352ad --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/sox_effects/sox_effects.py @@ -0,0 +1,272 @@ +import os +from typing import List, Optional, Tuple + +import torch +import torchaudio +from torchaudio._internal.module_utils import deprecated +from torchaudio.utils.sox_utils import list_effects + + +sox_ext = torchaudio._extension.lazy_import_sox_ext() + + +@deprecated("Please remove the call. This function is called automatically.") +def init_sox_effects(): + """Initialize resources required to use sox effects. + + Note: + You do not need to call this function manually. It is called automatically. + + Once initialized, you do not need to call this function again across the multiple uses of + sox effects though it is safe to do so as long as :func:`shutdown_sox_effects` is not called yet. + Once :func:`shutdown_sox_effects` is called, you can no longer use SoX effects and initializing + again will result in error. + """ + pass + + +@deprecated("Please remove the call. This function is called automatically.") +def shutdown_sox_effects(): + """Clean up resources required to use sox effects. + + Note: + You do not need to call this function manually. It is called automatically. + + It is safe to call this function multiple times. + Once :py:func:`shutdown_sox_effects` is called, you can no longer use SoX effects and + initializing again will result in error. + """ + pass + + +def effect_names() -> List[str]: + """Gets list of valid sox effect names + + Returns: + List[str]: list of available effect names. + + Example + >>> torchaudio.sox_effects.effect_names() + ['allpass', 'band', 'bandpass', ... ] + """ + return list(list_effects().keys()) + + +def apply_effects_tensor( + tensor: torch.Tensor, + sample_rate: int, + effects: List[List[str]], + channels_first: bool = True, +) -> Tuple[torch.Tensor, int]: + """Apply sox effects to given Tensor + + .. devices:: CPU + + .. properties:: TorchScript + + Note: + This function only works on CPU Tensors. + This function works in the way very similar to ``sox`` command, however there are slight + differences. For example, ``sox`` command adds certain effects automatically (such as + ``rate`` effect after ``speed`` and ``pitch`` and other effects), but this function does + only applies the given effects. (Therefore, to actually apply ``speed`` effect, you also + need to give ``rate`` effect with desired sampling rate.). + + Args: + tensor (torch.Tensor): Input 2D CPU Tensor. + sample_rate (int): Sample rate + effects (List[List[str]]): List of effects. + channels_first (bool, optional): Indicates if the input Tensor's dimension is + `[channels, time]` or `[time, channels]` + + Returns: + (Tensor, int): Resulting Tensor and sample rate. + The resulting Tensor has the same ``dtype`` as the input Tensor, and + the same channels order. The shape of the Tensor can be different based on the + effects applied. Sample rate can also be different based on the effects applied. + + Example - Basic usage + >>> + >>> # Defines the effects to apply + >>> effects = [ + ... ['gain', '-n'], # normalises to 0dB + ... ['pitch', '5'], # 5 cent pitch shift + ... ['rate', '8000'], # resample to 8000 Hz + ... ] + >>> + >>> # Generate pseudo wave: + >>> # normalized, channels first, 2ch, sampling rate 16000, 1 second + >>> sample_rate = 16000 + >>> waveform = 2 * torch.rand([2, sample_rate * 1]) - 1 + >>> waveform.shape + torch.Size([2, 16000]) + >>> waveform + tensor([[ 0.3138, 0.7620, -0.9019, ..., -0.7495, -0.4935, 0.5442], + [-0.0832, 0.0061, 0.8233, ..., -0.5176, -0.9140, -0.2434]]) + >>> + >>> # Apply effects + >>> waveform, sample_rate = apply_effects_tensor( + ... wave_form, sample_rate, effects, channels_first=True) + >>> + >>> # Check the result + >>> # The new waveform is sampling rate 8000, 1 second. + >>> # normalization and channel order are preserved + >>> waveform.shape + torch.Size([2, 8000]) + >>> waveform + tensor([[ 0.5054, -0.5518, -0.4800, ..., -0.0076, 0.0096, -0.0110], + [ 0.1331, 0.0436, -0.3783, ..., -0.0035, 0.0012, 0.0008]]) + >>> sample_rate + 8000 + + Example - Torchscript-able transform + >>> + >>> # Use `apply_effects_tensor` in `torch.nn.Module` and dump it to file, + >>> # then run sox effect via Torchscript runtime. + >>> + >>> class SoxEffectTransform(torch.nn.Module): + ... effects: List[List[str]] + ... + ... def __init__(self, effects: List[List[str]]): + ... super().__init__() + ... self.effects = effects + ... + ... def forward(self, tensor: torch.Tensor, sample_rate: int): + ... return sox_effects.apply_effects_tensor( + ... tensor, sample_rate, self.effects) + ... + ... + >>> # Create transform object + >>> effects = [ + ... ["lowpass", "-1", "300"], # apply single-pole lowpass filter + ... ["rate", "8000"], # change sample rate to 8000 + ... ] + >>> transform = SoxEffectTensorTransform(effects, input_sample_rate) + >>> + >>> # Dump it to file and load + >>> path = 'sox_effect.zip' + >>> torch.jit.script(trans).save(path) + >>> transform = torch.jit.load(path) + >>> + >>>> # Run transform + >>> waveform, input_sample_rate = torchaudio.load("input.wav") + >>> waveform, sample_rate = transform(waveform, input_sample_rate) + >>> assert sample_rate == 8000 + """ + return sox_ext.apply_effects_tensor(tensor, sample_rate, effects, channels_first) + + +def apply_effects_file( + path: str, + effects: List[List[str]], + normalize: bool = True, + channels_first: bool = True, + format: Optional[str] = None, +) -> Tuple[torch.Tensor, int]: + """Apply sox effects to the audio file and load the resulting data as Tensor + + .. devices:: CPU + + .. properties:: TorchScript + + Note: + This function works in the way very similar to ``sox`` command, however there are slight + differences. For example, ``sox`` commnad adds certain effects automatically (such as + ``rate`` effect after ``speed``, ``pitch`` etc), but this function only applies the given + effects. Therefore, to actually apply ``speed`` effect, you also need to give ``rate`` + effect with desired sampling rate, because internally, ``speed`` effects only alter sampling + rate and leave samples untouched. + + Args: + path (path-like object): + Source of audio data. + effects (List[List[str]]): List of effects. + normalize (bool, optional): + When ``True``, this function converts the native sample type to ``float32``. + Default: ``True``. + + If input file is integer WAV, giving ``False`` will change the resulting Tensor type to + integer type. + This argument has no effect for formats other than integer WAV type. + + channels_first (bool, optional): When True, the returned Tensor has dimension `[channel, time]`. + Otherwise, the returned Tensor's dimension is `[time, channel]`. + format (str or None, optional): + Override the format detection with the given format. + Providing the argument might help when libsox can not infer the format + from header or extension, + + Returns: + (Tensor, int): Resulting Tensor and sample rate. + If ``normalize=True``, the resulting Tensor is always ``float32`` type. + If ``normalize=False`` and the input audio file is of integer WAV file, then the + resulting Tensor has corresponding integer type. (Note 24 bit integer type is not supported) + If ``channels_first=True``, the resulting Tensor has dimension `[channel, time]`, + otherwise `[time, channel]`. + + Example - Basic usage + >>> + >>> # Defines the effects to apply + >>> effects = [ + ... ['gain', '-n'], # normalises to 0dB + ... ['pitch', '5'], # 5 cent pitch shift + ... ['rate', '8000'], # resample to 8000 Hz + ... ] + >>> + >>> # Apply effects and load data with channels_first=True + >>> waveform, sample_rate = apply_effects_file("data.wav", effects, channels_first=True) + >>> + >>> # Check the result + >>> waveform.shape + torch.Size([2, 8000]) + >>> waveform + tensor([[ 5.1151e-03, 1.8073e-02, 2.2188e-02, ..., 1.0431e-07, + -1.4761e-07, 1.8114e-07], + [-2.6924e-03, 2.1860e-03, 1.0650e-02, ..., 6.4122e-07, + -5.6159e-07, 4.8103e-07]]) + >>> sample_rate + 8000 + + Example - Apply random speed perturbation to dataset + >>> + >>> # Load data from file, apply random speed perturbation + >>> class RandomPerturbationFile(torch.utils.data.Dataset): + ... \"\"\"Given flist, apply random speed perturbation + ... + ... Suppose all the input files are at least one second long. + ... \"\"\" + ... def __init__(self, flist: List[str], sample_rate: int): + ... super().__init__() + ... self.flist = flist + ... self.sample_rate = sample_rate + ... + ... def __getitem__(self, index): + ... speed = 0.5 + 1.5 * random.randn() + ... effects = [ + ... ['gain', '-n', '-10'], # apply 10 db attenuation + ... ['remix', '-'], # merge all the channels + ... ['speed', f'{speed:.5f}'], # duration is now 0.5 ~ 2.0 seconds. + ... ['rate', f'{self.sample_rate}'], + ... ['pad', '0', '1.5'], # add 1.5 seconds silence at the end + ... ['trim', '0', '2'], # get the first 2 seconds + ... ] + ... waveform, _ = torchaudio.sox_effects.apply_effects_file( + ... self.flist[index], effects) + ... return waveform + ... + ... def __len__(self): + ... return len(self.flist) + ... + >>> dataset = RandomPerturbationFile(file_list, sample_rate=8000) + >>> loader = torch.utils.data.DataLoader(dataset, batch_size=32) + >>> for batch in loader: + >>> pass + """ + if not torch.jit.is_scripting(): + if hasattr(path, "read"): + raise RuntimeError( + "apply_effects_file function does not support file-like object. " + "Please use torchaudio.io.AudioEffector." + ) + path = os.fspath(path) + return sox_ext.apply_effects_file(path, effects, normalize, channels_first, format) diff --git a/lib/python3.12/site-packages/torchaudio/version.py b/lib/python3.12/site-packages/torchaudio/version.py new file mode 100644 index 0000000000000000000000000000000000000000..497be0ddcb55299abcda5ab5c65a0605f39166d5 --- /dev/null +++ b/lib/python3.12/site-packages/torchaudio/version.py @@ -0,0 +1,2 @@ +__version__ = '2.6.0+cu124' +git_version = 'd8831425203385077a03c1d92cfbbe3bf2106008'