Skip to content

afanasjev82/libretranslate-php

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LibreTranslate PHP

PHP client for LibreTranslate and LTEngine translation APIs with synchronous and asynchronous support.

Forked from jefs42/libretranslate, refactored for PHP 8.2+ with Guzzle HTTP client and async batch translations via Guzzle Promises.

Why async?

When translating the same content into multiple languages (e.g. en, et, ru, lt, lv, fi), sequential requests are slow. LTEngine uses vLLM with continuous batching, but consumer GPUs still need bounded concurrency. This client keeps async batches efficient while respecting server overload signals such as 429 Retry-After.

Translation modes

Mode Method(s) Blocks caller? Best for
Sync single translate() Yes — one blocking POST One-off translates, admin forms
Sync multi translate(['a','b',...]) Yes — one blocking POST with array payload Small fixed sets
Async batch translateBatch() Yes — blocks until all done Many texts, no per-result side effects
Pipelined async startAsyncBatch() + pump() + resolveAsyncBatch() Only at resolve() Maximum throughput — keeps GPU busy during DB I/O

The GPU-idle problem and how pump() solves it

startAsyncBatch() parks each request inside a deferred then() chain on Guzzle's global task queue. Until something calls PromiseUtils::queue()->run() (which wait() does internally), those callbacks have not fired — so requestAsync() has not even registered the cURL handle yet. Meanwhile responses already in the kernel buffer are not read because nobody is calling curl_multi_exec.

Result: LTEngine GPU goes idle between batches.

pump() fixes both in one non-blocking call:

PromiseUtils::queue()->run();   // fire deferred then() → register cURL handles
$multiHandler->tick();          // step curl_multi once → send data, read responses

With the default selectTimeout=0.0 the tick is fully non-blocking — safe to call from a save callback or a tight loop.

Changes from jefs42/libretranslate

Issue Fix
#6 Replace raw cURL Guzzle ^7.0 as HTTP backend (enables Promises for async)
#7 PascalCase method names camelCase methods: translate(), detect(), languages(), settings()
#9 Multi-mode translation broken Array input sent directly as JSON without urlencode corruption
#10 detect() returns only first result Returns complete detection results array with confidence scores

Additional changes:

  • PHP 8.2+ with strict types, typed properties, union types
  • Dual authentication: API key parameter + Authorization header (Basic/Bearer)
  • Format auto-detection: "html" or "text" inferred per-call from text content, overridable via constructor, setFormat(), or per-call parameter
  • Async batch translation: translateBatch(), translateMultiTarget(), startAsyncBatch() / resolveAsyncBatch() for pipelined throughput
  • Removed ltmanage functionality (not relevant for remote API usage)
  • Removed translateFiles() and suggest() (not supported by LTEngine)

Requirements

  • PHP 8.2+
  • ext-json
  • ext-curl (required by Guzzle)

Installation

composer require afanasjev82/libretranslate-php

Usage

Basic (synchronous)

use Afanasjev82\LibretranslatePhp\LibreTranslate;

$translator = new LibreTranslate("https://your-server.com", 9453);

# With API key (LibreTranslate standard)
$translator->setApiKey("your-api-key");

# Or with Authorization header (Basic/Bearer)
$translator->setAuth("Basic", "base64encodedcredentials");

# Translate (format auto-detected: "<p>..." → html, plain text → text)
$result = $translator->translate("Hello world", "en", "et");
echo $result; # "Tere maailm"

# Translate an HTML snippet — format auto-detected as "html"
$result = $translator->translate("<p>Hello</p>", "en", "et");

# Translate with default languages
$translator->setLanguages("en", "et");
$result = $translator->translate("Hello world");

# Detect language (returns full results array)
$detections = $translator->detect("Tere maailm");
# [{"language": "et", "confidence": 95.2}, ...]

# Get available languages
$languages = $translator->languages();
# ["en" => "English", "et" => "Estonian", ...]

# Get server settings
$settings = $translator->settings();

Format control

All translate methods auto-detect the content format from the text: an HTML opening tag anywhere in the string triggers "html", everything else is "text". Override at any level:

# Set a project-wide default (overrides auto-detect for every call)
$translator->setFormat("html");

# Per-call override (highest priority — overrides both default and auto-detect)
$translator->translate("<p>Hello</p>", "en", "et", "text");

