Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5870383
piper
ynot01 Nov 26, 2023
90d06a8
Update say.dm
ynot01 Nov 26, 2023
99f9c84
print debug info
ynot01 Nov 26, 2023
74a418c
remove binaries, update to work with api
ynot01 Nov 27, 2023
1e9b635
use cache oops
ynot01 Nov 27, 2023
91a5792
move some things around
ynot01 Nov 27, 2023
fd90e37
pitch
ynot01 Nov 27, 2023
9526083
pitch pref
ynot01 Nov 27, 2023
1d1e482
remove unused var
ynot01 Nov 27, 2023
f96694e
prefs UI
ynot01 Nov 27, 2023
c7ea5ed
fix pref bugs
ynot01 Nov 27, 2023
e110dde
thats a number not a string
ynot01 Nov 27, 2023
0c5c21d
0 pitch poly is in my nightmares
ynot01 Nov 27, 2023
9482539
"enable" config, alive ping checking
ynot01 Nov 27, 2023
7a5654e
better region names
ynot01 Nov 27, 2023
2d39b19
flag not string
ynot01 Nov 27, 2023
730c4e3
better american region names
ynot01 Nov 27, 2023
e1652e6
change cache cleanup dir
ynot01 Nov 27, 2023
b45f54f
radio pref
ynot01 Nov 27, 2023
2827724
whispering is quieter now
ynot01 Nov 28, 2023
bc6e5bb
loudmode is now louter
ynot01 Nov 28, 2023
78a46cd
filter support pog
ynot01 Nov 28, 2023
d144d2f
more filter stuff
ynot01 Nov 28, 2023
2864c6d
rename filters
ynot01 Nov 28, 2023
49e0ecb
tts player cap + give tongues to ipc/eth/pret
ynot01 Nov 29, 2023
3ff2904
Merge branch 'master' into piper-tts
ynot01 Nov 29, 2023
d2e2300
fix deleted var
ynot01 Nov 29, 2023
d344beb
Merge branch 'piper-tts' of https://github.com/ynot01/Yogstation into…
ynot01 Nov 29, 2023
3030008
fix radio being random output
ynot01 Nov 29, 2023
b3733df
simple bots get machine speak too!
ynot01 Nov 29, 2023
ba33c6b
piper cache was moved to tmp
ynot01 Nov 29, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions code/__HELPERS/mobs.dm
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,38 @@ GLOBAL_LIST_INIT(skin_tone_names, list(
"mixed4" = "Macadamia",
))

GLOBAL_LIST_INIT(tts_voices_names, sortList(list(
"GB-alba" = "Alba (Scottish Female)",
"GB-aru" = "Aru (North-East English Female)",
"GB-jenny_dioco" = "Jenny (Welsh Female)",
"GB-northern_english_male" = "Josh (Yorkshire Male)",
"GB-southern_english_female" = "Lucy (London Female)",
"GB-vctk" = "Vctk (Midlands Female)",
"US-amy" = "Amy (Northern American Female)",
"US-danny" = "Danny (British American Male)",
"US-joe" = "Joe (Hawaiian Male)",
"US-kathleen" = "Kathleen (Elder Eastern American Female)",
"US-kusal" = "Kusal (Asian American Male)",
"US-libritts_r" = "Libritts (Michigan Female)"
)))
GLOBAL_PROTECT(tts_voices_names)

GLOBAL_LIST_INIT(tts_voices, sortList(list(
"GB-alba",
"GB-aru",
"GB-jenny_dioco",
"GB-northern_english_male",
"GB-southern_english_female",
"GB-vctk",
"US-amy",
"US-danny",
"US-joe",
"US-kathleen",
"US-kusal",
"US-libritts_r"
)))
GLOBAL_PROTECT(tts_voices)

GLOBAL_LIST_EMPTY(species_list)

/proc/age2agedescription(age)
Expand Down
71 changes: 71 additions & 0 deletions code/__HELPERS/piper.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#define UNTIL(X) while(!(X)) stoplag() // Used to be in unsorted.dm, but that is later than this file

/**
* @param {String} message - The message to feed the model i.e. "Hello, world!"
*
* @param {String} model - The model i.e. "GB-alba"
*
* @param {number} pitch - Pitch multiplier, range (0.5-2.0)
*
* @returns {sound/} or FALSE
*/
/proc/piper_tts(message, model, pitch, filters)
if(!CONFIG_GET(flag/tts_enable))
return FALSE

