Logo of wyzie-docsWyzie Docs

Wyzie Subs

Wyzie Synced#

Wyzie Synced is a Pro feature: free keys get 403 Paid feature. Each successful sync costs 5 requests; a sync that finds no match is not charged. Downloading the synced link then counts like any other download.

Subtitles found online are often timed for a different release than the video you have: they start a few seconds early or late, or drift further out as the film goes on because that release runs at another frame rate. Wyzie Synced listens to your copy's audio, finds where people talk, and works out the offset and frame-rate fix that line the subtitle up with it. You get a normal download link with the fix applied (the offset and fps download options).

On the Web#

The easiest way: open sub.wyzie.io/synced, enter your Pro key, pick your video file and the title, and download the synced subtitle. The audio is analysed in your browser, so the video is never uploaded: only the speech timings are sent. MKV, MP4, AVI and most other formats work, including AC3, E-AC3 and DTS audio.

The API: POST /sync#

Send which subtitle you want (a download link, or the title to let Wyzie pick the best match) and the audio: either speech timings you detected yourself, or the audio/video file itself. POST /synced is the same API.

curl -X POST https://sub.wyzie.io/sync \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_KEY","id":"tt3659388","language":"en","speech":[[129.1,131.2],[131.4,133.6],[140.2,142.9]]}'

Which Subtitle#

ParameterExampleDescription
urlhttps://sub.wyzie.io/c/…A download link from /search (https://sub.wyzie.io/c/…). Other download options on it (to, sdh, …) are kept on the synced link.
idtt3659388 or 286217Instead of url: TMDB or IMDB ID. Wyzie tries the top 5 text subtitles in that language and returns the one that fits your audio best.
languageenWith id: ISO 639-1 code of the subtitle language (required).
season & episode1 and 4With id, for TV. Both must be present together.
keyYOUR_KEYYour Pro API key. Without it, the key behind url's tok is used; links from the keyless download page need key.

The Audio#