# Via constructor — 6th argument, after guzzleOptions
$translator = new LibreTranslate("https://your-server.com", 9453, "en", "et", [], "html");

# Reset to auto-detect
$translator->setFormat(null);

Priority chain: explicit call paramsetFormat() defaultauto-detect from text

Auto-detection adds a single regex match per call — negligible overhead (microseconds) compared to HTTP roundtrip time.

Async multi-target (one text → many languages)

use Afanasjev82\LibretranslatePhp\AsyncLibreTranslate;

$translator = new AsyncLibreTranslate("https://your-server.com", 9453);
$translator->setAuth("Basic", "base64encodedcredentials");
$translator->setMaxConcurrentRequests(4); # Good starting point for a single RTX 3090
$translator->setRetryPolicy(2, 2);        # Retry transient overloads using Retry-After when present

# Translate one text into 6 languages at once — dispatched with bounded concurrency
$results = $translator->translateMultiTarget(
    "Product description here",
    ["en", "et", "ru", "lt", "lv", "fi"],
    "auto",
);
# Returns: ["en" => "...", "et" => "...", "ru" => "...", ...]

foreach ($results as $lang => $translatedText) {
    echo $lang . ": " . $translatedText . "\n";
}

Async batch (mixed items concurrently)

# Translate different texts/language pairs concurrently
$batch = [
    ["text" => "Hello world", "source" => "en", "target" => "et"],
    ["text" => "Bonjour le monde", "source" => "fr", "target" => "en"],
    ["text" => "Tere maailm", "source" => "et", "target" => "ru"],
];

$results = $translator->translateBatch($batch);
# All translations returned — the client keeps only a bounded number in flight

foreach ($results as $index => $translatedText) {
    echo $batch[$index]["target"] . ": " . $translatedText . "\n";
}

Async single promise

$promise = $translator->translateAsync("Hello world", "en", "et");

# Do other work while request is in flight...

$result = $promise->wait(); # Block when ready
echo $result; # "Tere maailm"

Pipelined async with pump() (maximum GPU throughput)

startAsyncBatch() dispatches requests without blocking. resolveAsyncBatch() blocks when you need the results. Call pump() between the two to keep LTEngine busy while your code does I/O (database writes, file saves, etc.):

use Afanasjev82\LibretranslatePhp\AsyncLibreTranslate;

$translator = new AsyncLibreTranslate("https://your-server.com", 9453);
$translator->setAuth("Basic", "base64encodedcredentials");
$translator->setMaxConcurrentRequests(4);
# selectTimeout defaults to 0.0 s — pump() is non-blocking out of the box.
# Restore Guzzle default (1.0 s) if you need a sleeping event loop instead:
# $translator->setSelectTimeout(1.0);

$batches = array_chunk($items, 20);
$pendingPromises = null;
$pendingBatch    = null;

foreach ($batches as $batch) {
    $batchItems = array_map(fn($item) => [
        "text"   => $item["text"],
        "source" => "auto",
        "target" => $item["lang"],
    ], $batch);

    # Dispatch next batch — non-blocking, requests start flowing immediately
    $newPromises = $translator->startAsyncBatch($batchItems);

    # Save previous batch to DB while new requests are in flight
    if ($pendingPromises !== null) {
        $results = $translator->resolveAsyncBatch($pendingPromises);
        foreach ($pendingBatch as $index => $item) {
            $db->save($item["id"], $results[$index]);
            $translator->pump(); # keep LTEngine busy while we write to DB
        }
    }

    $pendingPromises = $newPromises;
    $pendingBatch    = $batch;
}

# Drain the final batch
if ($pendingPromises !== null) {
    $results = $translator->resolveAsyncBatch($pendingPromises);
    foreach ($pendingBatch as $index => $item) {
        $db->save($item["id"], $results[$index]);
    }
}

This eliminates the idle gap between batches — vLLM sees a continuous stream of requests instead of burst → pause → burst. A worked example with fake DB saves is in examples/pipelined-batch.php.

Async detect batch

$detections = $translator->detectBatch([
    "Hello world",
    "Tere maailm",
    "Bonjour le monde",
]);
# Returns detection results for each text concurrently

Custom Guzzle options