var/player_count = living_player_count()
if(!SSticker.tts_capped && player_count >= CONFIG_GET(number/tts_cap_shutoff))
SSticker.tts_capped = TRUE
return FALSE

if(SSticker.tts_capped)
if(player_count < CONFIG_GET(number/tts_uncap_reboot))
SSticker.tts_capped = FALSE
else
return FALSE

if(!SSticker.tts_alive)
return FALSE

var/san_message = sanitize_tts_input(message)
var/san_model = sanitize_tts_input(model)
if(!pitch || !isnum(pitch))
pitch = 1

var/string_filters = ""
if(filters && islist(filters))
string_filters = jointext(assoc_to_keys(filters), "-")
var/file_name = "tmp/tts/[md5("[san_message][san_model][pitch][string_filters]")].wav"

if(fexists(file_name))
return sound(file_name)

// TGS updates can clear out the tmp folder, so we need to create the folder again if it no longer exists.
if(!fexists("tmp/tts/init.txt"))
rustg_file_write("rustg HTTP requests can't write to folders that don't exist, so we need to make it exist.", "tmp/tts/init.txt")

if(!filters || !islist(filters))
filters = list()

var/list/headers = list()
headers["Content-Type"] = "application/json"
headers["Authorization"] = CONFIG_GET(string/tts_http_token)
var/datum/http_request/request = new()
request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts?model=[url_encode(san_model)]&pitch=[url_encode(pitch)]", json_encode(list("message" = san_message, "filters" = filters)), headers, file_name)

request.begin_async()

UNTIL(request.is_complete())

var/datum/http_response/response = request.into_response()
if(response.errored || response.status_code > 299)
fdel(file_name)
return FALSE

if(response.body == "bad auth" || response.body == "missing args")
fdel(file_name)
return FALSE

var/sound/tts_sound = sound(file_name)

return tts_sound
6 changes: 6 additions & 0 deletions code/__HELPERS/text.dm
Original file line number Diff line number Diff line change
Expand Up @@ -849,3 +849,9 @@ GLOBAL_LIST_INIT(binary, list("0","1"))
/proc/sanitize_css_class_name(name)
var/static/regex/regex = new(@"[^a-zA-Z0-9]","g")
return replacetext(name, regex, "")

/// Removes all unsafe cmd/shell text
/proc/sanitize_tts_input(txt)
var/and_replaced = replacetext(txt, "&", "and") // Manually sanitize "&" into "and" so it doesn't get consumed by the void
var/static/regex/regex = new(@"[^a-zA-Z0-9,._+:@%/\- ]","g")
return replacetext(and_replaced, regex, "")
116 changes: 116 additions & 0 deletions code/__HELPERS/tts_filters.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// shamelessly copied from traits.dm
// filter accessor defines
#define ADD_FILTER(target, filter, source) \
do { \
var/list/_L; \
if (!target.tts_filters) { \
target.tts_filters = list(); \
_L = target.tts_filters; \
_L[filter] = list(source); \
} else { \
_L = target.tts_filters; \
if (_L[filter]) { \
_L[filter] |= list(source); \
} else { \
_L[filter] = list(source); \
} \
} \
} while (0)
#define REMOVE_FILTER(target, filter, sources) \
do { \
var/list/_L = target.tts_filters; \
var/list/_S; \
if (sources && !islist(sources)) { \
_S = list(sources); \
} else { \
_S = sources\
}; \
if (_L && _L[filter]) { \
for (var/_T in _L[filter]) { \
if ((!_S && (_T != ROUNDSTART_FILTER)) || (_T in _S)) { \
_L[filter] -= _T \
} \
};\
if (!length(_L[filter])) { \
_L -= filter; \
}; \
if (!length(_L)) { \
target.tts_filters = null \
}; \
} \
} while (0)
#define REMOVE_FILTER_NOT_FROM(target, filter, sources) \
do { \
var/list/_filters_list = target.tts_filters; \
var/list/_sources_list; \
if (sources && !islist(sources)) { \
_sources_list = list(sources); \
} else { \
_sources_list = sources\
}; \
if (_filters_list && _filters_list[filter]) { \
for (var/_filter_source in _filters_list[filter]) { \
if (!(_filter_source in _sources_list)) { \
_filters_list[filter] -= _filter_source \
} \
};\
if (!length(_filters_list[filter])) { \
_filters_list -= filter; \
}; \
if (!length(_filters_list)) { \
target.tts_filters = null \
}; \
} \
} while (0)
#define REMOVE_FILTERS_NOT_IN(target, sources) \
do { \
var/list/_L = target.tts_filters; \
var/list/_S = sources; \
if (_L) { \
for (var/_T in _L) { \
_L[_T] &= _S;\
if (!length(_L[_T])) { \
_L -= _T; \
}; \
};\
if (!length(_L)) { \
target.tts_filters = null\
};\
}\
} while (0)
#define REMOVE_FILTERS_IN(target, sources) \
do { \
var/list/_L = target.tts_filters; \
var/list/_S = sources; \
if (sources && !islist(sources)) { \
_S = list(sources); \
} else { \
_S = sources\
}; \
if (_L) { \
for (var/_T in _L) { \
_L[_T] -= _S;\
if (!length(_L[_T])) { \
_L -= _T; \
}; \
};\
if (!length(_L)) { \
target.tts_filters = null\
};\
}\
} while (0)
#define HAS_FILTER(target, filter) (target.tts_filters ? (target.tts_filters[filter] ? TRUE : FALSE) : FALSE)
#define HAS_FILTER_FROM(target, filter, source) (target.tts_filters ? (target.tts_filters[filter] ? (source in target.tts_filters[filter]) : FALSE) : FALSE)

