HybridTM can run as a local JSON-over-HTTP server, for long-lived integrations (editor plugins, CAT tools) that want to keep an instance, and its loaded embedding model, open across many requests instead of paying the load cost on every invocation. It exposes the same HybridTM/HybridTMFactory operations documented in the earlier guides.
hybridtm serve [-port <number>] [-network]
hybridtm stop [-port <number>]serve spawns a detached background process listening on port 8050 by default; pass -port to use a different one. By default it binds 127.0.0.1 only, so it isn't reachable from other machines; pass -network to change that (there's no per-command authentication, so only do this on a trusted network). stop sends it a shutdown request; pass the same -port you started it with if it wasn't the default.
HybridTMServer is also exported from the package, for running the same server directly inside a Node.js process instead of launching it via the CLI:
import { HybridTMServer } from 'hybridtm';
const server = new HybridTMServer(8050); // binds 127.0.0.1 only
// const server = new HybridTMServer(8050, '0.0.0.0'); // reachable from other machines
await server.start();
// ... your application runs; anything that can reach this process
// (including code running in the same process) can now POST JSON
// commands to the address/port above
await server.stop();start()/stop() return Promises that resolve once the HTTP listener is actually up/down. The constructor's first argument is the port; the second is the bind address, defaulting to 127.0.0.1. A few properties worth knowing:
- It runs in-process: it shares your application's event loop and lifetime, and stops existing once that process exits.
- You choose the port (and host) via the constructor, so you can run several independent servers side by side, for example one per test, or one per plugin host.
- It's a plain object, not a singleton. The registry it reads from,
HybridTMFactory'sinstances.json, is shared machine-wide by everyHybridTMServer/HybridTM/HybridTMFactoryinstance. Creating an instance under a givennamefrom one server/process makes it visible toopenon any other.
The rest of this guide (the request/response envelope and every command below) describes the server's behavior regardless of how it was started.
The server exposes a single endpoint: POST a JSON object with a command field (plus whatever fields that command needs) to http://127.0.0.1:8050, and read a JSON object back.
curl -s -X POST http://127.0.0.1:8050 \
-H 'Content-Type: application/json' \
-d '{"command":"list"}'Every response has one of two shapes:
The server keeps a HybridTM instance (and its loaded embedder) in memory across requests, until it's explicitly closed. Every data command below (import, match, backup, batchTranslate, storeXliffUnit, concordanceSearch, semanticSearch, semanticTranslationSearch, close) requires the instance to already be open. restore requires it too, unless create is used (see below).
{ "command": "open", "name": "project" }Loads the instance via HybridTMFactory.getInstance and keeps it open. Calling open again for an already-open instance is a no-op success. Fails if no instance is registered under that name (run create first).
{ "command": "close", "name": "project" }Closes the instance (flushing LanceDB and disposing the embedder) and drops it from the server's open-instance set. Fails if the instance isn't currently open.
create, remove, and list manage the registered instances and don't require the instance to be open first.
{ "command": "create", "name": "project", "path": "./project.lancedb", "model": "large" }| Field | Required | Description |
|---|---|---|
name |
yes | Name to register the instance under |
path |
yes | Directory where the instance's LanceDB data will live; created if missing |
model |
no | compact, standard, large (default), or a literal Hugging Face model id |
Registers the instance and closes it immediately. create does not add the instance to the server's open-instance set; send open afterward before using it.
{ "command": "remove", "name": "project" }If the instance is currently open, it is closed first (so its LanceDB directory isn't deleted out from under an open handle), then the registry entry and data directory are permanently deleted.
{ "command": "list" }Payload is HybridTMInstanceMetadata[], the same array HybridTMFactory.listInstances() returns (name, filePath, modelName, createdAt).
Importing a large XLIFF/TMX/SDLTM file (generating an embedding per segment), matching a file against an instance (running a semantic search per segment), backing up an instance, or restoring one can all take a while, so none of these commands block the request until the work finishes. Each starts the operation in the background and returns a ticket immediately; poll status to find out when it's done.
{
"command": "import",
"name": "project",
"file": "./translations/project.xlf",
"type": "xliff",
"minState": "translated",
"noMetadata": false
}| Field | Required | Description |
|---|---|---|
name |
yes | Instance to import into (must already be open) |
file |
yes | File to import |
type |
no | xliff, tmx, or sdltm; inferred from the file extension when omitted |
minState |
no | Minimum segment state to import (see 02 · Importing Data) |
noMetadata |
no | true to skip extracting notes/metadata/extension attributes (default: false) |
Empty XLIFF targets are skipped automatically, unless the segment's @state is final.
Returns immediately:
{ "status": "success", "payload": { "ticket": "b6e7…" } }Once the job finishes, status returns { "imported": <count> } as the result (see below).
{
"command": "match",
"name": "project",
"file": "./new-content.xlf",
"output": "./new-content.matches.xlf",
"quality": 60,
"limit": 5
}| Field | Required | Description |
|---|---|---|
name |
yes | Instance to search against (must already be open) |
file |
yes | XLIFF file to enrich |
quality |
yes | Minimum hybrid match score, 0-100, written to the output's @matchQuality attribute |
output |
no | Output path; defaults to <file-without-extension>.matches.xlf next to the input |
limit |
no | Max candidates per segment; defaults to semanticTranslationSearch's own default (10) when omitted |
Every segment is processed except ones with state="final".
For every segment, it runs semanticTranslationSearch and adds a <mtc:match> (Translation Candidates module, urn:oasis:names:tc:xliff:matches:2.0) entry to that unit's <mtc:matches> block:
<unit id="auth.signin">
<mtc:matches>
<mtc:match matchQuality="92" origin="project" ref="#seg1" similarity="85" type="tm">
<source>Sign in</source>
<target>Iniciar sesión</target>
</mtc:match>
</mtc:matches>
...
</unit>matchQuality is the overall (hybrid) score; similarity here is source-to-source text similarity only. origin is the instance name; ref points at the specific segment the candidate applies to. Inline codes (<ph>, etc.) in the matched source/target are converted to XLIFF's own <ph>/<originalData> representation. Once the job finishes, status's result is { "segmentsProcessed": n, "segmentsWithMatches": n, "totalMatches": n, "outputPath": "…" }.
{ "command": "backup", "name": "project", "file": "./project-backup.xml" }| Field | Required | Description |
|---|---|---|
name |
yes | Instance to back up (must already be open) |
file |
yes | Output XML file path |
Once the job finishes, status's result is { "backedUp": <count> }.
{ "command": "restore", "file": "./project-backup.xml", "name": "project" }{
"command": "restore",
"file": "./project-backup.xml",
"create": true,
"path": "./project.lancedb",
"name": "project",
"model": "large"
}| Field | Required | Description |
|---|---|---|
file |
yes | Backup XML file to restore |
name |
see below | Existing open instance to restore into (appending its entries). With create, the name for the new instance (default: the name recorded in the backup file) |
create |
no | true to create a fresh instance before restoring, instead of appending to an already-open one |
path |
with create |
Directory for the new instance's LanceDB data |
model |
no | Embedding model for the new instance when create is used (default: the model recorded in the backup file) |
Without create, name is required and the instance must already be open. With create, the new instance is registered and left open on the server (no separate open call needed); name/model fall back to whatever the backup file itself recorded if omitted. Once the job finishes, status's result is { "restored": <count>, "name": "<instance name>" }.
{ "command": "status", "ticket": "b6e7…" }While the job is still running:
{ "status": "success", "payload": { "ticket": "b6e7…", "jobStatus": "running" } }Once it finishes:
{ "status": "success", "payload": { "ticket": "b6e7…", "jobStatus": "completed", "result": { "imported": 214 } } }
{ "status": "success", "payload": { "ticket": "b6e7…", "jobStatus": "failed", "reason": "…" } }A finished job is removed from the server's memory the first time status reads it; poll once you see completed/failed, not repeatedly. Polling again with the same ticket afterward returns { "status": "failed", "reason": "Unknown ticket" }, same as an unrecognized or expired ticket.
These require the instance to be open and return their result inline (not ticketed) since a single query is expected to be fast. See 03 · Search and Filtering for MetadataFilter/TranslationSearchFilters fields and how ranking works.
{
"command": "concordanceSearch",
"name": "project",
"textFragment": "error",
"language": "en",
"limit": 100,
"filters": { "minState": "translated" }
}limit/filters are optional; omitting limit uses concordanceSearch's own default (100). The payload is the JSON form of Map<string, XMLElement>[]: one object per matching unit, mapping language to the serialized XML element:
[
{ "en": "<source>Connection error</source>", "es": "<target>Error de conexión</target>" }
]{
"command": "semanticSearch",
"name": "project",
"queryText": "settings",
"language": "en",
"limit": 10,
"filters": { "contextIncludes": ["ui.settings"] }
}limit/filters are optional (default limit: 10). Payload is SearchResult[], already plain JSON.
{
"command": "semanticTranslationSearch",
"name": "project",
"searchStr": "Reset password",
"srcLang": "en",
"tgtLang": "es",
"similarity": 55,
"limit": 10,
"filters": { "target": { "states": ["reviewed", "final"] } }
}similarity is required. limit/filters are optional. Payload is Match[], serialized via Match.toJSON(): source/target come through as XML strings alongside origin, type, similarity, semantic, fuzzy, and properties.
For editors/CAT tools that want to persist a single confirmed <unit> as the user works, without writing and importing a whole file.
{
"command": "storeXliffUnit",
"name": "project",
"unit": "<unit id=\"u1\"><segment><source>Sign in</source><target>Iniciar sesión</target></segment></unit>",
"fileId": "demo.xlf",
"original": "demo.docx",
"srcLang": "en",
"tgtLang": "es"
}This command returns { "status": "success", "payload": {} } directly on success.
For editors/CAT tools that want to enrich a batch of <unit> elements with TM match candidates in one go.
{
"command": "batchTranslate",
"name": "project",
"units": [
"<unit id=\"u1\"><segment><source>Sign in</source><target>Iniciar sesión</target></segment></unit>",
"<unit id=\"u2\"><segment><source>Reset password</source><target>Restablecer contraseña</target></segment></unit>",
"<unit id=\"u3\"><segment state=\"final\"><source>Welcome back</source><target>Bienvenido de nuevo</target></segment></unit>"
],
"srcLang": "en",
"tgtLang": "es",
"quality": 60,
"limit": 5,
"fileId": "content.xlf"
}| Field | Required | Description |
|---|---|---|
name |
yes | Instance to search against (must already be open) |
units |
yes | Array of <unit> XML strings to enrich |
srcLang |
yes | Source language of the units |
tgtLang |
yes | Target language to search for |
quality |
yes | Minimum hybrid match score, 0-100, written to the output's @matchQuality attribute |
limit |
no | Max candidates per segment; defaults to semanticTranslationSearch's own default (10) when omitted |
fileId |
no | File identifier for segment references; used in ref when a segment has no id |
Every segment is processed except ones with state="final". For each segment, it runs semanticTranslationSearch and adds <mtc:match> entries to that unit's <mtc:matches> block, declaring the xmlns:mtc namespace on the unit if it isn't already present. The payload contains the enriched units as XML strings, in the same order as the input:
{
"status": "success",
"payload": {
"units": [
"<unit id=\"u1\">...</unit>",
"<unit id=\"u2\">...</unit>",
"<unit id=\"u3\">...</unit>"
],
"segmentsProcessed": 2,
"segmentsWithMatches": 2,
"totalMatches": 6
}
}{ "command": "stop" }Shuts down the HTTP server.
| Command | Requires open |
Ticketed | Payload on success |
|---|---|---|---|
open |
n/a | no | {} |
close |
yes | no | {} |
create |
no | no | {} |
remove |
no | no | {} |
list |
no | no | HybridTMInstanceMetadata[] |
import |
yes | yes | { ticket }, later { imported } |
match |
yes | yes | { ticket }, later match stats |
backup |
yes | yes | { ticket }, later { backedUp } |
restore |
see above | yes | { ticket }, later { restored, name } |
batchTranslate |
yes | no | enriched units, plus segment/match counts |
status |
no | no | job status/result |
concordanceSearch |
yes | no | Map<string, XMLElement>[] (as JSON) |
semanticSearch |
yes | no | SearchResult[] |
semanticTranslationSearch |
yes | no | Match[] |
storeXliffUnit |
yes | no | {} |
stop |
no | no | {} |
- 01 · Getting Started covers the equivalent programmatic API if you're calling
HybridTM/HybridTMFactorydirectly instead of through the server - 05 · Command-Line Interface documents the
hybridtm serve/hybridtm stopconvenience commands for this server - 03 · Search and Filtering documents the
MetadataFilter/TranslationSearchFiltersshapes accepted by the search commands
{ "status": "success", "payload": /* command-specific */ } { "status": "failed", "reason": "human-readable message" }