Real-time English -> Thai translation overlay for games running in Windowed Fullscreen (borderless) on Windows 10 / 11.
โปรแกรม overlay แปลอังกฤษเป็นไทยแบบเรียลไทม์ สำหรับเกมที่รันแบบ Windowed Fullscreen บน Windows 10/11 — ไม่ inject DLL เข้าเกม แปลได้ทั้งแบบออฟไลน์ 100% ด้วยโมเดลในเครื่อง หรือผ่าน Google / OpenAI สำหรับเครื่องสเปกต่ำที่รันโมเดลเองไม่ไหว
Latest release — download
TLOverlay-<version>-win-x64.zip, unzip it somewhere writable, run TLOverlay.exe.
Self-contained: no .NET runtime to install. Windows SmartScreen warns on first run because the executable is unsigned - "More info" then "Run anyway". Do not run it from inside the zip; the app says so if you try, because everything it downloads would be deleted with the archive's temp folder.
After that it keeps itself up to date - see Updates.
Windows.Graphics.Capture capture the game's window only, never the whole screen
v
ChangeDetector skip frames that did not change (>90% of them)
v
Windows.Media.Ocr offline English OCR, ships with Windows
v
TextAssembler rebuild wrapped lines into whole sentences
v
CachingTranslator SQLite + LRU; repeat dialogue is instant
v
llama.cpp on 127.0.0.1 local GGUF model, no network egress
v
Overlay window layered, click-through, excluded from capture
Three design decisions carry most of the weight:
SetWindowDisplayAffinity
(WDA_EXCLUDEFROMCAPTURE). Without it, OCR reads back its own Thai output and
the pipeline feeds on itself.To run
To build
dotnet build fails outside Windows no matter the
runtime identifier.dotnet run --project src/TLOverlay.AppCtrl+Alt+R, drag a box over the dialogue area, press Enter. The editor opens
showing the area already set, so redrawing replaces it deliberately and Escape
keeps what was there. One area per game, saved and reloaded automatically.Ctrl+Alt+T to start translating.Ctrl+Alt+C), drag the panel where you want it and drag its bottom-right
corner - the ridged grip - to resize. The position is saved per game. Switch
back to click-through to play.| Hotkey | Action |
|---|---|
Ctrl+Alt+T | Start / stop translating |
Ctrl+Alt+R | Draw the capture region |
Ctrl+Alt+S | Translate now (the current mode decides how much) |
Ctrl+Alt+F | Translate the whole screen |
Ctrl+Alt+H | Show / hide the translated text |
Ctrl+Alt+G | Show / hide the translation area |
Ctrl+Alt+C | Switch between click-through and interactive |
These are the defaults; all six can be rebound under ตั้งค่า. A binding needs at least one of Ctrl, Alt or Shift - a global hotkey with no modifier is swallowed system-wide, which would take that key away from the game. Changes are saved as the differences from the defaults, so an action added in a later version arrives with a working key rather than none. คืนค่าพื้นฐาน puts everything in that window back.
| Mode | What it does |
|---|---|
| อัตโนมัติ | Watches the capture region and translates each line as it settles. The default. |
| เฉพาะพื้นที่ที่เลือก | Same region, but only when you press Ctrl+Alt+S. |
| ทั้งหน้าจอ | Reads every line of text anywhere on the game window and draws the Thai over the original, in place. Ctrl+Alt+F. |
On-demand suits a game that redraws its dialogue box constantly, or a session where you only want a line here and there. It ignores the "same text as last time" guard, so pressing twice on one line really does translate it again.
Full screen is for menus, item tables and quest logs - text scattered in
places a single box cannot cover. One press reads the whole window, translates
every line in one request, and covers each original with an opaque Thai
label. The labels stay until the next press; Ctrl+Alt+H hides and shows them.
The opacity of those labels is a slider, saved per game.
Text size is a slider too, next to it, and it applies to both presentations. For the subtitle panel it is the size outright; for full-screen labels it scales the size each label takes from its own OCR box, so a menu entry and a line of dialogue stay proportioned to each other. Turned up far enough, labels grow past the English they cover and may overlap the lines around them - which is the right trade when the alternative is text too small to read.
It only works on demand, and that is structural rather than a preference: a whole game screen always has something moving on it - a clock, a health bar, an idle animation - so change detection over the window would fire continuously and translate everything several times a second.
Two things to know before leaning on it. OCR reads a full screen less well than a tight crop, because the region path upscales a small box before reading it and a whole window cannot be upscaled - large dialogue text comes back well, small HUD text less so. And on a metered engine every press translates the whole HUD, while a local model on a CPU will take a long time over forty lines; the mode hint says so for whichever engine is selected. Lines already translated once are served from cache, so pressing again after the dialogue advances usually costs only the lines that changed.
Three, chosen in Setup. The pipeline in front of them - capture, change detection, OCR, sentence assembly, glossary, cache - is the same either way; only the last step moves.
| Engine | Needs | Privacy | Cost |
|---|---|---|---|
| Local model (default) | ~3 GB free RAM, a 0.8-2.4 GB download | Nothing leaves the machine | Free |
| Google Translate | Nothing, or a Cloud Translation key | Game text goes to Google | Free, or per character |
| OpenAI-compatible | An API key | Game text goes to the provider | Per line |
The local model is the default and the only fully offline option. It is also the one that does not fit everywhere: a machine with 8 GB of RAM and a game already running has no 3 GB to spare, and on a slow CPU a line can take longer than the dialogue stays on screen. That is what the other two are for.
Google works with no account at all. Without a key it uses the endpoint the Google Translate web page itself calls - undocumented, rate-limited per IP address, and something Google can change or withdraw without notice. Setup says so where the choice is made rather than letting it be discovered mid-game. With a Cloud Translation API key it uses the documented, billed v2 API instead.
OpenAI-compatible gives the most natural Thai of the three for game dialogue, because it gets the same prompt and the same few-shot examples the local model does - switching between them changes the speed and the bill, not the voice. The endpoint is configurable, so the same setting points at OpenRouter, Groq, or a model running on another PC in the house.
API keys are encrypted with DPAPI under the current Windows account before they
are written to settings.json, so a settings file that ends up in a bug report
or a backup does not hand over someone's billing credentials. Setup has a test
button that translates one sentence and shows the result - a wrong key or model
name otherwise surfaces much later as an overlay that silently never says
anything.
The cache is keyed on the engine's identity, so switching engines never serves one engine's lines from another's.
These are for the local engine only - on Google or OpenAI there is nothing to download.
The translation model and llama-server.exe are not in this repository, and
you do not need a terminal to get them. Run the app: if either is missing it
opens a setup screen that downloads both, with a progress bar, a resume if the
connection drops, and a Browse button for files you already have. Reachable
later from the control panel as ตั้งค่าโมเดล, which is also how you switch
model or move the work between CPU and GPU.
If you would rather script it, tools/fetch-models.ps1 does the same job, but
it needs PowerShell 7 (pwsh), which Windows does not ship by default.
Both rows have a ลบ button. The model is the largest thing this app puts on
a disk by two orders of magnitude, and a player who has moved to a cloud engine -
or who just needs the space back for a game - should not have to go hunting
through %LocalAppData% for it. Deleting the model takes any half-finished
download with it; deleting the server takes the whole runtime folder, since the
executable does not run without the DLLs beside it. A server pointed somewhere
else by hand loses only its executable - that folder is the player's.
The server and the model together are several gigabytes, so the setup screen lets you choose the folder they go in - useful when the system drive is the full one. It shows the free space on the chosen drive and warns before a download that would not fit.
Changing the location when something is already installed offers to move it.
The move copies everything across first and only deletes the original once every
file has arrived, so an interrupted move costs you the copy, never the install.
Settings, profiles, logs and the translation cache are small and stay under
%LocalAppData%\TLOverlay regardless; only runtime\ and models\ move.
See NOTICE.md for model licensing - it matters, and not every model here may be used commercially. The setup screen shows each model's licence next to it in the dropdown.
dotnet build tloverlay.sln -c Release
dotnet test tests/TLOverlay.Core.Tests/TLOverlay.Core.Tests.csproj
dotnet run --project src/TLOverlay.App
The projects compile against net8.0-windows10.0.22621.0 but declare
SupportedOSPlatformVersion 10.0.19041.0. The newer projection is only needed at
compile time, for GraphicsCaptureSession.IsBorderRequired; that property is
probed with ApiInformation before use, so the app still runs on Windows 10
2004 - it just keeps the yellow capture border there.
dotnet publish src/TLOverlay.App/TLOverlay.App.csproj `
--configuration Release `
--runtime win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:DebugType=None `
--output artifacts/TLOverlay-win-x64
Three of those flags are load-bearing, and one option must stay off:
--self-contained true - the audience is players, not developers. Asking them
to install the .NET Desktop Runtime before they can read their game is a step
most will not get past. The cost is size: the release zip is around 75 MB.
Single-file packing means it really is one TLOverlay.exe, nothing beside it.-p:IncludeNativeLibrariesForSelfExtract=true - Microsoft.Data.Sqlite needs
the native e_sqlite3.dll. Without this it never leaves the bundle and the
translation cache throws on the first lookup, at runtime, on the user's
machine.-p:EnableCompressionInSingleFile=true - halves the executable, from about
183 MB to about 75 MB. That is the file the in-app updater downloads, so
without it every update costs the user two and a half times the data of the
zip. The price is a few hundred milliseconds of decompression at startup,
once per launch, which nobody waiting for a game to load will notice.--runtime win-x64 - required for a self-contained build, and the only
architecture the capture interop targets.-p:PublishTrimmed=true. WPF does not support trimming. The build
succeeds and the executable crashes on startup, which is the worst possible
place to find out.The zip contains TLOverlay.exe, NOTICE.md and README.md.
It does not contain models/ or runtime/. Those are several gigabytes, and
their licences differ per model - some are non-commercial. The app fetches them
itself on first run, which is the whole reason the setup screen exists.
<Version> in Directory.Build.props.git tag v0.2.0 && git push origin v0.2.0The tag is what triggers .github/workflows/release.yml: it runs the tests,
publishes, zips, and creates the GitHub Release with generated notes. Nothing
else publishes a release, so a tag always corresponds to something that shipped.
A release carries three assets, and the updater needs all three shapes to be right:
| Asset | For |
|---|---|
TLOverlay-<version>-win-x64.zip | people downloading by hand |
TLOverlay.exe | the in-app updater - one file, no unpacking |
SHA256SUMS.txt | what the updater checks the download against |
A release missing the executable or the checksums is simply not offered as an update. That is deliberate: the binary is unsigned, so those hashes are the only thing standing between the updater and whatever arrived over the wire.
CI runs the same publish command on every push and uploads the result as a build artifact. A publish that has stopped working is therefore caught on the commit that broke it, not while cutting a tag.
The executable is unsigned, so Windows SmartScreen shows a warning the first time a user runs it ("More info" then "Run anyway"). Removing that needs a code signing certificate, which this project does not have. Worth saying plainly in release notes rather than leaving people to wonder.
End to end and working: capture, OCR, translation, overlay, per-game profiles, glossary, caching, hotkeys and the region editor. The control panel reports average OCR and translation time plus the frame skip ratio, which is the number that tells you whether change detection is doing its job - below roughly 80% during play means a region is picking up animated scenery.
Not built yet: the NLLB ONNX backend as a lighter alternative to the local LLM, and automatic region detection.
The app checks GitHub for a newer release once a day, on startup, and says so with a banner on the control panel. Nothing is downloaded until you press the button. Three settings, under การอัพเดทโปรแกรม:
| Setting | What it does |
|---|---|
| แจ้งเตือน (default) | Checks and tells you. You decide when. |
| ดาวน์โหลดอัตโนมัติ | Checks, downloads and restarts into it. |
| ไม่ต้องตรวจสอบ | Never contacts GitHub. |
Notify is the default because this program runs beside a game: a background
download of 70 MB is not something to start on someone's connection mid-session.
For the same reason it is a banner rather than a dialog, and installing stops a
running translation session first - replacing the executable underneath a live
capture session and a model server held in a job object is how you end up with an
orphaned llama-server sitting on two gigabytes.
How the swap works. Windows will not let a running executable be overwritten,
but it will let one be renamed. So there is no separate updater program to
install, keep in sync, or fail to clean up: the running exe becomes
TLOverlay.exe.old, the new one takes its name, the app restarts and deletes the
leftover. If the update dies between those two renames - the one moment the folder
can hold a .old and no program - the next startup puts the old one back.
What is checked before anything is replaced, in order: the download starts
with MZ, its SHA-256 matches SHA256SUMS.txt from the release, and the new
build runs --version and answers with the version it claims. Only then is
anything renamed. That last check is not paranoia - a publish can be broken in
ways that only appear at startup, and finding out here means you keep a working
program instead of one that no longer opens.
Installed somewhere unwritable (Program Files, read-only media), the app says so and sends you to the release page rather than asking for administrator rights an overlay has no business holding.
Two processes, and they are worth understanding separately.
The app should sit at roughly 150-250 MB and stay there. It is flat by construction: a 1080p frame is 8 MB and several are pulled every second, so every frame buffer is rented from a pool and returned rather than allocated. Frames go through one WinRT buffer that is reused until the capture size changes - a fresh one per frame is native memory the GC cannot see, so nothing about allocating it creates any pressure to collect the wrapper that owns it. Getting that wrong once took the app past 10 GB inside two minutes.
Polling backs off on its own. Eight frames a second is right while dialogue is moving and pure waste in a menu, so after a quiet stretch the interval doubles up to half a second, and returns to full rate on the first change. The control panel shows the current interval next to the memory figures.
The model server needs roughly the model's file size plus a few hundred megabytes. That is the number that decides which model a machine can run, so Setup shows it beside each model along with the machine's own RAM, and says so plainly when the two do not leave room for a game. On 8 GB, use Gemma 3 1B.
If the app's figure climbs steadily during a session, that is a bug - the readout is on the panel so it can be reported with a number.
gpuLayers defaults to 0 (CPU)
in %AppData%\TLOverlay\settings.json for that reason; raise it if you have
headroom and want sub-second translations.Real-time English -> Thai translation overlay for games running in Windowed Fullscreen (borderless) on Windows 10 / 11.
โปรแกรม overlay แปลอังกฤษเป็นไทยแบบเรียลไทม์ สำหรับเกมที่รันแบบ Windowed Fullscreen บน Windows 10/11 — ไม่ inject DLL เข้าเกม แปลได้ทั้งแบบออฟไลน์ 100% ด้วยโมเดลในเครื่อง หรือผ่าน Google / OpenAI สำหรับเครื่องสเปกต่ำที่รันโมเดลเองไม่ไหว
Latest release — download
TLOverlay-<version>-win-x64.zip, unzip it somewhere writable, run TLOverlay.exe.
Self-contained: no .NET runtime to install. Windows SmartScreen warns on first run because the executable is unsigned - "More info" then "Run anyway". Do not run it from inside the zip; the app says so if you try, because everything it downloads would be deleted with the archive's temp folder.
After that it keeps itself up to date - see Updates.
Windows.Graphics.Capture capture the game's window only, never the whole screen
v
ChangeDetector skip frames that did not change (>90% of them)
v
Windows.Media.Ocr offline English OCR, ships with Windows
v
TextAssembler rebuild wrapped lines into whole sentences
v
CachingTranslator SQLite + LRU; repeat dialogue is instant
v
llama.cpp on 127.0.0.1 local GGUF model, no network egress
v
Overlay window layered, click-through, excluded from capture
Three design decisions carry most of the weight:
SetWindowDisplayAffinity
(WDA_EXCLUDEFROMCAPTURE). Without it, OCR reads back its own Thai output and
the pipeline feeds on itself.To run
To build
dotnet build fails outside Windows no matter the
runtime identifier.dotnet run --project src/TLOverlay.AppCtrl+Alt+R, drag a box over the dialogue area, press Enter. The editor opens
showing the area already set, so redrawing replaces it deliberately and Escape
keeps what was there. One area per game, saved and reloaded automatically.Ctrl+Alt+T to start translating.Ctrl+Alt+C), drag the panel where you want it and drag its bottom-right
corner - the ridged grip - to resize. The position is saved per game. Switch
back to click-through to play.| Hotkey | Action |
|---|---|
Ctrl+Alt+T | Start / stop translating |
Ctrl+Alt+R | Draw the capture region |
Ctrl+Alt+S | Translate now (the current mode decides how much) |
Ctrl+Alt+F | Translate the whole screen |
Ctrl+Alt+H | Show / hide the translated text |
Ctrl+Alt+G | Show / hide the translation area |
Ctrl+Alt+C | Switch between click-through and interactive |
These are the defaults; all six can be rebound under ตั้งค่า. A binding needs at least one of Ctrl, Alt or Shift - a global hotkey with no modifier is swallowed system-wide, which would take that key away from the game. Changes are saved as the differences from the defaults, so an action added in a later version arrives with a working key rather than none. คืนค่าพื้นฐาน puts everything in that window back.
| Mode | What it does |
|---|---|
| อัตโนมัติ | Watches the capture region and translates each line as it settles. The default. |
| เฉพาะพื้นที่ที่เลือก | Same region, but only when you press Ctrl+Alt+S. |
| ทั้งหน้าจอ | Reads every line of text anywhere on the game window and draws the Thai over the original, in place. Ctrl+Alt+F. |
On-demand suits a game that redraws its dialogue box constantly, or a session where you only want a line here and there. It ignores the "same text as last time" guard, so pressing twice on one line really does translate it again.
Full screen is for menus, item tables and quest logs - text scattered in
places a single box cannot cover. One press reads the whole window, translates
every line in one request, and covers each original with an opaque Thai
label. The labels stay until the next press; Ctrl+Alt+H hides and shows them.
The opacity of those labels is a slider, saved per game.
Text size is a slider too, next to it, and it applies to both presentations. For the subtitle panel it is the size outright; for full-screen labels it scales the size each label takes from its own OCR box, so a menu entry and a line of dialogue stay proportioned to each other. Turned up far enough, labels grow past the English they cover and may overlap the lines around them - which is the right trade when the alternative is text too small to read.
It only works on demand, and that is structural rather than a preference: a whole game screen always has something moving on it - a clock, a health bar, an idle animation - so change detection over the window would fire continuously and translate everything several times a second.
Two things to know before leaning on it. OCR reads a full screen less well than a tight crop, because the region path upscales a small box before reading it and a whole window cannot be upscaled - large dialogue text comes back well, small HUD text less so. And on a metered engine every press translates the whole HUD, while a local model on a CPU will take a long time over forty lines; the mode hint says so for whichever engine is selected. Lines already translated once are served from cache, so pressing again after the dialogue advances usually costs only the lines that changed.
Three, chosen in Setup. The pipeline in front of them - capture, change detection, OCR, sentence assembly, glossary, cache - is the same either way; only the last step moves.
| Engine | Needs | Privacy | Cost |
|---|---|---|---|
| Local model (default) | ~3 GB free RAM, a 0.8-2.4 GB download | Nothing leaves the machine | Free |
| Google Translate | Nothing, or a Cloud Translation key | Game text goes to Google | Free, or per character |
| OpenAI-compatible | An API key | Game text goes to the provider | Per line |
The local model is the default and the only fully offline option. It is also the one that does not fit everywhere: a machine with 8 GB of RAM and a game already running has no 3 GB to spare, and on a slow CPU a line can take longer than the dialogue stays on screen. That is what the other two are for.
Google works with no account at all. Without a key it uses the endpoint the Google Translate web page itself calls - undocumented, rate-limited per IP address, and something Google can change or withdraw without notice. Setup says so where the choice is made rather than letting it be discovered mid-game. With a Cloud Translation API key it uses the documented, billed v2 API instead.
OpenAI-compatible gives the most natural Thai of the three for game dialogue, because it gets the same prompt and the same few-shot examples the local model does - switching between them changes the speed and the bill, not the voice. The endpoint is configurable, so the same setting points at OpenRouter, Groq, or a model running on another PC in the house.
API keys are encrypted with DPAPI under the current Windows account before they
are written to settings.json, so a settings file that ends up in a bug report
or a backup does not hand over someone's billing credentials. Setup has a test
button that translates one sentence and shows the result - a wrong key or model
name otherwise surfaces much later as an overlay that silently never says
anything.
The cache is keyed on the engine's identity, so switching engines never serves one engine's lines from another's.
These are for the local engine only - on Google or OpenAI there is nothing to download.
The translation model and llama-server.exe are not in this repository, and
you do not need a terminal to get them. Run the app: if either is missing it
opens a setup screen that downloads both, with a progress bar, a resume if the
connection drops, and a Browse button for files you already have. Reachable
later from the control panel as ตั้งค่าโมเดล, which is also how you switch
model or move the work between CPU and GPU.
If you would rather script it, tools/fetch-models.ps1 does the same job, but
it needs PowerShell 7 (pwsh), which Windows does not ship by default.
Both rows have a ลบ button. The model is the largest thing this app puts on
a disk by two orders of magnitude, and a player who has moved to a cloud engine -
or who just needs the space back for a game - should not have to go hunting
through %LocalAppData% for it. Deleting the model takes any half-finished
download with it; deleting the server takes the whole runtime folder, since the
executable does not run without the DLLs beside it. A server pointed somewhere
else by hand loses only its executable - that folder is the player's.
The server and the model together are several gigabytes, so the setup screen lets you choose the folder they go in - useful when the system drive is the full one. It shows the free space on the chosen drive and warns before a download that would not fit.
Changing the location when something is already installed offers to move it.
The move copies everything across first and only deletes the original once every
file has arrived, so an interrupted move costs you the copy, never the install.
Settings, profiles, logs and the translation cache are small and stay under
%LocalAppData%\TLOverlay regardless; only runtime\ and models\ move.
See NOTICE.md for model licensing - it matters, and not every model here may be used commercially. The setup screen shows each model's licence next to it in the dropdown.
dotnet build tloverlay.sln -c Release
dotnet test tests/TLOverlay.Core.Tests/TLOverlay.Core.Tests.csproj
dotnet run --project src/TLOverlay.App
The projects compile against net8.0-windows10.0.22621.0 but declare
SupportedOSPlatformVersion 10.0.19041.0. The newer projection is only needed at
compile time, for GraphicsCaptureSession.IsBorderRequired; that property is
probed with ApiInformation before use, so the app still runs on Windows 10
2004 - it just keeps the yellow capture border there.
dotnet publish src/TLOverlay.App/TLOverlay.App.csproj `
--configuration Release `
--runtime win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:DebugType=None `
--output artifacts/TLOverlay-win-x64
Three of those flags are load-bearing, and one option must stay off:
--self-contained true - the audience is players, not developers. Asking them
to install the .NET Desktop Runtime before they can read their game is a step
most will not get past. The cost is size: the release zip is around 75 MB.
Single-file packing means it really is one TLOverlay.exe, nothing beside it.-p:IncludeNativeLibrariesForSelfExtract=true - Microsoft.Data.Sqlite needs
the native e_sqlite3.dll. Without this it never leaves the bundle and the
translation cache throws on the first lookup, at runtime, on the user's
machine.-p:EnableCompressionInSingleFile=true - halves the executable, from about
183 MB to about 75 MB. That is the file the in-app updater downloads, so
without it every update costs the user two and a half times the data of the
zip. The price is a few hundred milliseconds of decompression at startup,
once per launch, which nobody waiting for a game to load will notice.--runtime win-x64 - required for a self-contained build, and the only
architecture the capture interop targets.-p:PublishTrimmed=true. WPF does not support trimming. The build
succeeds and the executable crashes on startup, which is the worst possible
place to find out.The zip contains TLOverlay.exe, NOTICE.md and README.md.
It does not contain models/ or runtime/. Those are several gigabytes, and
their licences differ per model - some are non-commercial. The app fetches them
itself on first run, which is the whole reason the setup screen exists.
<Version> in Directory.Build.props.git tag v0.2.0 && git push origin v0.2.0The tag is what triggers .github/workflows/release.yml: it runs the tests,
publishes, zips, and creates the GitHub Release with generated notes. Nothing
else publishes a release, so a tag always corresponds to something that shipped.
A release carries three assets, and the updater needs all three shapes to be right:
| Asset | For |
|---|---|
TLOverlay-<version>-win-x64.zip | people downloading by hand |
TLOverlay.exe | the in-app updater - one file, no unpacking |
SHA256SUMS.txt | what the updater checks the download against |
A release missing the executable or the checksums is simply not offered as an update. That is deliberate: the binary is unsigned, so those hashes are the only thing standing between the updater and whatever arrived over the wire.
CI runs the same publish command on every push and uploads the result as a build artifact. A publish that has stopped working is therefore caught on the commit that broke it, not while cutting a tag.
The executable is unsigned, so Windows SmartScreen shows a warning the first time a user runs it ("More info" then "Run anyway"). Removing that needs a code signing certificate, which this project does not have. Worth saying plainly in release notes rather than leaving people to wonder.
End to end and working: capture, OCR, translation, overlay, per-game profiles, glossary, caching, hotkeys and the region editor. The control panel reports average OCR and translation time plus the frame skip ratio, which is the number that tells you whether change detection is doing its job - below roughly 80% during play means a region is picking up animated scenery.
Not built yet: the NLLB ONNX backend as a lighter alternative to the local LLM, and automatic region detection.
The app checks GitHub for a newer release once a day, on startup, and says so with a banner on the control panel. Nothing is downloaded until you press the button. Three settings, under การอัพเดทโปรแกรม:
| Setting | What it does |
|---|---|
| แจ้งเตือน (default) | Checks and tells you. You decide when. |
| ดาวน์โหลดอัตโนมัติ | Checks, downloads and restarts into it. |
| ไม่ต้องตรวจสอบ | Never contacts GitHub. |
Notify is the default because this program runs beside a game: a background
download of 70 MB is not something to start on someone's connection mid-session.
For the same reason it is a banner rather than a dialog, and installing stops a
running translation session first - replacing the executable underneath a live
capture session and a model server held in a job object is how you end up with an
orphaned llama-server sitting on two gigabytes.
How the swap works. Windows will not let a running executable be overwritten,
but it will let one be renamed. So there is no separate updater program to
install, keep in sync, or fail to clean up: the running exe becomes
TLOverlay.exe.old, the new one takes its name, the app restarts and deletes the
leftover. If the update dies between those two renames - the one moment the folder
can hold a .old and no program - the next startup puts the old one back.
What is checked before anything is replaced, in order: the download starts
with MZ, its SHA-256 matches SHA256SUMS.txt from the release, and the new
build runs --version and answers with the version it claims. Only then is
anything renamed. That last check is not paranoia - a publish can be broken in
ways that only appear at startup, and finding out here means you keep a working
program instead of one that no longer opens.
Installed somewhere unwritable (Program Files, read-only media), the app says so and sends you to the release page rather than asking for administrator rights an overlay has no business holding.
Two processes, and they are worth understanding separately.
The app should sit at roughly 150-250 MB and stay there. It is flat by construction: a 1080p frame is 8 MB and several are pulled every second, so every frame buffer is rented from a pool and returned rather than allocated. Frames go through one WinRT buffer that is reused until the capture size changes - a fresh one per frame is native memory the GC cannot see, so nothing about allocating it creates any pressure to collect the wrapper that owns it. Getting that wrong once took the app past 10 GB inside two minutes.
Polling backs off on its own. Eight frames a second is right while dialogue is moving and pure waste in a menu, so after a quiet stretch the interval doubles up to half a second, and returns to full rate on the first change. The control panel shows the current interval next to the memory figures.
The model server needs roughly the model's file size plus a few hundred megabytes. That is the number that decides which model a machine can run, so Setup shows it beside each model along with the machine's own RAM, and says so plainly when the two do not leave room for a game. On 8 GB, use Gemma 3 1B.
If the app's figure climbs steadily during a session, that is a bug - the readout is on the panel so it can be reported with a number.
gpuLayers defaults to 0 (CPU)
in %AppData%\TLOverlay\settings.json for that reason; raise it if you have
headroom and want sub-second translations.