// common filter sources
#define ROUNDSTART_FILTER "roundstart" // quirks, mob types, cannot be removed
#define RADIO_PROCESSING_FILTER "radio_processing"

// tts filters
#define TTS_FILTER_LIZARD "lizard"
#define TTS_FILTER_ALIEN "alien"
#define TTS_FILTER_ETHEREAL "ethereal"
#define TTS_FILTER_ROBOTIC "robotic"
#define TTS_FILTER_MASKED "masked"
#define TTS_FILTER_ROBOCOP "robocop"
#define TTS_FILTER_RADIO "radio"
2 changes: 0 additions & 2 deletions code/__HELPERS/unsorted.dm
Original file line number Diff line number Diff line change
Expand Up @@ -1257,8 +1257,6 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new)
return FALSE
return TRUE

#define UNTIL(X) while(!(X)) stoplag()

/proc/pass(...)
return

Expand Down
16 changes: 16 additions & 0 deletions code/controllers/configuration/entries/game_options.dm
Original file line number Diff line number Diff line change
Expand Up @@ -444,3 +444,19 @@

/datum/config_entry/number/max_shuttle_size
config_entry_value = 250

/datum/config_entry/string/tts_http_url
protection = CONFIG_ENTRY_LOCKED

/datum/config_entry/string/tts_http_token
protection = CONFIG_ENTRY_LOCKED|CONFIG_ENTRY_HIDDEN

/datum/config_entry/flag/tts_enable

/datum/config_entry/number/tts_cap_shutoff
config_entry_value = 75
protection = CONFIG_ENTRY_LOCKED

/datum/config_entry/number/tts_uncap_reboot
config_entry_value = 60
protection = CONFIG_ENTRY_LOCKED
23 changes: 23 additions & 0 deletions code/controllers/subsystem/ticker.dm
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ SUBSYSTEM_DEF(ticker)

var/music_available = 0

var/pinging_tts = FALSE
var/tts_alive = FALSE
var/tts_capped = FALSE

/datum/controller/subsystem/ticker/Initialize(timeofday)
load_mode()

Expand Down Expand Up @@ -154,6 +158,9 @@ SUBSYSTEM_DEF(ticker)
for(var/obj/machinery/cryopod/pod as anything in GLOB.cryopods)
pod.PowerOff()

if(CONFIG_GET(flag/tts_enable) && !pinging_tts)
INVOKE_ASYNC(src, PROC_REF(ping_tts))

switch(current_state)
if(GAME_STATE_STARTUP)
if(Master.initializations_finished_with_no_players_logged_in)
Expand Down Expand Up @@ -768,3 +775,19 @@ SUBSYSTEM_DEF(ticker)

/datum/controller/subsystem/ticker/Shutdown()
gather_newscaster() //called here so we ensure the log is created even upon admin reboot

/// Ping TTS API - If we don't get a response, shut down TTS
/datum/controller/subsystem/ticker/proc/ping_tts()
pinging_tts = TRUE

var/datum/http_request/request = new()
request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/ping")
request.begin_async()
UNTIL(request.is_complete())
var/datum/http_response/response = request.into_response()
if(response.errored || response.status_code > 299)
tts_alive = FALSE
else
tts_alive = TRUE

