Subtitle Engine

Introduction

image

Welcome to Subtitle Engine!

Subtitle Engine is a free, open source, advanced subtitle system intended to improve accessibility for users with auditory-related impairments.

Most recent release: Subtitle Engine 1.01

Features:

  • Quick and easy setup
  • Global subtitles
  • User-specific (local) subtitles
  • Subtitles run by the client
  • Speaker profiles
  • Support for rich text tags
  • Creation and modification of speaker profiles during runtime
  • Modification of certain settings during a running subtitle via control tags
  • Waiting during a subtitle via control tags
  • Running multiple sequential subtitles with one command
  • Localization

Known Issues:

  • Instant subtitles that are longer than one line have a choppy move in animation.

Updates:

Version 1.01:

  • Fixed a bug preventing toggleSubtitles() from functioning properly.
  • Clarified type formatting on globalSubtitle(), localSubtitle(), createProfile(), and editProfile() to indicate the use of a table.
  • Added error messages relating to localization and the getString() function.
  • Removed the incomplete instructions from the SubtitleEngine module, excluding installation instructions.

Documentation

Note: Not all of the documentation has been completed. If you have any questions regarding the use of Subtitle Engine, reply to this post and I will answer your question.

Setup

Initial Setup

  1. Import Subtitle Engine to your project using this link.
  2. Move the SubtitleEngine module into game.ReplicatedStorage.
  3. Move the Subtitles ScreenGui into game.StarterGui.
  4. [Optional] Set the Enabled property of the Subtitles ScreenGui to false.

You’re done! Setup is that simple.
If you decide to move the SubtitleEngine module into a folder or into some other client accessible location, make sure you update the path to it on the first line of Subtitles.SubtitleController.

Settings

Documentation incomplete.

Updating Versions

Updating your copy of Subtitle Engine is relatively simple, and should take less than a minute.

  1. Import the newest version of Subtitle Engine to your project using this link.
  2. Open the SubtitleEngine module for both the old and new versions and locate the settings.
  3. Copy the entire subtitles.profiles table from the old version and replace it in the new version.
  4. For each setting in the new version, replace the value with the desired value using the old version as a reference. It is not recommended to copy and paste the entire settings list in case new settings were added in the update.
  5. Delete the old SubtitleEngine module and Subtitles ScreenGui.
  6. Proceed with setup starting at step 2.

That’s it! Enjoy the new version.

Customization

Documentation incomplete.


Running Subtitles

globalSubtitle()

SERVER

se.globalSubtitle(profile : string, text : string, extraOptions : {})

Displays a subtitle for all users.

Example:

local se = require(game:GetService("ReplicatedStorage").SubtitleEngine)

se.globalSubtitle("Debug", "This is a global subtitle using the Debug profile!")

For information on the use of profile, see Speaker Profiles.
For information on the use of extraOptions, see Extra Options.

localSubtitle()

SERVERCLIENT

se.localSubtitle(player : Player, profile : string, text : string, extraOptions : {})

Displays a subtitle for a specific user.

When called from the client, the first argument, for player, may be omitted. However, it must be present if called from the server. Attempting to run the subtitle for another user will result in the subtitle appearing to the client that called it.

Server Example:

local se = require(game:GetService("ReplicatedStorage").SubtitleEngine)

se.localSubtitle(game.Players.Norteous, "Debug", "This is a local subtitle using the Debug profile!")

Replace my username, Norteous, with the username of the player you want to receive the subtitle.
Do note that this server example technically works when called from the client, but the client example shown below is a better option.

Client Example:

local se = require(game:GetService("ReplicatedStorage"):WaitForChild("SubtitleEngine"))

se.localSubtitle("Debug", "This is a local subtitle using the Debug profile!")

For information on the use of profile, see Speaker Profiles.
For information on the use of extraOptions, see Extra Options.

Sequential Subtitles

The text parameter found in globalSubtitle() and localSubtitle() can also be filled with multiple strings by providing them in a table. This will run them separately as sequential subtitles.

