Skip to content

Commit 4ec4f86

Browse files
Add text-to-speech (discordia-space#7342)
* Add text-to-speech (discordia-space#7316) * gitignore * rust lib * first iteration of text-to-speech * first amendment (discordia-space#7318) * Add some error logs for tts (discordia-space#7321) * add error logs for tts * forgot to use the var * one more verb (discordia-space#7323) * finally give voice to the people (discordia-space#7330) * Another batch of TTS improvements for test server (discordia-space#7334) * removed debug proc * tts second iteration * itsy-bitsy fix (discordia-space#7335) * more tts utility and working voice changer (discordia-space#7339) * tts third iteration * ci fail
1 parent 82f16bc commit 4ec4f86

File tree

37 files changed

+864
-135
lines changed

37 files changed

+864
-135
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
#Ignore byond config folder.
88
/cfg/**/*
99

10+
#Ignore cached sound files.
11+
/sound/tts_cache/**/*
12+
/sound/tts_scrambled/**/*
13+
1014
# Ignore compiled linux libs in the root folder, e.g. librust_g.so
1115
/*.so
1216

cev_eris.dme

+4
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
#include "code\__DEFINES\qdel.dm"
6262
#include "code\__DEFINES\reagents.dm"
6363
#include "code\__DEFINES\research.dm"
64+
#include "code\__DEFINES\rust_g.dm"
6465
#include "code\__DEFINES\sanity.dm"
6566
#include "code\__DEFINES\shields.dm"
6667
#include "code\__DEFINES\spaceman_dmm.dm"
@@ -1456,6 +1457,7 @@
14561457
#include "code\modules\client\preference_setup\global\01_ui.dm"
14571458
#include "code\modules\client\preference_setup\global\02_prefixes.dm"
14581459
#include "code\modules\client\preference_setup\global\03_pai.dm"
1460+
#include "code\modules\client\preference_setup\global\04_ooc.dm"
14591461
#include "code\modules\client\preference_setup\global\05_settings.dm"
14601462
#include "code\modules\client\preference_setup\global\06_media.dm"
14611463
#include "code\modules\client\preference_setup\global\preferences.dm"
@@ -2736,6 +2738,8 @@
27362738
#include "code\modules\telesci\circuits.dm"
27372739
#include "code\modules\telesci\telepads.dm"
27382740
#include "code\modules\telesci\telesci_computer.dm"
2741+
#include "code\modules\text_to_speech\tts_html.dm"
2742+
#include "code\modules\text_to_speech\tts_main.dm"
27392743
#include "code\modules\tips_and_tricks\gameplay.dm"
27402744
#include "code\modules\tips_and_tricks\jobs.dm"
27412745
#include "code\modules\tips_and_tricks\mobs.dm"

code/__DEFINES/admin.dm

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#define MUTE_PRAY (1<<2)
77
#define MUTE_ADMINHELP (1<<3)
88
#define MUTE_DEADCHAT (1<<4)
9+
#define MUTE_TTS (1<<5)
910
#define MUTE_ALL (~0)
1011

1112
// Number of identical messages required to get the spam-prevention auto-mute thing to trigger warnings and automutes.

code/__DEFINES/misc.dm

+4
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,7 @@
356356
/// Misc atmos equations
357357

358358
#define FIRESTACKS_TEMP_CONV(firestacks) min(5200,max(2.25*round(FIRESUIT_MAX_HEAT_PROTECTION_TEMPERATURE*(fire_stacks/FIRE_MAX_FIRESUIT_STACKS)**2), 700))
359+
360+
#define TTS_SEED_DEFAULT_FEMALE "Female_1"
361+
#define TTS_SEED_DEFAULT_MALE "Male_1"
362+
#define TTS_SEED_ANNOUNCER "Robot_2"

code/__DEFINES/rust_g.dm

+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// rust_g.dm - DM API for rust_g extension library
2+
//
3+
// To configure, create a `rust_g.config.dm` and set what you care about from
4+
// the following options:
5+
//
6+
// #define RUST_G "path/to/rust_g"
7+
// Override the .dll/.so detection logic with a fixed path or with detection
8+
// logic of your own.
9+
//
10+
// #define RUSTG_OVERRIDE_BUILTINS
11+
// Enable replacement rust-g functions for certain builtins. Off by default.
12+
13+
#ifndef RUST_G
14+
// Default automatic RUST_G detection.
15+
// On Windows, looks in the standard places for `rust_g.dll`.
16+
// On Linux, looks in `.`, `$LD_LIBRARY_PATH`, and `~/.byond/bin` for either of
17+
// `librust_g.so` (preferred) or `rust_g` (old).
18+
19+
/* This comment bypasses grep checks */ /var/__rust_g
20+
21+
/proc/__detect_rust_g()
22+
if (world.system_type == UNIX)
23+
if (fexists("./librust_g.so"))
24+
// No need for LD_LIBRARY_PATH badness.
25+
return __rust_g = "./librust_g.so"
26+
else if (fexists("./rust_g"))
27+
// Old dumb filename.
28+
return __rust_g = "./rust_g"
29+
else if (fexists("[world.GetConfig("env", "HOME")]/.byond/bin/rust_g"))
30+
// Old dumb filename in `~/.byond/bin`.
31+
return __rust_g = "rust_g"
32+
else
33+
// It's not in the current directory, so try others
34+
return __rust_g = "librust_g.so"
35+
else
36+
return __rust_g = "rust_g"
37+
38+
#define RUST_G (__rust_g || __detect_rust_g())
39+
#endif
40+
41+
/// Gets the version of rust_g
42+
/proc/rustg_get_version() return call(RUST_G, "get_version")()
43+
44+
45+
/**
46+
* Sets up the Aho-Corasick automaton with its default options.
47+
*
48+
* The search patterns list and the replacements must be of the same length when replace is run, but an empty replacements list is allowed if replacements are supplied with the replace call
49+
* Arguments:
50+
* * key - The key for the automaton, to be used with subsequent rustg_acreplace/rustg_acreplace_with_replacements calls
51+
* * patterns - A non-associative list of strings to search for
52+
* * replacements - Default replacements for this automaton, used with rustg_acreplace
53+
*/
54+
#define rustg_setup_acreplace(key, patterns, replacements) call(RUST_G, "setup_acreplace")(key, json_encode(patterns), json_encode(replacements))
55+
56+
/**
57+
* Sets up the Aho-Corasick automaton using supplied options.
58+
*
59+
* The search patterns list and the replacements must be of the same length when replace is run, but an empty replacements list is allowed if replacements are supplied with the replace call
60+
* Arguments:
61+
* * key - The key for the automaton, to be used with subsequent rustg_acreplace/rustg_acreplace_with_replacements calls
62+
* * options - An associative list like list("anchored" = 0, "ascii_case_insensitive" = 0, "match_kind" = "Standard"). The values shown on the example are the defaults, and default values may be omitted. See the identically named methods at https://docs.rs/aho-corasick/latest/aho_corasick/struct.AhoCorasickBuilder.html to see what the options do.
63+
* * patterns - A non-associative list of strings to search for
64+
* * replacements - Default replacements for this automaton, used with rustg_acreplace
65+
*/
66+
#define rustg_setup_acreplace_with_options(key, options, patterns, replacements) call(RUST_G, "setup_acreplace")(key, json_encode(options), json_encode(patterns), json_encode(replacements))
67+
68+
/**
69+
* Run the specified replacement engine with the provided haystack text to replace, returning replaced text.
70+
*
71+
* Arguments:
72+
* * key - The key for the automaton
73+
* * text - Text to run replacements on
74+
*/
75+
#define rustg_acreplace(key, text) call(RUST_G, "acreplace")(key, text)
76+
77+
/**
78+
* Run the specified replacement engine with the provided haystack text to replace, returning replaced text.
79+
*
80+
* Arguments:
81+
* * key - The key for the automaton
82+
* * text - Text to run replacements on
83+
* * replacements - Replacements for this call. Must be the same length as the set-up patterns
84+
*/
85+
#define rustg_acreplace_with_replacements(key, text, replacements) call(RUST_G, "acreplace_with_replacements")(key, text, json_encode(replacements))
86+
87+
/**
88+
* This proc generates a cellular automata noise grid which can be used in procedural generation methods.
89+
*
90+
* Returns a single string that goes row by row, with values of 1 representing an alive cell, and a value of 0 representing a dead cell.
91+
*
92+
* Arguments:
93+
* * percentage: The chance of a turf starting closed
94+
* * smoothing_iterations: The amount of iterations the cellular automata simulates before returning the results
95+
* * birth_limit: If the number of neighboring cells is higher than this amount, a cell is born
96+
* * death_limit: If the number of neighboring cells is lower than this amount, a cell dies
97+
* * width: The width of the grid.
98+
* * height: The height of the grid.
99+
*/
100+
#define rustg_cnoise_generate(percentage, smoothing_iterations, birth_limit, death_limit, width, height) \
101+
call(RUST_G, "cnoise_generate")(percentage, smoothing_iterations, birth_limit, death_limit, width, height)
102+
103+
#define rustg_dmi_strip_metadata(fname) call(RUST_G, "dmi_strip_metadata")(fname)
104+
#define rustg_dmi_create_png(path, width, height, data) call(RUST_G, "dmi_create_png")(path, width, height, data)
105+
#define rustg_dmi_resize_png(path, width, height, resizetype) call(RUST_G, "dmi_resize_png")(path, width, height, resizetype)
106+
107+
#define rustg_file_read(fname) call(RUST_G, "file_read")(fname)
108+
#define rustg_file_exists(fname) call(RUST_G, "file_exists")(fname)
109+
#define rustg_file_write(text, fname) call(RUST_G, "file_write")(text, fname)
110+
#define rustg_file_append(text, fname) call(RUST_G, "file_append")(text, fname)
111+
#define rustg_file_get_line_count(fname) text2num(call(RUST_G, "file_get_line_count")(fname))
112+
#define rustg_file_seek_line(fname, line) call(RUST_G, "file_seek_line")(fname, "[line]")
113+
114+
#ifdef RUSTG_OVERRIDE_BUILTINS
115+
#define file2text(fname) rustg_file_read("[fname]")
116+
#define text2file(text, fname) rustg_file_append(text, "[fname]")
117+
#endif
118+
119+
#define rustg_git_revparse(rev) call(RUST_G, "rg_git_revparse")(rev)
120+
#define rustg_git_commit_date(rev) call(RUST_G, "rg_git_commit_date")(rev)
121+
122+
#define RUSTG_HTTP_METHOD_GET "get"
123+
#define RUSTG_HTTP_METHOD_PUT "put"
124+
#define RUSTG_HTTP_METHOD_DELETE "delete"
125+
#define RUSTG_HTTP_METHOD_PATCH "patch"
126+
#define RUSTG_HTTP_METHOD_HEAD "head"
127+
#define RUSTG_HTTP_METHOD_POST "post"
128+
#define rustg_http_request_blocking(method, url, body, headers, options) call(RUST_G, "http_request_blocking")(method, url, body, headers, options)
129+
#define rustg_http_request_async(method, url, body, headers, options) call(RUST_G, "http_request_async")(method, url, body, headers, options)
130+
#define rustg_http_check_request(req_id) call(RUST_G, "http_check_request")(req_id)
131+
132+
#define RUSTG_JOB_NO_RESULTS_YET "NO RESULTS YET"
133+
#define RUSTG_JOB_NO_SUCH_JOB "NO SUCH JOB"
134+
#define RUSTG_JOB_ERROR "JOB PANICKED"
135+
136+
#define rustg_json_is_valid(text) (call(RUST_G, "json_is_valid")(text) == "true")
137+
138+
#define rustg_log_write(fname, text, format) call(RUST_G, "log_write")(fname, text, format)
139+
/proc/rustg_log_close_all() return call(RUST_G, "log_close_all")()
140+
141+
#define rustg_noise_get_at_coordinates(seed, x, y) call(RUST_G, "noise_get_at_coordinates")(seed, x, y)
142+
143+
#define rustg_sql_connect_pool(options) call(RUST_G, "sql_connect_pool")(options)
144+
#define rustg_sql_query_async(handle, query, params) call(RUST_G, "sql_query_async")(handle, query, params)
145+
#define rustg_sql_query_blocking(handle, query, params) call(RUST_G, "sql_query_blocking")(handle, query, params)
146+
#define rustg_sql_connected(handle) call(RUST_G, "sql_connected")(handle)
147+
#define rustg_sql_disconnect_pool(handle) call(RUST_G, "sql_disconnect_pool")(handle)
148+
#define rustg_sql_check_query(job_id) call(RUST_G, "sql_check_query")("[job_id]")
149+
150+
#define rustg_time_microseconds(id) text2num(call(RUST_G, "time_microseconds")(id))
151+
#define rustg_time_milliseconds(id) text2num(call(RUST_G, "time_milliseconds")(id))
152+
#define rustg_time_reset(id) call(RUST_G, "time_reset")(id)
153+
154+
#define rustg_raw_read_toml_file(path) json_decode(call(RUST_G, "toml_file_to_json")(path) || "null")
155+
156+
/proc/rustg_read_toml_file(path)
157+
var/list/output = rustg_raw_read_toml_file(path)
158+
if (output["success"])
159+
return output["content"]
160+
else
161+
CRASH(output["content"])
162+
163+
#define rustg_url_encode(text) call(RUST_G, "url_encode")("[text]")
164+
#define rustg_url_decode(text) call(RUST_G, "url_decode")(text)
165+
166+
#ifdef RUSTG_OVERRIDE_BUILTINS
167+
#define url_encode(text) rustg_url_encode(text)
168+
#define url_decode(text) rustg_url_decode(text)
169+
#endif
170+

code/__HELPERS/time.dm

+1
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ var/global/rollovercheck_last_timeofday = 0
107107
//Increases delay as the server gets more overloaded,
108108
//as sleeps aren't cheap and sleeping only to wake up and sleep again is wasteful
109109
#define DELTA_CALC max(((max(world.tick_usage, world.cpu) / 100) * max(Master.sleep_delta,1)), 1)
110+
#define UNTIL(X) while(!X) stoplag()
110111

111112
/proc/stoplag()
112113
if (!Master || !(Master.current_runlevel & RUNLEVELS_DEFAULT))

code/controllers/configuration.dm

+12
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,10 @@ GLOBAL_LIST_EMPTY(storyteller_cache)
223223
var/webhook_url
224224
var/webhook_key
225225

226+
var/tts_bearer // Token we use to talk with text-to-speech service
227+
var/tts_enabled // Global switch
228+
var/tts_cache // Store generated tts files and reuse them, instead of always requesting new
229+
226230
var/static/regex/ic_filter_regex //For the cringe filter.
227231

228232
var/generate_loot_data = FALSE //for loot rework
@@ -722,6 +726,14 @@ GLOBAL_LIST_EMPTY(storyteller_cache)
722726
if("webhook_url")
723727
config.webhook_url = value
724728

729+
if("tts_bearer")
730+
config.tts_bearer = value
731+
732+
if("tts_enabled")
733+
config.tts_enabled = config.tts_bearer ? value : FALSE
734+
735+
if("tts_cache")
736+
config.tts_cache = value
725737

726738
if("random_start")
727739
var/list/startlist = list(

code/controllers/subsystems/chat.dm

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ SUBSYSTEM_DEF(chat)
88
var/list/payload = list()
99

1010

11+
/datum/controller/subsystem/chat/Initialize()
12+
. = ..()
13+
init_tts_directories()
14+
15+
1116
/datum/controller/subsystem/chat/fire()
1217
for(var/i in payload)
1318
var/client/C = i

code/datums/uplink/announcements.dm

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
var/message = input(user, "What would you like the text of the announcement to be? Write as much as you like, The title will appear as Unknown Broadcast", "False Announcement") as text|null
2121
if (!message)
2222
return FALSE
23-
command_announcement.Announce(message, "Unknown Broadcast")
23+
command_announcement.Announce(message, "Unknown Broadcast", use_text_to_speech = TRUE)
2424
return 1
2525

2626
/datum/uplink_item/abstract/announcements/fake_crew_arrival

code/defines/procs/announce.dm

+14-14
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
title = "Security Announcement"
3131
announcement_type = "Security Announcement"
3232

33-
/datum/announcement/proc/Announce(var/message as text, var/new_title = "", var/new_sound = null, var/do_newscast = newscast, var/msg_sanitized = 0, var/zlevels = GLOB.maps_data.contact_levels)
33+
/datum/announcement/proc/Announce(message, new_title = "", new_sound, do_newscast = newscast, msg_sanitized, zlevels = GLOB.maps_data.contact_levels, use_text_to_speech)
3434
if(!message)
3535
return
3636
var/message_title = new_title ? new_title : title
@@ -40,7 +40,7 @@
4040
message = sanitize(message, extra = 0)
4141
message_title = html_encode(message_title)
4242

43-
Message(message, message_title)
43+
Message(message, message_title, use_text_to_speech)
4444
if(do_newscast)
4545
NewsCast(message, message_title)
4646

@@ -49,20 +49,20 @@
4949
sound_to(M, message_sound)
5050
Log(message, message_title)
5151

52-
datum/announcement/proc/Message(message as text, message_title as text)
53-
global_announcer.autosay("<span class='warning'>[title]:</span> [message]", announcer ? announcer : ANNOUNCER_NAME)
52+
datum/announcement/proc/Message(message as text, message_title as text, use_text_to_speech)
53+
global_announcer.autosay("<span class='warning'>[title]:</span> [message]", announcer ? announcer : ANNOUNCER_NAME, use_text_to_speech = use_text_to_speech)
5454

55-
datum/announcement/minor/Message(message as text, message_title as text)
56-
global_announcer.autosay(message, ANNOUNCER_NAME)
55+
datum/announcement/minor/Message(message as text, message_title as text, use_text_to_speech)
56+
global_announcer.autosay(message, ANNOUNCER_NAME, use_text_to_speech = use_text_to_speech)
5757

58-
datum/announcement/priority/Message(message as text, message_title as text)
59-
global_announcer.autosay("<span class='alert'>[message_title]:</span> [message]", announcer ? announcer : ANNOUNCER_NAME)
58+
datum/announcement/priority/Message(message as text, message_title as text, use_text_to_speech)
59+
global_announcer.autosay("<span class='alert'>[message_title]:</span> [message]", announcer ? announcer : ANNOUNCER_NAME, use_text_to_speech = use_text_to_speech)
6060

61-
datum/announcement/priority/command/Message(message as text, message_title as text)
62-
global_announcer.autosay("<span class='warning'>[message_title]:</span> [message]", ANNOUNCER_NAME)
61+
datum/announcement/priority/command/Message(message as text, message_title as text, use_text_to_speech)
62+
global_announcer.autosay("<span class='warning'>[message_title]:</span> [message]", ANNOUNCER_NAME, use_text_to_speech = use_text_to_speech)
6363

64-
datum/announcement/priority/security/Message(message as text, message_title as text)
65-
global_announcer.autosay("<font color='red'>[message_title]:</span> [message]", ANNOUNCER_NAME)
64+
datum/announcement/priority/security/Message(message as text, message_title as text, use_text_to_speech)
65+
global_announcer.autosay("<font color='red'>[message_title]:</span> [message]", ANNOUNCER_NAME, use_text_to_speech = use_text_to_speech)
6666

6767
datum/announcement/proc/NewsCast(message as text, message_title as text)
6868
if(!newscast)
@@ -120,6 +120,6 @@ datum/announcement/proc/Log(message as text, message_title as text)
120120
/proc/AnnounceArrival(var/mob/living/character, var/rank, var/join_message)
121121
if (join_message && SSticker.current_state == GAME_STATE_PLAYING && SSjob.ShouldCreateRecords(rank))
122122
if(issilicon(character))
123-
global_announcer.autosay("A new [rank] [join_message].", ANNOUNCER_NAME)
123+
global_announcer.autosay("A new [rank] [join_message].", ANNOUNCER_NAME, use_text_to_speech = TRUE)
124124
else
125-
global_announcer.autosay("[character.real_name], [rank], [join_message].", ANNOUNCER_NAME)
125+
global_announcer.autosay("[character.real_name], [rank], [join_message].", ANNOUNCER_NAME, use_text_to_speech = TRUE)

code/game/gamemodes/malfunction/newmalf_ability_trees/tree_networking.dm

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
announce_hack_failure(user, "quantum message relay")
107107
return
108108

109-
command_announcement.Announce(text, title)
109+
command_announcement.Announce(text, title, use_text_to_speech = TRUE)
110110

111111
/datum/game_mode/malfunction/verb/elite_encryption_hack()
112112
set category = "Software"

0 commit comments

Comments
 (0)