ParameterExampleDescription
speech[[12.1, 14.8], [15.3, 17]]Where people talk: [[start, end], …] in seconds, from any voice activity detector (wyzie-lib's detectSpeech, Silero VAD, webrtcvad). A 2-hour film is roughly 2,000 segments, about 40 KB of JSON.
mediamovie-audio.m4aOr the audio/video file itself, up to 8 GB (a whole film is fine). Sent as the raw request body (with the other fields in the query string) or the multipart field media if it is under ~90 MB; above that, Cloudflare rejects a single request outright, so it goes up in pieces over POST /sync/upload instead. For a multipart upload over 8 MB, put key in the query string: it is checked before the file is read.
Fields go in a JSON body, a multipart form, or the query string (with a raw media body).
# Upload the audio track itself (Pro keys, one request under ~90 MB)
ffmpeg -i movie.mkv -vn -ac 1 -b:a 48k audio.m4a
curl -X POST "https://sub.wyzie.io/sync?key=YOUR_KEY&id=tt3659388&language=en" \
  -H "Content-Type: audio/mp4" --data-binary @audio.m4a

A whole movie file, not just its audio, works too -- up to 8 GB -- but past ~90 MB it has to go up in chunks instead of one request.

Response#

A 200 response is JSON:

{
  "url": "https://sub.wyzie.io/c/198e0c4d/id/1955024019?format=srt&encoding=UTF-8&id=tt3659388&offset=4.09&tok=...",
  "offset": 4.09,
  "fps": null,
  "confidence": 0.62,
  "inSync": false,
  "subtitle": {
    "display": "English",
    "language": "en",
    "format": "srt",
    "release": "The.Martian.2015.720p.BluRay.x264-SPARKS",
    "fileName": "The.Martian.2015.720p.BluRay.x264-SPARKS.srt",
    "source": "charlie",
    "isHearingImpaired": false
  },
  "tried": 5
}
  • url: the subtitle's download link with the timing fix (offset, fps) and a fresh tok for your key. Use it like any /search url: each download costs 1 request.
  • offset: seconds added to every line after the frame-rate fix (negative is earlier).
  • fps: the frame-rate fix as SUBTITLE_FPS:VIDEO_FPS (e.g. "25:23.976"), or null when none was needed.
  • confidence: 0 to 1: how clearly this timing beats every other. Anything returned has passed the match test; higher is more certain.
  • inSync: true when the subtitle already matched your copy.
  • subtitle: which subtitle was used (release, fileName, format, source, …). With url, only its format.

Errors#

Errors are JSON with message and details. Refused and failed syncs are not charged.

StatusMessageMeaning
400e.g. "No audio", "Invalid speech"Missing or invalid fields: no subtitle, no audio, or speech that isn't [start, end] pairs.
401"API key required"No key, url's download link is invalid or expired, or a multipart upload over 8 MB has no key in the query string.
403"Paid feature", "Invalid API key"The key is free (Wyzie Synced needs Pro), invalid, or on hold.
404"No subtitles found"No text subtitles in that language for the title.
413"File too large"The media file is over the endpoint's own limit (~90 MB for a single request, 8 GB total across a chunked upload, or a per-chunk limit on POST /sync/upload/:id). Upload the audio track alone, send speech, or use the chunked upload for anything big.
422"Couldn't sync", "No speech found"The subtitle doesn't line up with the audio at any offset or frame rate (probably another cut or episode), the audio has too little speech, or the file can't be decoded.
429 / 402"Too many syncs", "Pro key request balance exhausted"The key can't pay: a sync needs at least 5 requests left, checked before any work. Or 429 Too many syncs: a key can start 60 syncs an hour.
503"Unavailable", "Search failed"Busy decoding or reading other uploads, or search is briefly unavailable. Retry shortly, or send speech.

With wyzie-lib#

wyzie-lib has detectSpeech (the same detector the site runs in your browser) and syncSubtitle. Pass media over ~90 MB and it uploads it in chunks for you (see below) -- no other client does this today, so a whole movie file is effectively a wyzie-lib feature: calling the raw API yourself past ~90 MB means implementing the chunked upload protocol.

import { detectSpeech, syncSubtitle } from 'wyzie-lib';
 
// Decode the audio any way you like (here: the browser's decoder); one channel at any sample rate will do.
const ctx = new OfflineAudioContext(1, 8000, 8000);
const audio = await ctx.decodeAudioData(await file.arrayBuffer());
const speech = detectSpeech(audio.getChannelData(0), audio.sampleRate);
 
const synced = await syncSubtitle({
  id: 'tt3659388',
  language: 'en',
  speech,
  key: 'YOUR_KEY',
});
console.log(synced.offset, synced.fps, synced.url);

Large Files: Chunked Upload#

wyzie-lib calls this automatically -- read this section only if you are integrating without the library. A file over ~90 MB is sent as several chunks instead of one request, since Cloudflare rejects anything bigger outright. Three calls, all Pro-key-only:

1. Start: POST /sync/upload#

POST /sync/upload?key=YOUR_KEY starts a session and returns { uploadId, chunkMaxBytes }. Sessions are single-flight per key and expire after 30 minutes of inactivity.

2. Chunks: POST /sync/upload/:id#

POST /sync/upload/:id?key=YOUR_KEY, with header X-Chunk-Index (0, 1, 2, …), appends one chunk (the raw request body, up to chunkMaxBytes) in order. A chunk sent out of order gets 409, with expectedIndex telling you where to resume.

3. Finish: POST /sync/upload/:id/finish#

POST /sync/upload/:id/finish takes the same fields as POST /sync (url, or id + language [+ season/episode], key -- as JSON or the query string) and runs the sync against everything uploaded, then deletes the session either way. There is no separate cancel call: an unfinished session simply expires.

Roughly, chunking a 2 GB file into 64 MB pieces:

wyzie-lib's syncSubtitle retries a chunk up to 2 extra times on a network error or a 5xx before giving up; a 4xx (e.g. a stale index) fails immediately. Rolling your own client, do the same rather than restarting the whole upload.
UPLOAD=$(curl -s -X POST "https://sub.wyzie.io/sync/upload?key=YOUR_KEY" | jq -r .uploadId)
 
split -b 64m -d movie.mkv chunk_
i=0
for f in chunk_*; do
  curl -X POST "https://sub.wyzie.io/sync/upload/$UPLOAD?key=YOUR_KEY" \
    -H "X-Chunk-Index: $i" --data-binary @"$f"
  i=$((i + 1))
done
 
curl -X POST "https://sub.wyzie.io/sync/upload/$UPLOAD/finish" \
  -H "Content-Type: application/json" \
  -d '{"key":"YOUR_KEY","id":"tt3659388","language":"en"}'
// wyzie-lib does exactly this for any media over ~90 MB -- nothing extra to write:
import { syncSubtitle } from 'wyzie-lib';
 
const synced = await syncSubtitle({
  id: 'tt3659388',
  language: 'en',
  media: movieFile,
  key: 'YOUR_KEY',
});

How It Works#

  1. Speech: the audio is decoded to 8 kHz mono (the centre channel alone for 5.1 and 7.1 mixes, where dialogue lives), and a voice activity detector marks where people talk: loud, speech-band sound that rises and falls with syllables.

  2. Alignment: the subtitle's on-screen times are cross-correlated with that speech at every offset within ±10 minutes, for the usual frame-rate mismatches (25 vs 23.976, 25 vs 24, 24 vs 23.976 fps).

  3. Refinement: the best timing is refined to 10 ms by lining up where lines start with where speech starts.

  4. A timing is only returned when it stands far above every other offset, so a subtitle for another cut or episode gets 422 Couldn't sync instead of a wrong shift.

Limitations#

  • Wyzie Synced fixes a constant offset and a frame-rate difference. A subtitle for a different cut (added or missing scenes) can't be fixed by one shift, and is refused.
  • It needs speech: films with little dialogue, or audio that's mostly music, may not sync.
  • Offsets up to ±10 minutes are found.
  • sub.wyzie.io stays behind Cloudflare (its DDoS and WAF protection matter more to us than a bigger upload limit), and Cloudflare caps a single request around 100 MB regardless of anything the app itself allows. A file bigger than that has to go up in pieces over POST /sync/upload -- wyzie-lib's syncSubtitle does this for you automatically.