Sequential subtitles will wait for the previous subtitle to finish before starting the next one, allowing you to initiate multiple subtitles in the same function without needing to manually adjust wait times between them.

Example:

local se = require(game:GetService("ReplicatedStorage").SubtitleEngine)

se.globalSubtitle("Debug", {
"Example subtitle 1", -- Runs first
"Example subtitle 2", -- Runs second after 1 is completed
"Example subtitle 3" -- Runs third after 1 and 2 are completed
})

toggleSubtitles()

CLIENT

se.toggleSubtitles(state : boolean)

Enables or disables subtitles on the client. Overridden by the forced extra option. Subtitles are enabled by default.

Example:

local se = require(game:GetService("ReplicatedStorage"):WaitForChild("SubtitleEngine"))

se.toggleSubtitles(false) -- Disables subtitles for this client.

se.toggleSubtitles(true) -- Enables subtitles for this client.

Speaker Profiles

All subtitles in Subtitle Engine run on speaker profiles. These profiles determine the subtitle’s appearance as well as how the subtitle is typed.

Creating Profiles

Subtitle profiles can be created in two ways:

  1. Before the game runs, in the SubtitleEngine module.
  2. During runtime, through the createProfile() function.

It is recommended to use the first method if you plan on reusing the same profile in multiple scripts. However, the second option can be useful for profiles that are only be used once, or cannot be planned ahead of time, like using a player as a speaker.

To create a profile in the SubtitleEngine module, locate the subtitles.profiles table. Once there, you should see a Debug profile as shown below.

subtitles.profiles = {
	Debug = { -- Unique name that will be referenced when creating a subtitle
			speaker = "Debug", -- Name that will be displayed in the subtitle
			speakerColor = Color3.fromRGB(255,100,100), -- The color of the speaker's name
			textColor = Color3.fromRGB(255,255,255), -- The color of what is being said
			typeDelay = 0.04, -- How long is waited between characters
			characterRange = {1,1}, -- How many characters are printed out at once. Chooses randomly within the range provided.
			extraWaitTime = 5 -- The extra amount of seconds that the subtitle should be visible after typing is completed. Should always be above 1.
		},
}

To create a new profile, simply copy and paste an existing profile, or type it by hand, and modify the options as needed. Note that the profile name must be unique, and should be easy to remember since it is used every time a subtitle is created.

For information on creating profiles during runtime, see createProfile().

Profile Options

Profile Option Effect
Profile Name The unique name that will be referenced when creating a subtitle
speaker Name that will be displayed in the subtitle
speakerColor The color of the speaker’s name
textColor The color of what is being said
typeDelay How much time is waited between characters in seconds
characterRange How many characters are printed out at once. Chooses randomly within the range provided.
extraWaitTime The extra amount of seconds that the subtitle should be visible after typing is completed. Should always be above 1, though 5+ is recommended.

createProfile()

SERVERCLIENT

se.createProfile(profileName : string, data : {})

Documentation incomplete.

editProfile()

SERVERCLIENT

se.editProfile(profileName : string, data : {})

Documentation incomplete.


Rich Text Tags

Subtitle Engine supports most Rich Text Tags, see Roblox’s documentation on them for information on usage. You may encounter issues if you attempt to heavily modify the size of text.


Control Tags

Control tags allow certain profile options to be changed temporarily for a section of a subtitle.

To open a control tag, follow the following format:

<tag = value>

Most control tags need to be closed. To do so, simply add a forward slash (/ ) in front of the tag.

</tag>

Example:

local se = require(game:GetService("ReplicatedStorage").SubtitleEngine)

se.globalSubtitle("Debug", "Hmm<typeDelay = 0.4>....</typeDelay> I wonder what that was.")

typeDelay

<typeDelay = number> string </typeDelay>

Temporarily changes the typeDelay profile option to the number provided until closed.

characterRange

<characterRange = {number, number}> string <characterRange>

