项目文件夹

文件
2026-07-13 12:28:23 +08:00

450 行
15 KiB
Python

from __future__ import annotations
import copy
import json
import shutil
import urllib.request
import zipfile
from pathlib import Path
from threading import Lock
import packaging.version
from argostranslate import networking, settings
from argostranslate.tokenizer import BPETokenizer, SentencePieceTokenizer
from argostranslate.utils import error, info, warning
"""
## `package` module example usage
```
from argostranslate import package
# Update package definitions from remote
package.update_package_index()
# Load available packages from local package index
available_packages = package.get_available_packages()
# Download and install all available packages
for available_package in available_packages:
download_path = available_package.download()
package.install_from_path(download_path)
```
"""
# Threading logic
# Hold lock while installing or uninstalling packages
package_lock = Lock()
class IPackage:
"""A package, can be either installed locally or available from a remote package index.
Attributes:
package_path: The path to the installed package. None if not installed.
package_version: The version of the package.
argos_version: The version of Argos Translate the package is intended for.
from_code: The code of the language the package translates from.
from_name: Human readable name of the language the package translates from.
to_code: The code of the language the package translates to.
to_name: Human readable name of the language the package translates to.
links: A list of links to download the package
Packages are a zip archive of a directory with metadata.json
in its root the .argosmodel file extension. By default, an
OpenNMT CTranslate2 directory named model/ is expected in the root directory
along with a sentencepiece model named sentencepiece.model or a bpe.model
for tokenizing and Stanza data for sentence boundary detection.
Packages may also optionally have a README.md in the root.
from_code and to_code should be ISO 639 codes if applicable.
Example metadata.json
{
"package_version": "1.0",
"argos_version": "1.0",
"from_code": "en",
"from_name": "English",
"to_code": "es",
"to_name": "Spanish",
"links": ["https://example.com/en_es.argosmodel"]
}
"""
code: str
package_path: Path
package_version: str
argos_version: str
from_code: str
from_name: str
from_codes: list
to_code: str
to_codes: list
to_name: str
links: list
type: str
languages: list
dependencies: list
source_languages: list
target_languages: list
links: list[str]
def load_metadata_from_json(self, metadata):
"""Loads package metadata from a JSON object.
Args:
metadata: A json object from json.load
"""
self.code = metadata.get("code")
self.package_version = metadata.get("package_version", "")
self.argos_version = metadata.get("argos_version", "")
self.from_code = metadata.get("from_code")
self.from_name = metadata.get("from_name", "")
self.from_codes = metadata.get("from_codes", list())
self.to_code = metadata.get("to_code")
self.to_codes = metadata.get("to_codes", list())
self.to_name = metadata.get("to_name", "")
self.links = metadata.get("links", list())
self.type = metadata.get("type", "translate")
self.languages = metadata.get("languages", list())
self.dependencies = metadata.get("dependencies", list())
self.source_languages = metadata.get("source_languages", list())
self.target_languages = metadata.get("target_languages", list())
self.target_prefix = metadata.get("target_prefix", "")
# Add all package source and target languages to
# source_languages and target_languages
if self.from_code is not None or self.from_name is not None:
from_lang = dict()
if self.from_code is not None:
from_lang["code"] = self.from_code
if self.from_name is not None:
from_lang["name"] = self.from_name
self.source_languages.append(from_lang)
if self.to_code is not None or self.to_name is not None:
to_lang = dict()
if self.to_code is not None:
to_lang["code"] = self.to_code
if self.to_name is not None:
to_lang["name"] = self.to_name
self.source_languages.append(to_lang)
self.source_languages += copy.deepcopy(self.languages)
self.target_languages += copy.deepcopy(self.languages)
def get_readme(self) -> str:
"""Returns the text of the README.md in this package.
Returns:
The text of the package README.md, None
if README.md can't be read
"""
raise NotImplementedError()
def get_description(self):
raise NotImplementedError()
def __eq__(self, other):
return (
self.package_version == other.package_version
and self.argos_version == other.argos_version
and self.from_code == other.from_code
and self.from_name == other.from_name
and self.to_code == other.to_code
and self.to_name == other.to_name
)
def __repr__(self):
if len(self.from_name) > 0 and len(self.to_name) > 0:
return "{} -> {}".format(self.from_name, self.to_name)
elif self.type:
return self.type
return ""
def __str__(self):
return repr(self).replace("->", "→")
class Package(IPackage):
"""An installed package"""
def __init__(self, package_path: Path):
"""Create a new Package from path.
Args:
package_path: Path to installed package directory.
"""
if type(package_path) == str:
# Convert strings to pathlib.Path objects
package_path = Path(package_path)
self.package_path = package_path
metadata_path = package_path / "metadata.json"
if not metadata_path.exists():
raise FileNotFoundError(
"Error opening package at " + str(metadata_path) + " no metadata.json"
)
with open(metadata_path) as metadata_file:
metadata = json.load(metadata_file)
self.load_metadata_from_json(metadata)
""" As of spacy multilingual support, the sbd package shall depend on the Argos package's content"""
minisbd_package = package_path / "minisbd"
stanza_package = package_path / "stanza"
spacy_package = package_path / "spacy"
if minisbd_package.exists(): # MiniSBD model within the package (prioritize over stanza if both are there)
self.packaged_sbd_path = minisbd_package
elif stanza_package.exists(): # Stanza tokenizer within the package
self.packaged_sbd_path = stanza_package
elif (
spacy_package.exists()
): # Explicit/language-specific spacy model within the package
self.packaged_sbd_path = spacy_package
else: # None if no sbd package embedded in the argos package (will default to cache)
self.packaged_sbd_path = None
sp_model_path = package_path / "sentencepiece.model"
bpe_model_path = package_path / "bpe.model"
if sp_model_path.exists():
self.tokenizer = SentencePieceTokenizer(sp_model_path)
elif bpe_model_path.exists():
self.tokenizer = BPETokenizer(bpe_model_path, self.from_code, self.to_code)
def update(self):
"""Update the package if a newer version is available."""
for available_package in get_available_packages():
if (
available_package.from_code == self.from_code
and available_package.to_code == self.to_code
):
if packaging.version.parse(
available_package.package_version
) > packaging.version.parse(self.package_version):
new_package_path = available_package.download()
uninstall(self)
install_from_path(new_package_path)
def get_readme(self) -> str | None:
"""Returns the text of the README.md in this package.
Returns:
The text of the package README.md, None
if README.md can't be read
"""
readme_path = self.package_path / "README.md"
if not readme_path.exists():
return None
with open(readme_path, "r") as readme_file:
return readme_file.read()
def get_description(self):
return self.get_readme()
def install_from_path(path: Path):
"""Install a package file (zip archive ending in .argosmodel).
Args:
path: The path to the .argosmodel file to install.
"""
with package_lock:
if not zipfile.is_zipfile(path):
raise Exception("Not a valid Argos Model (must be a zip archive)")
with zipfile.ZipFile(path, "r") as zipf:
zipf.extractall(path=settings.package_data_dir)
# Clear language cache after package installation
from argostranslate.translate import get_installed_languages
get_installed_languages.cache_clear()
class AvailablePackage(IPackage):
"""A package available for download and installation"""
def __init__(self, metadata):
"""Creates a new AvailablePackage from a metadata object"""
self.load_metadata_from_json(metadata)
def download(self) -> Path:
"""Downloads the AvailablePackage and returns its path"""
filename = argospm_package_name(self) + ".argosmodel"
"""
# Install sbd package if needed
if self.type == "translate" and not settings.stanza_available:
if (
len(list(filter(lambda x: x.type == "sbd", get_installed_packages())))
== 0
):
# No sbd packages are installed, download all available
sbd_packages = filter(
lambda x: x.type == "sbd", get_available_packages()
)
for sbd_package in sbd_packages:
download_path = sbd_package.download()
install_from_path(download_path)
"""
filepath = settings.downloads_dir / filename
if not filepath.exists():
data = networking.get_from(self.links)
if data is None:
raise Exception(f"Download failed for {str(self)}")
with open(filepath, "wb") as f:
f.write(data)
return filepath
def install(self):
download_path = self.download()
install_from_path(download_path)
download_path.unlink()
def get_description(self):
return "{}{}".format(self.from_name, self.to_name)
def uninstall(pkg: Package):
"""Uninstalls a package.
Args:
pkg: The package to uninstall
"""
with package_lock:
shutil.rmtree(pkg.package_path)
# Clear language cache after package uninstallation
from argostranslate.translate import get_installed_languages
get_installed_languages.cache_clear()
def get_installed_packages(path: Path = None) -> list[Package]:
"""Return a list of installed Packages
Looks for packages in <home>/.argos-translate/local/share/packages by
default. Will also look in the directory specified
in the ARGOS_PACKAGE_DIR environment variable
if it is set.
Args:
path: Path to look for installed package directories in.
Defaults to the path in settings module.
"""
with package_lock:
to_return = []
packages_path = settings.package_dirs if path is None else path
for directory in packages_path:
for path in directory.iterdir():
if path.is_dir():
to_return.append(Package(path))
return to_return
def update_package_index():
"""Downloads remote package index"""
with package_lock:
try:
response = urllib.request.urlopen(settings.remote_package_index)
except Exception as err:
error(err)
return
data = response.read()
with open(settings.local_package_index, "wb") as f:
f.write(data)
def get_available_packages() -> list[AvailablePackage]:
"""Returns a list of AvailablePackages from the package index."""
try:
with open(settings.local_package_index) as index_file:
index = json.load(index_file)
packages = []
for metadata in index:
package = AvailablePackage(metadata)
packages.append(package)
"""
# If stanza not available filter for sbd available
if not settings.stanza_available:
installed_and_available_packages = packages + get_installed_packages()
sbd_packages = list(
filter(lambda x: x.type == "sbd", installed_and_available_packages)
)
sbd_available_codes = set()
for sbd_package in sbd_packages:
sbd_available_codes = sbd_available_codes.union(
sbd_package.from_codes
)
packages = list(
filter(lambda x: x.from_code in sbd_available_codes, packages)
)
return packages + sbd_packages
"""
return packages
except FileNotFoundError:
update_package_index()
return get_available_packages()
def argospm_package_name(pkg: IPackage) -> str:
"""Gets argospm name of an IPackage.
Args:
The package to get the name of.
Returns:
Package name for argospm
"""
to_return = pkg.type
if pkg.from_code and pkg.to_code:
to_return += "-" + pkg.from_code + "_" + pkg.to_code
return to_return
def install_package_for_language_pair(from_code: str, to_code: str) -> bool:
"""Installs the necessary package to translate between a pair of languages
Args:
from_code (str): The ISO 639 code for the language being translated from
to_code (str): The ISO 639 code for the language being translated to
Returns:
True if the package was installed successfully,
False if the installation failed or was not possible
"""
available_packages = get_available_packages()
try:
package_to_install = next(
filter(
lambda x: x.from_code == from_code and x.to_code == to_code,
available_packages,
)
)
except StopIteration:
return False
install_from_path(package_to_install.download())
return True
def load_available_packages() -> list[Package]:
"""Deprecated 1.2, use get_available_packages"""
info(
"Using deprecated function load_available_packages, use get_available_packages instead"
)
return get_available_packages()