Skip to content
screenjson

screenjson-server

Translate a whole library with a pass

Share out every line of dialogue in a screenjson-server library across many workers, each line translated exactly once, with progress you can watch.

Last updated September 2026

A pass is a named job over the library: “translate every line of dialogue into French”. Workers ask the server for a batch, get lines nobody else holds and nobody has finished, and mark each one done as they write it back. Run one worker or a hundred; every line is handed out once.

1. Ask for a batch

S=http://127.0.0.1:8080
curl -s -X POST $S/documents/-/scenes/-/elements/dialogue/checkout \
     -H "Authorization: Bearer $T" \
     -d '{"limit": 20, "pass": "translate-fr", "has_lang": "en"}'

- means any document and any scene. has_lang keeps to lines that have English to translate from. The answer holds up to 20 lines, each checked out to your token:

{
  "checkouts": [
    {
      "path": "/documents/3f64…/scenes/0fea…/elements/dialogue/a1be…",
      "rev": 1,
      "node": { "type": "dialogue", "text": { "en": "Say that again." }, "…": "…" },
      "pass": "translate-fr",
      "expires": "2026-10-01T17:58:43Z"
    }
  ],
  "exhausted": false
}

2. Write each line back

Add the French and finish the line for the pass in one request:

curl -s -X PATCH "$S$path?pass=translate-fr&release=true" \
     -H "Authorization: Bearer $T" -H "If-Match: \"$rev\"" \
     -d '{"text": {"fr": "Redis-le."}}'

PATCH merges, so the English stays and the French is added beside it. ?pass= marks the line done for the pass; release=true lets go of the checkout.

3. A worker

Put the two together in a loop. This one is Python; translate() is wherever your translations come from (an LLM, a translation API, a human queue):

import os, requests

S = os.environ.get("S", "http://127.0.0.1:8080")
H = {"Authorization": f"Bearer {os.environ['T']}"}
PASS = "translate-fr"

def translate(text: str) -> str:
    ...  # call your model or service here

while True:
    batch = requests.post(
        f"{S}/documents/-/scenes/-/elements/dialogue/checkout",
        headers=H, json={"limit": 20, "pass": PASS, "has_lang": "en"},
    ).json()
    if batch["exhausted"]:
        break
    for c in batch["checkouts"]:
        fr = translate(c["node"]["text"]["en"])
        requests.patch(
            f"{S}{c['path']}", params={"pass": PASS, "release": "true"},
            headers={**H, "If-Match": f'"{c["rev"]}"'},
            json={"text": {"fr": fr}},
        ).raise_for_status()

Start as many copies as you like, each with its own token. When the server has nothing left to hand out, exhausted is true and the workers stop.

4. Watch progress

curl -s $S/passes/translate-fr
# {"label":"translate-fr","done":1840,"checked_out":60,…}

The same numbers stream live on the pass:translate-fr channel. A worker that dies just lets its checkouts expire; those lines go back into the pool.

Variations

  • Other element types: …/elements/action/checkout for action lines, or …/elements/checkout for every type.
  • One script: put its ID in place of the first -.
  • Filter: "where": {"/locked": false} skips locked lines.
  • Agents as workers: an MCP-connected agent can do the same with the api_request tool. See Have a team of agents edit a library over MCP.

Next