Temporarily changes the characterRange profile option to the number range provided until closed.

wait

<wait = number>

Pauses subtitle typing and waits for the number provided in seconds before continuing. The wait control tag cannot be closed.


Extra Options

Write a description

Profile Modification

Documentation incomplete.

forced

Documentation incomplete.


Localization

Write a description

String Tables

Documentation incomplete.

getString()

SERVERCLIENT

se.getString(group : string, index : number)

Documentation incomplete.

setLanguage()

CLIENT

se.setLanguage(language : string)

Documentation incomplete.


License

Subtitle Engine uses the MIT License, meaning you can do essentially anything with it. Although credit for use is always appreciated, it is not required.

24 Likes

Performance wise how is this?

Could you show how performant this is and why we should use this over lets say other systems which could substitute like notification frameworks. I use notif frameworks as an cheap example just to wrap the head around what I mean and for context.

We should normalise publishing performance stats. Not directed at you but in general.

Other than that its pretty chill.

What performance stats would you like to see? I don’t think Subtitle Engine is very costly on performance but would be happy to provide such data.

The reason to use Subtitle Engine over something like a sidebar notification is because the subtitles are designed the follow along with audio. In a situation where there are many sounds playing, sidebar notifications could start taking up a significant portion of a player’s screen. Another benefit is that the player does not have to look too far away from the center of the screen to read a subtitle.
Overall, I think that subtitles are simply a better option than notifications for dictation.

2 Likes

Anything and everything honesty. I like to dig deep with performance/resources optimisation.

Do get back to me soon however its current 2:37am here (GMT +0, I am not switching to some silly daylights savings thing) so I will reply a bit late.

I’ll look into what information I can provide.

Unfortunately I don’t think I will be able to send it for a few hours as my computer is currently having an issue booting. Hopefully it won’t take that long to fix though.

1 Like

Execution time for a local subtitle from the client:

local subtitleEngine = require(game:GetService("ReplicatedStorage").SubtitleEngine)
task.wait(6)
for i=1,5 do
	local start = os.clock()
	subtitleEngine.localSubtitle("Debug", "This is a subtitle!")
	print(os.clock()-start)
end

Times:
0.0187 milliseconds
0.0328 milliseconds
0.0079 milliseconds
0.0113 milliseconds
0.0158 milliseconds
Average Time: 0.0173 milliseconds

Execution time for a local subtitle from the server:

local subtitleEngine = require(game:GetService("ReplicatedStorage").SubtitleEngine)
task.wait(6)
for i=1,5 do
	local start = os.clock()
	subtitleEngine.localSubtitle(game.Players.Norteous, "Debug", "This is a subtitle!")
	print(os.clock()-start)
end

Times:
0.0304 milliseconds
0.0105 milliseconds
0.0168 milliseconds
0.0073 milliseconds
0.006 milliseconds
Average Time: 0.0142 milliseconds

Execution time for a global subtitle:

local subtitleEngine = require(game:GetService("ReplicatedStorage").SubtitleEngine)
task.wait(6)
for i=1,5 do
	local start = os.clock()
	subtitleEngine.globalSubtitle("Debug", "This is a subtitle!")
	print(os.clock()-start)
end

Times:
0.0215 milliseconds
0.0074 milliseconds
0.005 milliseconds
0.0044 milliseconds
0.0159 milliseconds
Average Time: 0.01084 milliseconds

Running 1 subtitle every second.

local subtitleEngine = require(game:GetService("ReplicatedStorage").SubtitleEngine)
task.wait(6)
while task.wait(1) do
	subtitleEngine.globalSubtitle("Debug", "This is a subtitle!")
end

image


I have no clue how to use the MicroProfiler properly, I hope these screenshots are useful.

I don’t really know that much about monitoring script performance. I hope this included the information you wanted to see.

1 Like

Seems fairly optimised which is great. I can be the same with the micro profiler sometimes but seems to be good enough. I will consider this for a WIP project.