$translator = new LibreTranslate(
    host: "https://your-server.com",
    port: 9453,
    source: "en",
    target: "et",
    guzzleOptions: [
        "timeout" => 300,
        "connect_timeout" => 5,
        "verify" => true,
    ],
    format: "html",  # optional default format (null = auto-detect)
);

### Overload-aware defaults

For LTEngine behind the Smartech proxy, the server may respond with `429 Too Many Requests` and a `Retry-After` header when the GPU is saturated. The client now retries those transient failures automatically.

```php
$translator = new AsyncLibreTranslate("https://your-server.com", 9453);
$translator->setMaxConcurrentRequests(4);
$translator->setRetryPolicy(2, 2); # 2 retries, 2s fallback when Retry-After is missing

On a single RTX 3090, start with 4 concurrent requests and only raise it if the server remains stable.


## Benchmark

CLI tool for measuring translation throughput in sync and async (parallel) modes with response validation.

Validates every response: checks `translatedText` field is present, non-empty, and actually differs from the source text (catches echo-back bugs).

On Linux/macOS with the `pcntl` extension, Ctrl+C triggers graceful shutdown with partial results.

### Usage (benchmark.php)

```bash
# Sync mode (sequential requests)
php benchmark/benchmark.php http://localhost:5000

# Async mode with 24 concurrent workers
php benchmark/benchmark.php http://localhost:5000 --mode=async --repeat=5 --workers=24

# With authentication
php benchmark/benchmark.php https://your-server --port=9453 --auth=Basic:<base64-credentials>

# Custom test cases from JSON file
php benchmark/benchmark.php http://localhost --test-cases=cases.json

# Export results to JSON
php benchmark/benchmark.php http://localhost --mode=async --workers=4 --export=results.json

Options

Option Default Description
--port=PORT from URL API port
--mode=MODE sync sync or async
--repeat=N 10 Times to repeat all test cases
--workers=N 4 Concurrency level (async mode)
--source=LANG auto Source language
--target=LANG et Target language
--timeout=SECONDS 120 Request timeout
--auth=TYPE:CRED Authorization header
--api-key=KEY API key
--test-cases=FILE JSON file with test cases
--export=FILE Export results to JSON
-v, --verbose off Show per-request details

Test case JSON format

[
  { "text": "Hello world", "source": "en", "target": "et" },
  { "text": "Goodbye", "source": "en", "target": "ru" }
]

Architecture

src/
├── LibreTranslate.php              # Base sync class (Guzzle HTTP)
├── AsyncLibreTranslate.php         # Async extension (Guzzle Promises)
└── Benchmark/
    ├── BenchmarkCli.php            # CLI orchestrator (args, signals, exit codes)
    ├── BenchmarkOutput.php         # Header, summary, failures, export
    ├── BenchmarkResult.php         # Single request result data class
    ├── BenchmarkRunner.php         # Sync & async execution with validation
    ├── BenchmarkStats.php          # Aggregate statistics
    ├── ProgressBar.php             # CLI progress bar
    ├── TestCaseLoader.php          # Built-in & JSON test case loading
    └── TranslationValidator.php    # Response validation (4-step check)
  • LibreTranslate — synchronous client compatible with both LibreTranslate and LTEngine APIs; format auto-detected per-call via regex, overridable via constructor, setFormat(), or per-call parameter
  • AsyncLibreTranslate — extends base class with translateAsync(), translateBatch(), translateMultiTarget(), detectBatch(), startAsyncBatch(), resolveAsyncBatch(), and pump() methods; installs a CurlMultiHandler with selectTimeout=0.0 by default so pump() is non-blocking out of the box; setSelectTimeout() restores the Guzzle default (1.0 s) when needed

LTEngine API

This library is designed to work with LTEngine, which provides a LibreTranslate-compatible API powered by OpenAI-compatible models via vLLM.

Supported endpoints:

  • POST /translate — translate text (with auto source language detection)
  • GET /languages — list available languages
  • POST /detect — detect language of given text
  • GET /settings — retrieve server settings and model information

License

GPL-3.0-or-later

Original library by Jeffrey Shilt (AGPL-3.0).

About

PHP client for LibreTranslate / LTEngine API with sync and async support

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages

  • PHP 97.4%
  • Shell 2.6%