Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 63 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A command-line interface for local translation powered by plamo-3-translate-2b, a PFN™ translation model

Built with PLaMo. The example translations in this README are output of PLaMo Translate models; see [License](#license).

## Features

- Translate text between 16+ languages including Japanese, English, Chinese, Korean, and more
Expand All @@ -11,23 +13,41 @@ A command-line interface for local translation powered by plamo-3-translate-2b,

## Installation

### For macOS

`plamo-translate` currently installs on Python 3.10 through 3.14 on macOS.
No additional workaround is required for `sentencepiece` on Python 3.13 or 3.14 with current upstream releases.
`plamo-translate` installs on Python 3.10 through 3.14:

```sh
pip install plamo-translate
```

#### [`uv tool`](https://docs.astral.sh/uv/concepts/tools/)

If you use [`uv`](https://github.com/astral-sh/uv) as a package manager rather than `pip`, you can install `plamo-translate` into an isolated environment:
If you use [`uv`](https://github.com/astral-sh/uv) as a package manager rather than `pip`, you can install
`plamo-translate` into an isolated environment with [`uv tool`](https://docs.astral.sh/uv/concepts/tools/):

```sh
uv tool install -p 3.14 plamo-translate
```

| Platform | Default backend | Notes |
| --- | --- | --- |
| Apple Silicon Mac, macOS 14 or later | MLX | Installs MLX. The default model needs about 2.6 GB of memory while it loads. |
| Linux, Intel Mac | llama.cpp | Installs without MLX; install `llama-server` as described below. |

MLX needs macOS 14 or later: on an Apple Silicon Mac with macOS 13, pip installs an old release without PLaMo 3
Translate. Windows is not supported; use WSL, where the CLI runs as on Linux.

### Linux and Intel Macs

These platforms translate with [llama.cpp](https://github.com/ggml-org/llama.cpp). Install `llama-server` b11960 or
newer, for example with [Homebrew](https://brew.sh) (`brew install llama.cpp`), or build the tested revision from a
checkout of this repository with `scripts/build_llama_cpp.sh`. CI builds that revision on Ubuntu and translates with
it on the CPU; Intel Macs and Homebrew on Linux have not been tested. Put `llama-server` on `PATH`, or point to it:

```sh
export PLAMO_TRANSLATE_CLI_LLAMA_SERVER=/path/to/llama.cpp/build/bin/llama-server
plamo-translate --from English --to Japanese --input "Hello"
```

The first run downloads the default GGUF (1.81 GB); see [PLaMo 3 on llama.cpp](#plamo-3-on-llamacpp).

## Development

```sh
Expand All @@ -41,7 +61,7 @@ source .venv/bin/activate
- Common dependencies:
- mcp[cli]
- numba
- On macOS:
- On Apple Silicon Macs (macOS 14 or later):
- mlx-lm
- For the llama.cpp backend: `llama-server` from llama.cpp b11960 (0.8.0) or newer, for example `brew install llama.cpp`;
measurements used `scripts/build_llama_cpp.sh`
Expand All @@ -54,10 +74,10 @@ Without `--model`, the MLX backend translates with
[`pfnet/plamo-3-translate-2b`](https://huggingface.co/pfnet/plamo-3-translate-2b).
The first run downloads its original weights (5.76 GB) into the Hugging Face cache, and each
load quantizes them to 4bit or to the precision given with `--precision`; `bf16` skips quantization
and computes in FP16. The model is distributed under the
[PLaMo Community License](https://plamo.preferredai.jp/info/plamo-community-license-en).
and computes in FP16. The model is distributed under the PLaMo Community License; see [License](#license).

With `--backend-type llama.cpp`, the default is the official GGUF
With the llama.cpp backend (`--backend-type llama.cpp`, the default on Linux and Intel Macs), the default model is the
official GGUF
[`pfnet/plamo-3-translate-2b-GGUF`](https://huggingface.co/pfnet/plamo-3-translate-2b-GGUF) at Q4_K_M (1.81 GB),
which loads without conversion. See [PLaMo 3 on llama.cpp](#plamo-3-on-llamacpp).

Expand All @@ -77,7 +97,6 @@ If you don't specify them, the input/output language will be automatically selec

```sh
$ plamo-translate
Loading models...done!
Interactive mode enabled. Type your input below (Ctrl+D to exit).
> こんにちは、お元気ですか?
Hello, how are you?
Expand Down Expand Up @@ -117,7 +136,6 @@ You can also use the interactive mode with the server:

```sh
$ plamo-translate
Loading models...done!
Interactive mode enabled. Type your input below (Ctrl+D to exit).
> 家計は火の車だ
The household finances are in dire straits.
Expand Down Expand Up @@ -400,8 +418,9 @@ and experimental groups below describe PLaMo 2.

## Server Backends

- mlx: Optimized for macOS with Apple Silicon (default on macOS)
- llama.cpp: Local GGUF inference with the native `llama-server`, exposed through the same CLI and MCP tool.
- mlx: Optimized for Apple Silicon (default there)
- llama.cpp: Local GGUF inference with the native `llama-server`, exposed through the same CLI and MCP tool
(default on Linux and Intel Macs).
It starts in about a second (two with the draft model that servers and interactive mode load), and with the
draft it decodes PLaMo 3 at about 130 tok/s on M1 Max

Expand Down Expand Up @@ -483,7 +502,8 @@ must be removed once a llama.cpp release honors `yarn_truncate`.

### llama.cpp runtime and PLaMo 2 GGUFs

Build the tested runtime from this checkout (requires Git, CMake and Xcode Command Line Tools):
Build the tested runtime from this checkout (requires Git, CMake and a C/C++ compiler: Xcode Command Line Tools on
macOS, for example `build-essential` on Ubuntu):

```sh
bash scripts/build_llama_cpp.sh
Expand Down Expand Up @@ -579,7 +599,8 @@ content and do not interpret it as general translation accuracy.
- --input TEXT Input text to translate
- --from TEXT Input language for translation (default: English|Japanese, chosen from the text)
- --to TEXT Output language for translation (default: the other of English and Japanese)
- --backend-type Server backend: [mlx, llama.cpp] (default: reuse a running server, otherwise mlx)
- --backend-type Server backend: [mlx, llama.cpp] (default: reuse a running server, otherwise mlx on Apple Silicon
and llama.cpp elsewhere)
- --model MLX: local model directory or Hugging Face repository (default: pfnet/plamo-3-translate-2b).
llama.cpp: GGUF file or `<user>/<repo>[:QUANT or file.gguf]` (default: pfnet/plamo-3-translate-2b-GGUF:Q4_K_M)
- --draft-model llama.cpp draft GGUF or repository, or `none` (default: pfnet/plamo-3-translate-0.8b-GGUF:Q4_0
Expand Down Expand Up @@ -609,3 +630,28 @@ You can configure the following parameters using environment variables:
```sh
bash scripts/deploy.sh
```

## License

The source code of `plamo-translate` is licensed under the [Apache License 2.0](LICENSE).

The models it downloads are distributed under the PLaMo Community License
([English](https://plamo.preferredai.jp/info/plamo-community-license-en),
[Japanese](https://plamo.preferredai.jp/info/plamo-community-license-ja)), not under the Apache License. This includes
[`pfnet/plamo-3-translate-2b`](https://huggingface.co/pfnet/plamo-3-translate-2b) (MLX default),
[`pfnet/plamo-3-translate-2b-GGUF`](https://huggingface.co/pfnet/plamo-3-translate-2b-GGUF) (llama.cpp default),
[`pfnet/plamo-3-translate-0.8b-GGUF`](https://huggingface.co/pfnet/plamo-3-translate-0.8b-GGUF) (draft model that
llama.cpp servers and interactive mode download) and the PLaMo 2 Translate models. Read the license before using
them. In short, as summarized on the model cards:

- Personal and non-commercial use, including academic use, is free and needs no registration.
- Commercial use is free when the revenue or sales of the user or its affiliates in the most recent fiscal year were
1 billion yen or less, after registering through the [PLaMo Commercial Use Registration Form](https://forms.gle/mTL8tBLrMYXKNZD56). The license
counts use within an organization as commercial use. Above 1 billion yen, a separate agreement with PFN is needed
([inquiry page](https://translate.preferredai.jp/llm/contact/)).
- Translations that you publish must be clearly indicated as output of the model (Article 5).
- Providing the models or their output to others requires a copy of the license for the recipients, "Built with
PLaMo" in related websites, interfaces or documentation, and "PLaMo" in the names of models you derive from them
(Article 4).

The license text is authoritative; this summary is not.
2 changes: 2 additions & 0 deletions benchmarks/llama_cpp_plamo3_20261105/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# PLaMo 3 Translate 2B on llama.cpp, 2026-11-05

Built with PLaMo.

Apple M1 Max, 32 GPU cores, 64 GB. llama.cpp b11960 (`8642541b255bfb5dc39cd351a178be09013a55d6`, the revision of
Homebrew's llama.cpp 0.8.0) built with `scripts/build_llama_cpp.sh`, Release, Metal. Models:
`pfnet/plamo-3-translate-2b-GGUF` at `f2b4599d` and the draft `pfnet/plamo-3-translate-0.8b-GGUF` at `03891e90`.
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/llama_cpp_plamo3_yarn_20261105/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# PLaMo 3 YaRN range on llama.cpp, 2026-11-05

Built with PLaMo.

Apple M1 Max, 64 GB. llama.cpp b11960 built with `scripts/build_llama_cpp.sh`, as in the
[llama.cpp measurements](../llama_cpp_plamo3_20261105/README.md). Models: `pfnet/plamo-3-translate-2b` (original
weights for MLX; BF16 GGUF converted with `scripts/prepare_llama_cpp.py`) and the official Q4_K_M GGUF.
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/plamo3_mlx_20261104/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# PLaMo 3 Translate 2B on fused Metal kernels, 2026-11-04

Built with PLaMo.

Source: `pfnet/plamo-3-translate-2b` at `355637cc04db67195ba87a97da85eaf1d52a22e8`. The measured copy
has byte-identical weights, configuration and tokenizer. On 2026-11-05 the downloaded repository reproduced
the same outputs at 161 and 167 tok/s on the shared machine.
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ classifiers = [
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: MacOS",
"Operating System :: POSIX :: Linux",
"Topic :: Utilities",
"Environment :: MacOS X",
"Environment :: Console",
Expand All @@ -40,8 +41,9 @@ dependencies = [
"httpx>=0.27",
"huggingface-hub>=1,<3",
"mcp[cli]>=1.9.2,<2",
"numba>=0.60.0",
"mlx-lm>=0.31.2,<0.32 ; sys_platform == 'darwin'",
# Only PLaMo's tokenizer code needs numba, and only the MLX backend runs it; Intel Macs have no current wheels.
"numba>=0.60.0 ; sys_platform == 'darwin' and platform_machine == 'arm64'",
"mlx-lm>=0.31.2,<0.32 ; sys_platform == 'darwin' and platform_machine == 'arm64'",
"transformers>=5,<6",
]

Expand Down
23 changes: 13 additions & 10 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail

version="$(uv version --short)"
# PyPI accepts a new file name for an existing version, and files can never be replaced: bump the version first.
if curl -sf "https://pypi.org/pypi/plamo-translate/${version}/json" > /dev/null; then
echo "plamo-translate ${version} is already on PyPI; bump the version in pyproject.toml and uv.lock." >&2
exit 1
fi

rm -rf dist
ARCHFLAGS="-arch arm64" MACOSX_DEPLOYMENT_TARGET="11.0" \
# The package is pure Python: publish its py3-none-any wheel for every platform. Dependency markers install MLX on
# Apple Silicon only; elsewhere the CLI uses llama.cpp.
uv build --wheel

WHEEL_FILENAME=$(ls dist/plamo_translate-*.whl)
uv run -m wheel tags \
--python-tag py3 \
--abi-tag none \
--platform-tag macosx_11_0_arm64 \
${WHEEL_FILENAME}
rm -rf ${WHEEL_FILENAME}

uv run twine upload dist/*
uv run twine upload dist/*
89 changes: 82 additions & 7 deletions src/plamo_translate/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
import argparse
import asyncio
import atexit
import importlib.util
import json
import logging
import multiprocessing
import os
import readline
import platform
try:
import readline
except ImportError: # Windows, which main() refuses with a message
readline = None
import signal
import subprocess
import sys
Expand All @@ -25,7 +30,12 @@
resolve_model,
same_model,
)
from plamo_translate.servers.mlx.pfml3 import is_plamo3_checkpoint
from plamo_translate.servers.mlx.pfml3 import (
CHECKPOINT_FILES,
checkpoint_complete,
checkpoint_config,
is_plamo3_checkpoint,
)
from plamo_translate.servers.utils import (
DEFAULT_MODEL_NAME,
PLAMO_TRANSLATE_CLI_REPETITION_CONTEXT_SIZE,
Expand All @@ -35,8 +45,6 @@
verify_mcp_server_ready,
)

os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -84,6 +92,58 @@ def fetch_gguf(name: str) -> str:
return resolve_model(name)


def fetch_mlx_checkpoint(name: str | None) -> None:
"""Download an original PLaMo 3 checkpoint before its server starts, with a message and progress bars.

Inside the server the 5.8 GB download of the default model was silent and counted against the startup wait.
PLaMo 2 checkpoints are still fetched by the MLX loader.
"""
if not name or os.environ.get("PLAMO_TRANSLATE_CLI_USE_MOCK_SERVER") == "1" or Path(name).expanduser().is_dir():
return
from huggingface_hub import snapshot_download
from huggingface_hub.errors import HFValidationError, LocalEntryNotFoundError

try:
if checkpoint_complete(snapshot_download(name, allow_patterns=CHECKPOINT_FILES, local_files_only=True)):
return
except LocalEntryNotFoundError:
pass
except HFValidationError:
raise SystemExit(f"{name} is neither a checkpoint directory nor a Hugging Face repository.") from None
config = checkpoint_config(name)
if config is None or config.get("model_type") != "plamo3" or config.get("quantization"):
# The server reports missing and quantized checkpoints, and loads PLaMo 2 its own way.
return
print(f"Downloading {name}{download_size(name)} into the Hugging Face cache...", file=sys.stderr, flush=True)
try:
snapshot_download(name, allow_patterns=CHECKPOINT_FILES)
except KeyboardInterrupt:
# Exit at once rather than waiting for the download threads to finish their files.
print("\nDownload interrupted; the next run resumes it.", file=sys.stderr, flush=True)
os._exit(130)
except Exception as exc:
raise SystemExit(f"Could not download {name}: {exc}") from exc


def download_size(name: str) -> str:
"""" (5.8 GB)" for the checkpoint files of a repository; the progress bar only counts files."""
from fnmatch import fnmatchcase

from huggingface_hub import HfApi

try:
files = HfApi().model_info(name, files_metadata=True).siblings or []
except Exception:
return ""
size = sum(f.size or 0 for f in files if any(fnmatchcase(f.rfilename, p) for p in CHECKPOINT_FILES))
return f" ({size / 1e9:.1f} GB)" if size else ""


def mlx_available() -> bool:
"""MLX runs on Apple Silicon only, and is installed there only."""
return sys.platform == "darwin" and platform.machine() == "arm64" and importlib.util.find_spec("mlx") is not None


def check_server_running() -> bool:
config = update_config()
if "port" not in config:
Expand Down Expand Up @@ -144,6 +204,8 @@ def run_translate(args: argparse.Namespace) -> None:

server = None
if not check_server_running():
if backend_type == "mlx":
fetch_mlx_checkpoint(os.environ.get("PLAMO_TRANSLATE_CLI_MODEL_NAME"))
if args.interactive:
show_progress = True
else:
Expand Down Expand Up @@ -231,6 +293,8 @@ def run_translate(args: argparse.Namespace) -> None:


def main() -> None:
if sys.platform == "win32":
sys.exit("plamo-translate does not support Windows; run it in WSL, where it uses llama.cpp.")
global_parser = argparse.ArgumentParser(add_help=False)
global_parser.add_argument(
"--version",
Expand Down Expand Up @@ -263,7 +327,7 @@ def main() -> None:
type=str,
default=None,
choices=["mlx", "llama.cpp"],
help="Server backend (default: reuse a running server, otherwise mlx)",
help="Server backend (default: reuse a running server, otherwise mlx on Apple Silicon and llama.cpp elsewhere)",
)
global_parser.add_argument(
"--precision",
Expand Down Expand Up @@ -354,7 +418,16 @@ def main() -> None:
args.stream = not args.no_stream
config = update_config()
running = check_server_running()
args.backend_type = args.backend_type or (config.get("backend_type", "mlx") if running else "mlx")
mock = os.environ.get("PLAMO_TRANSLATE_CLI_USE_MOCK_SERVER") == "1"
if not running and args.backend_type is None:
# The mock server stands in for either backend; keep the MLX default so tests do not depend on the host.
args.backend_type = "mlx" if mock or mlx_available() else "llama.cpp"
args.backend_type = args.backend_type or config.get("backend_type", "mlx")
if args.backend_type == "mlx" and not running and not mock and not mlx_available():
parser.error(
"MLX needs an Apple Silicon Mac with macOS 14 or later. Use --backend-type llama.cpp, which runs "
"llama-server from llama.cpp (see README)."
)
if args.backend_type == "llama.cpp" and args.precision is not None:
parser.error("For llama.cpp, select precision with the GGUF passed to --model; --precision is for MLX.")
if args.backend_type != "llama.cpp" and args.draft_model is not None:
Expand All @@ -367,7 +440,7 @@ def main() -> None:
if model_name is None:
model_name = DEFAULT_MODEL_NAME if args.backend_type == "mlx" else DEFAULT_GGUF_MODEL
# The mock server ignores the model and records its name as "mock".
llama = args.backend_type == "llama.cpp" and os.environ.get("PLAMO_TRANSLATE_CLI_USE_MOCK_SERVER") != "1"
llama = args.backend_type == "llama.cpp" and not mock
if llama:
try:
model_name = canonical_model(model_name)
Expand Down Expand Up @@ -446,6 +519,8 @@ def main() -> None:
if check_server_running():
print("MCP server is already running. Skipping server start.")
sys.exit(0)
if args.backend_type == "mlx":
fetch_mlx_checkpoint(model_name)
while not check_server_running():
try:
logger.info("Starting server...")
Expand Down
Loading
Loading