1 Like

Documentation Update

Added documentation for

  • Controls Tags
    • typeDelay
    • characterRange
    • wait

Wow, this is actually great!
I start wondering if you took inspiration from this…

The only problem I have with it is that you can’t combine the same subtitles or have a way to toggle off the typing animation.

I took some inspiration from the appearance of IITPP’s subtitles, but I haven’t actually looked into the code.

The typing animation can be togged off by setting the typeDelay to 0. Could you specify what you mean by combining subtitles?

1 Like


Like this.

Basically when two of the same things are fired, it puts up a (x) with the amount of duplicates.

While this is not currently a feature, it is certainly something I will look into adding to Subtitle Engine in the near future.

1 Like

Still though, this is definitely something I would use. I even like how the rich text never breaks off!

1 Like

Now that I think of it, adding a time until for the wait would be helpful

something like this incrementing

subtitles:FireAllClients("<font color='rgb(114, 167, 236)'>Attention Reactor Operations personel,</font>")
subtitles:FireAllClients("<font color='rgb(114, 167, 236)'>WHY AM I RECIEVING REPORTS, OF A <b>BLACK HOLE</b>, IN THE CORE CHAMBER?!?</font>", 3.1)
subtitles:FireAllClients("<font color='rgb(114, 167, 236)'>EVERYONE IN THE REACTOR CONTROL ROOM IS <b>FIRED</b>.</font>", 8.8)
subtitles:FireAllClients("<font color='rgb(114, 167, 236)'>BOX YOUR STUFF.</font>", 11.3)
subtitles:FireAllClients("<font color='rgb(114, 167, 236)'>OUT.</font>", 12.6)
subtitles:FireAllClients("<font color='rgb(114, 167, 236)'>PARKING LOT.</font>", 13)
subtitles:FireAllClients("<font color='rgb(114, 167, 236)'>GOODBYE.</font>", 13.9)

Basically the number on the left waits until the timer has reached the specified time

I could add a separate function for this. However, I recommend using sequential subtitles instead. That way, all of the function calls can be condensed into one without requiring as much effort.

local se = require(game:GetService("ReplicatedStorage").SubtitleEngine)

se.globalSubtitle("Debug", {
"<font color='rgb(114, 167, 236)'>Attention Reactor Operations personel,</font>",
"<font color='rgb(114, 167, 236)'>WHY AM I RECIEVING REPORTS, OF A <b>BLACK HOLE</b>, IN THE CORE CHAMBER?!?</font>",
"<font color='rgb(114, 167, 236)'>EVERYONE IN THE REACTOR CONTROL ROOM IS <b>FIRED</b>.</font>", 
"<font color='rgb(114, 167, 236)'>BOX YOUR STUFF.</font>",
"<font color='rgb(114, 167, 236)'>OUT.</font>",
"<font color='rgb(114, 167, 236)'>PARKING LOT.</font>",
"<font color='rgb(114, 167, 236)'>GOODBYE.</font>"
})

In this example, each string would run as its own subtitle immediately after the previous string finishes. If you want to add an extra delay between the strings running, you can use the wait control tag at the end of each.

Edit: Just an extra note, but speaker profiles contain an option that will globally change the color of all text for subtitles using that speaker.

a function would probably be better though…

How come? I don’t see the purpose of calling every subtitle in a separate special function that pauses the script when you can do the same thing through one function call.
Could you explain why you think it would be beneficial so that I can better understand?

Oh, I meant without the typing effect, my bad

The typing effect can be removed by setting the typeDelay value of the speaker profile to 0.


Alternatively, if you wish to remove the typing from a specific subtitle while keeping it on for the speaker you are using, you can use an extraOption to overwrite the profile.

local se = require(game:GetService("ReplicatedStorage").SubtitleEngine)

se.globalSubtitle("Debug", "This subtitle won't type!", {typeDelay = 0})

Yeah, but then it won’t measure out how long it would take for it to finish if it were typing