source-caption-maker (scm for short) is a Python tool for creating and managing subtitles and closed captions for Source engine games. It converts JSON-formatted caption data into the Source engine's caption format, making it easy to add multilingual captions to Source engine mods.
Note
Closed captions & subtitles technically function the same but are seperate things.
- Takes subtitle/caption data from a simple JSON format of a provided mod.
- Converts it into Source engine-compatible captions (
.txtfiles). - Optionally compiles the output
.txtfiles into.datfiles that the Source engine can use (does so by usingcaptioncompiler.exeof desired Source game).
Note
For detailed information about Source engine's caption system, visit the Valve Developer Wiki.
- Python 3.x
- Python
colourmodule (Only if you use HEX color codes)
If you want to compile the output captions to .dat on Linux:
- Install Wine
- Enable Steam Play (Proton) for your Source game in Steam properties (to get Windows binaries like the required
bin/captioncompiler.exe) - That's it!
python3 scm.py --mod <modname> [options]
sh scm.sh --mod <modname> [options]
| Option | Type | Purpose | Required? | Description |
|---|---|---|---|---|
--mod, -m |
string | Mod Folder | Yes | Specifies which mod folder to process from the in/ directory |
--game-path, -p |
string | Game Path | No | Path to Source game installation (for caption compilation) |
--subtitles, -s |
boolean | Process Subtitles | No | Convert subtitles of provided mod |
--captions, -c |
boolean | Process Captions | No | Convert captions of provided mod |
--clear, -cls |
boolean | Clear Screen | No | Clear console history before starting |
--langs, -l |
boolean | List Languages | No | Print all supported languages |
--verbose, -v |
boolean | Detailed Output | No | Print additional processing information |
--delete-output, -do |
boolean | Clear Output | No | Clear output folder before writing output (Proceed with caution!) |
--open-output, -oo |
boolean | Open Output | No | Open output folder when finished |
--copy-output, -co |
string | Copy Output | No | Copy output folder to specified location when finished |
The tool uses a structured JSON format for defining captions. Here are the available keys and their purposes:
| Key | Type | Purpose | Required? | Example |
|---|---|---|---|---|
txt |
string | The actual text of the caption | Yes | "txt": "Hello, world!" |
dn |
string | Display name (character name) | No | "dn": "GLaDOS" |
| Key | Type | Purpose | Required? | Example (JSON) | Example (Inline) |
|---|---|---|---|---|---|
ndn |
boolean | Hide display name | No | "ndn": true |
- |
clr |
string | Text color (HEX or RGB) | No, but advised | "clr": "#FF0000" or "clr": "255,0,0" |
<clr:255,0,0> |
playerclr |
string | Player name color | No | "playerclr": "255,255,0:255,0,255" or "playerclr": "#ffff00:#ff00ff" |
<playerclr:255,255,0:255,0,255> |
bold |
boolean | Bold text | No | "bold": true |
<B> |
italic |
boolean | Italic text | No | "italic": true |
<I> |
sfx |
boolean | Sound effect indicator | No | "sfx": true |
<sfx> |
norepeat |
integer | Prevent caption repeat | No | "norepeat": 1 |
<norepeat:1> |
len |
integer | Display duration | No | "len": 5 |
<len:5> |
in/example/subtitles.json:
{
"misc": {
"Act 2": {
"npc_floorturret": {
"clr": "#aaf0d1"
}
}
},
"english": {
"Act 2": {
"npc_floorturret": {
"dn": "Turret",
"lines": {
"talkactive": {
"txt": "Activated!"
},
"talkcollide": {
"txt": "Coming through!"
},
"talkdisabled": {
"txt": "Critical Error!"
},
"talkdeploy": {
"txt": "Deploying!"
},
"talkshotat": {
"txt": "Hey! It's me!"
},
"talktipped": {
"txt": "Ouch!"
},
"talkdissolved": {
"txt": "Ow ow ow!"
},
"talkpickup": {
"txt": "Put me down!"
},
"talkautosearch": {
"txt": "Search mode activated!"
},
"talksearch": {
"txt": "Searching!"
},
"talkretire": {
"txt": "Target lost!"
}
}
}
}
},
"german": {
"Act 2": {
"npc_floorturret": {
"dn": "Turm",
"lines": {
"talkactive": {
"txt": "Aktiviert!"
},
"talkcollide": {
"txt": "Platz da!"
},
"talkdisabled": {
"txt": "Kritischer Fehler!"
},
"talkdeploy": {
"txt": "Hochfahren!"
},
"talkshotat": {
"txt": "He, ich bin's!"
},
"talktipped": {
"txt": "Aua!"
},
"talkdissolved": {
"txt": "Aua!"
},
"talkpickup": {
"txt": "Ich will runter!"
},
"talkautosearch": {
"txt": "Suchmodus aktiviert!"
},
"talksearch": {
"txt": "Suche!"
},
"talkretire": {
"txt": "Zielverlust!"
}
}
}
}
}
}in/example/credits.txt:
2025 timmycelle
subtitles_english.txt:
"lang"
{
"Language" "english"
"Tokens"
{
// ----- Act 2 -----
// --- npc_floorturret ---
"npc_floorturret.talkactive""<clr:170,240,209><B>Turret<B>: Activated!"
"npc_floorturret.talkcollide""<clr:170,240,209><B>Turret<B>: Coming through!"
"npc_floorturret.talkdisabled""<clr:170,240,209><B>Turret<B>: Critical Error!"
"npc_floorturret.talkdeploy""<clr:170,240,209><B>Turret<B>: Deploying!"
"npc_floorturret.talkshotat""<clr:170,240,209><B>Turret<B>: Hey! It's me!"
"npc_floorturret.talktipped""<clr:170,240,209><B>Turret<B>: Ouch!"
"npc_floorturret.talkdissolved""<clr:170,240,209><B>Turret<B>: Ow ow ow!"
"npc_floorturret.talkpickup""<clr:170,240,209><B>Turret<B>: Put me down!"
"npc_floorturret.talkautosearch""<clr:170,240,209><B>Turret<B>: Search mode activated!"
"npc_floorturret.talksearch""<clr:170,240,209><B>Turret<B>: Searching!"
"npc_floorturret.talkretire""<clr:170,240,209><B>Turret<B>: Target lost!"
}
}
// Auto-generated with source-caption-maker 1.0.2 by timmycelle
// More info: https://www.github.com/timmycelle/source-caption-maker
// Generated on 2025-10-31 at 20:36:53 UTC+1:00
// 2025 timmycelle
subtitles_german.txt:
"lang"
{
"Language" "german"
"Tokens"
{
// ----- Act 2 -----
// --- npc_floorturret ---
"npc_floorturret.talkactive""<clr:170,240,209><B>Turm<B>: Aktiviert!"
"npc_floorturret.talkcollide""<clr:170,240,209><B>Turm<B>: Platz da!"
"npc_floorturret.talkdisabled""<clr:170,240,209><B>Turm<B>: Kritischer Fehler!"
"npc_floorturret.talkdeploy""<clr:170,240,209><B>Turm<B>: Hochfahren!"
"npc_floorturret.talkshotat""<clr:170,240,209><B>Turm<B>: He, ich bin's!"
"npc_floorturret.talktipped""<clr:170,240,209><B>Turm<B>: Aua!"
"npc_floorturret.talkdissolved""<clr:170,240,209><B>Turm<B>: Aua!"
"npc_floorturret.talkpickup""<clr:170,240,209><B>Turm<B>: Ich will runter!"
"npc_floorturret.talkautosearch""<clr:170,240,209><B>Turm<B>: Suchmodus aktiviert!"
"npc_floorturret.talksearch""<clr:170,240,209><B>Turm<B>: Suche!"
"npc_floorturret.talkretire""<clr:170,240,209><B>Turm<B>: Zielverlust!"
}
}
// Auto-generated with source-caption-maker 1.0.2 by timmycelle
// More info: https://www.github.com/timmycelle/source-caption-maker
// Generated on 2025-10-31 at 20:36:53 UTC+1:00
// 2025 timmycelle
- Developer Commentary
- GUI
- Replace the use of
captioncompiler.exewith source-caption-compiler for wider compatibility and more convenience
source-caption-maker was exclusively a tool for the development of a work-in-progress Portal 2 mod that I'm apart of, it was built quite differently but was basically the same concept as today's version.
My goal is to help modders quickly make subtitles/captions for their Source Engine mods across multiple languages, and potentially automate it.
Can I convert the subtitle/caption files of Source games to JSON (that's compatible with SCM) and then modify that to my liking?
I have thought about making this, but I'm not sure how I go about making it yet. It's definitely still on my mind though.
Definitely.
Not yet - it's planned.