pinging_tts = FALSE
5 changes: 4 additions & 1 deletion code/game/atoms_movable.dm
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
///Highest-intensity light affecting us, which determines our visibility.
var/affecting_dynamic_lumi = 0

var/tts_voice
var/tts_pitch = 1
var/list/tts_filters

/atom/movable/Initialize(mapload, ...)
. = ..()
Expand Down Expand Up @@ -95,7 +98,7 @@
CanAtmosPass = ATMOS_PASS_YES
air_update_turf(TRUE)
loc.handle_atom_del(src)

if(opacity)
RemoveElement(/datum/element/light_blocking)

Expand Down
8 changes: 5 additions & 3 deletions code/game/machinery/_machinery.dm
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ Class Procs:
. = ..()
GLOB.machines += src

ADD_FILTER(src, TTS_FILTER_ROBOTIC, ROUNDSTART_FILTER)

if(ispath(circuit, /obj/item/circuitboard))
circuit = new circuit
circuit.apply_default_parts(src)
Expand Down Expand Up @@ -387,7 +389,7 @@ Class Procs:
if((interaction_flags_machine & INTERACT_MACHINE_WIRES_IF_OPEN) && panel_open && (attempt_wire_interaction(user) == WIRE_INTERACTION_BLOCK))
return TRUE
if((user.mind?.has_martialart(MARTIALART_BUSTERSTYLE)) && (user.a_intent == INTENT_GRAB)) //buster arm shit since it can throw vendors
return
return
return ..()

/obj/machinery/tool_act(mob/living/user, obj/item/tool, tool_type, is_right_clicking)
Expand Down Expand Up @@ -500,7 +502,7 @@ Class Procs:
/obj/proc/default_unfasten_wrench(mob/user, obj/item/wrench, time = 20) //try to unwrench an object in a WONDERFUL DYNAMIC WAY
if((flags_1 & NODECONSTRUCT_1) || wrench.tool_behaviour != TOOL_WRENCH)
return CANT_UNFASTEN

var/turf/ground = get_turf(src)
if(!anchored && ground.is_blocked_turf(exclude_mobs = TRUE, source_atom = src))
to_chat(user, span_notice("You fail to secure [src]."))
Expand Down Expand Up @@ -547,7 +549,7 @@ Class Procs:
for(var/obj/item/B in W.contents)
if(istype(B, P) && istype(A, P))
//won't replace beakers if they have reagents in them to prevent funny explosions
if(istype(B,/obj/item/reagent_containers) && length(B.reagents?.reagent_list))
if(istype(B,/obj/item/reagent_containers) && length(B.reagents?.reagent_list))
continue
// If it's a corrupt or rigged cell, attempting to send it through Bluespace could have unforeseen consequences.
if(istype(B, /obj/item/stock_parts/cell) && W.works_from_distance)
Expand Down
15 changes: 15 additions & 0 deletions code/game/machinery/telecomms/broadcasting.dm
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,28 @@
if(M.client && (M.client.prefs.chat_toggles & CHAT_GHOSTRADIO))
receive |= M

// TTS generation
var/model = pick(GLOB.tts_voices)
if(GLOB.tts_voices.Find(virt.virt_tts_voice)) // Sanitize with an immutable list
model = virt.virt_tts_voice

var/pitch = rand(0.8, 1.2)
if(virt.virt_tts_pitch)
pitch = virt.virt_tts_pitch

var/tts_sound = piper_tts(html_decode(message), model, pitch, virt.virt_tts_filters)

// Render the message and have everybody hear it.
// Always call this on the virtualspeaker to avoid issues.
var/spans = data["spans"]
var/list/message_mods = data["mods"]
var/rendered = virt.compose_message(virt, language, message, frequency, spans)
for(var/atom/movable/hearer in receive)
hearer.Hear(rendered, virt, language, message, frequency, spans, message_mods)
if(ismob(hearer))
var/mob/hearing_mob = hearer
if(tts_sound && hearing_mob.client?.prefs?.read_preference(/datum/preference/toggle/tts_hear_radio) && hearing_mob.has_language(language))
hearing_mob.playsound_local(vol = spans[SPAN_COMMAND] ? 45 : 30, S = tts_sound) // TTS play

// This following recording is intended for research and feedback in the use of department radio channels
if(length(receive))
Expand Down
Loading