Your writing engine.
Your integrations.
Create a rewrite, follow its progress, and collect the finished draft from the same GPU engine that powers the studio.
/api/v1JSON · asynchronous jobsStart a rewrite
Start a private session, send your text, then poll the returned job ID until its stage is complete, cancelled, or error. Keep the same session header throughout. The first job loads the model; later jobs reuse it.
SESSION_ID=$(curl -s https://human.saeidnia.com/api/v1/session \
-H "Content-Type: application/json" --data '{}' \
| python -c 'import json,sys; print(json.load(sys.stdin)["session_id"])')
curl https://human.saeidnia.com/api/v1/jobs \
-H "X-Humanizer-Session: $SESSION_ID" \
-H "Content-Type: application/json" \
--data '{"text":"The garden has 12 beds. Volunteers water it each Saturday.","frozen_ranges":[{"start":0,"end":23}]}'
# HTTP 202
{"id":"0123456789abcdef0123456789abcdef"}
curl https://human.saeidnia.com/api/v1/jobs/JOB_ID \
-H "X-Humanizer-Session: $SESSION_ID"For another machine, use the HTTPS address. On the server PC, http://127.0.0.1:8393/api/v1 also works.
Private sessions
No account or service key is needed. Each studio tab automatically starts a private session. Refreshes keep that tab’s session; a fresh tab starts empty. Drafts cannot be read or cancelled from another session, even with their job IDs.
API clients first send POST /api/v1/session with {}, then pass the returned session_id in X-Humanizer-Session. Keep this token private. Sessions expire after an idle hour or a service restart. PowerShell:
$session = (Invoke-RestMethod https://human.saeidnia.com/api/v1/session -Method Post -ContentType "application/json" -Body '{}').session_id
$headers = @{ "X-Humanizer-Session" = $session }
Invoke-RestMethod https://human.saeidnia.com/api/v1/status -Headers $headersEndpoints
/api/v1/sessionSend {} to start a private session. Returns HTTP 201 with session_id and expires_in (idle timeout in seconds).
/api/v1/statusModel state, GPU name and actual offloaded layers, and your session’s active/latest job IDs. Without a session, both IDs are null. The shared engine_busy flag indicates only whether the GPU is occupied.
/api/v1/jobsAccepts text and optional frozen_ranges. Returns 202 {"id":"…"}. Up to max_concurrent_jobs run simultaneously, sharing one model. When all slots are occupied, another submission returns 409. Check available_slots in status before retrying.
/api/v1/jobs/{id}Returns the job’s current text, output, stage, passage counts, generated-token count, timing, frozen ranges and detail checks. Poll about once a second. Output grows while generating.
/api/v1/jobs/{id}/cancelSend {}. Returns {"status":"stopping"}; poll until the job reaches a terminal stage. An already finished job returns its terminal stage as status. Partial output is retained. Cancellation closes only that job’s stream; another session’s rewrite keeps running. The model unloads after cancellation only when no jobs remain active.
/api/v1/unloadSend {} to release the idle model’s GPU memory. Returns 409 during a rewrite. The next rewrite reloads it.
/healthzPublic liveness check. Returns the service status and version without draft IDs or content.
/docs and /openapi.json are public. The studio’s existing /api/… routes remain compatible aliases of v1.
Job response
{
"id": "0123456789abcdef0123456789abcdef",
"stage": "complete",
"input": "The garden has 12 beds. Volunteers water it each Saturday.",
"output": "The garden has 12 beds. Each Saturday, volunteers water the garden.",
"error": null,
"frozen_ranges": [{"start": 0, "end": 23, "text": "The garden has 12 beds."}],
"frozen_output_ranges": [{"start": 0, "end": 23, "text": "The garden has 12 beds."}],
"completed_chunks": 1,
"total_chunks": 1,
"tokens_generated": 14,
"started_at": 1792800000.0,
"elapsed_seconds": 2.5,
"finished_at": 1792800002.5,
"check": {
"missing_numbers": [], "extra_numbers": [],
"citations_preserved": true, "truncated": false,
"frozen_total": 1, "frozen_kept": 1
}
}Illustrative response. Wording and timing vary. Stages: queued → loading (when needed) → tokenizing → generating → complete. Cancellation or failure can end any active stage. An entirely frozen draft completes with zero model passages and zero generated tokens.
Keep words or sentences exact
Pass frozen_ranges as up to 128 selections with inclusive start and exclusive end offsets in the original, untrimmed text. Overlapping or adjacent selections are merged. Frozen wording stays exact and in order. Whole frozen sentences are copied. Word freezes are checked within complete sentence context; if the model changes a frozen selection, expands the sentence excessively, or its passage cannot fit, that passage keeps its original wording. The response reports these fallbacks in protected_passages_kept. Whole sentences usually produce smoother prose than individual words.
Offsets count UTF-16 code units, matching JavaScript’s selectionStart, selectionEnd, and String.slice. Emoji may occupy two units. Splitting a surrogate pair is rejected. Python conversion:
def utf16_offset(text, python_index):
return len(text[:python_index].encode("utf-16-le")) // 2
start = text.index("keep this")
frozen = {"start": utf16_offset(text, start),
"end": utf16_offset(text, start + len("keep this"))}frozen_output_ranges locates the copied selections in the current output, using the same offset convention. A cancelled partial draft may not yet contain every frozen selection. Compare check.frozen_kept with check.frozen_total.
A complete Python client
Uses only Python’s standard library and creates its private session automatically.
import json, time, urllib.request
BASE = "https://human.saeidnia.com/api/v1"
SESSION = None
def api(path, body=None):
data = None if body is None else json.dumps(body).encode()
headers = {"Content-Type": "application/json",
"User-Agent": "Humanizer-Studio-API-Client/1.0"}
if SESSION:
headers["X-Humanizer-Session"] = SESSION
req = urllib.request.Request(BASE + path, data=data, headers=headers)
with urllib.request.urlopen(req, timeout=15) as response:
return json.load(response)
SESSION = api("/session", {})["session_id"]
job_id = api("/jobs", {"text": "Your draft here."})["id"]
while True:
job = api("/jobs/" + job_id)
if job["stage"] in {"complete", "cancelled", "error"}:
break
time.sleep(1)
if job["stage"] != "complete":
raise RuntimeError(job["error"] or job["stage"])
print(job["output"])Try your engine
Requests use this tab’s private session automatically. No service key or login is needed.
Ready when you are.
No request sent yet.
Limits, errors & retention
- Up to 5,000 whitespace-delimited words and 40,000 UTF-16 code units per draft; maximum JSON request size 200,000 bytes.
- The engine exposes its configured simultaneous-job limit in status. Up to five recent jobs per session remain in memory for up to one hour after finishing; older entries may be evicted sooner when new jobs arrive. Restarting the container clears them.
- Copy or save output you want to keep. This service does not write drafts to disk.
- The model unloads after 30 idle minutes. A cold first rewrite takes longer.
| Status | Meaning |
|---|---|
| 400 | Missing text, invalid JSON, Unicode or frozen ranges |
| 401 | Session missing or expired; start a new session |
| 403 | Host or browser origin not allowed |
| 404 | Unknown or expired job / route |
| 409 | Engine busy; retry after the current job ends |
| 413 | Body or draft exceeds limits |
| 415 | POST requires Content-Type: application/json |
For Python clients, set a descriptive User-Agent as shown above. The Cloudflare edge rejects the default Python agent with HTTP 403 / error 1010 before a request reaches this API. Request errors return {"error":"Readable explanation"}. A rewrite failure after acceptance is reported as stage: "error" inside the job’s HTTP 200 response. Numeric mentions and bracketed citations are compared as text patterns; these checks do not verify meaning. Rewrite quality and detector scores are not guaranteed.