[MIRROR] Adds TTS to the game. Players can select their own voices in preferences. [MDB IGNORE] (#21232)

* Adds TTS to the game. Players can select their own voices in preferences.

* [SEMI-MODULAR] [MIRROR FIX] Fixes the TTS PR. (#21267)

Fixes the TTS PR.

---------

Co-authored-by: Watermelon914 <37270891+Watermelon914@users.noreply.github.com>
Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
This commit is contained in:
SkyratBot
2023-05-19 01:47:19 +01:00
committed by GitHub
parent 406275dc5a
commit 6c9be73f51
40 changed files with 795 additions and 32 deletions
@@ -107,7 +107,9 @@
/// From mob/living/treat_message(): (list/message_args)
#define COMSIG_LIVING_TREAT_MESSAGE "living_treat_message"
/// The index of message_args that corresponds to the actual message
#define TREAT_MESSAGE_MESSAGE 1
#define TREAT_MESSAGE_ARG 1
#define TREAT_TTS_MESSAGE_ARG 2
#define TREAT_TTS_FILTER_ARG 3
///From obj/item/toy/crayon/spraycan
#define COMSIG_LIVING_MOB_PAINTED "living_mob_painted"
-6
View File
@@ -4,9 +4,3 @@
#define SPEECH_CONTROLLER_QUEUE_WHISPER_VERB "whisper_verb"
#define SPEECH_CONTROLLER_QUEUE_EMOTE_VERB "emote_verb"
#define MOB_INDEX 1
#define MESSAGE_INDEX 2
#define CATEGORY_INDEX 3
+2
View File
@@ -161,6 +161,7 @@
#define INIT_ORDER_OUTPUTS 35
#define INIT_ORDER_RESTAURANT 34
#define INIT_ORDER_POLLUTION 32 //SKYRAT EDIT ADDITION - //Needs to be above atoms
#define INIT_ORDER_TTS 33
#define INIT_ORDER_ATOMS 30
#define INIT_ORDER_ARMAMENTS 27 // SKYRAT EDIT ADDITION - Needs to be between atoms and default so it runs before gun companies
#define INIT_ORDER_LANGUAGE 25
@@ -225,6 +226,7 @@
#define FIRE_PRIORITY_STATPANEL 390
#define FIRE_PRIORITY_CHAT 400
#define FIRE_PRIORITY_RUNECHAT 410
#define FIRE_PRIORITY_TTS 425
#define FIRE_PRIORITY_MOUSE_ENTERED 450
#define FIRE_PRIORITY_OVERLAYS 500
#define FIRE_PRIORITY_EXPLOSIONS 666
+2 -2
View File
@@ -20,7 +20,7 @@
return !length(L)
//insert and place at its position a new node in the heap
/datum/heap/proc/insert(atom/A)
/datum/heap/proc/insert(A)
L.Add(A)
swim(length(L))
@@ -70,7 +70,7 @@
return index * 2
//Replaces a given node so it verify the heap condition
/datum/heap/proc/resort(atom/A)
/datum/heap/proc/resort(A)
var/index = L.Find(A)
swim(index)
+4
View File
@@ -0,0 +1,4 @@
/proc/tts_speech_filter(text)
// Only allow alphanumeric characters and whitespace
var/static/regex/bad_chars_regex = regex("\[^a-zA-Z0-9 ,?.!'&-]", "g")
return bad_chars_regex.Replace(text, " ")
@@ -418,4 +418,14 @@
/datum/config_entry/flag/disallow_circuit_sounds
/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/number/tts_max_concurrent_requests
default = 4
min_val = 1
/datum/config_entry/flag/give_tutorials_without_db
+2 -1
View File
@@ -36,7 +36,8 @@ SUBSYSTEM_DEF(communications)
minor_announce(html_decode(input),"[user.name] Announces:", players = players)
COOLDOWN_START(src, silicon_message_cooldown, COMMUNICATION_COOLDOWN_AI)
else
priority_announce(html_decode(user.treat_message(input)), null, ANNOUNCER_CAPTAIN, "[syndicate? "Syndicate " : ""]Captain", has_important_message = TRUE, players = players)//SKYRAT EDIT CHANGE
var/list/message_data = user.treat_message(input)
priority_announce(html_decode(message_data["message"]), null, ANNOUNCER_CAPTAIN, "[syndicate? "Syndicate " : ""]Captain", has_important_message = TRUE, players = players)
COOLDOWN_START(src, nonsilicon_message_cooldown, COMMUNICATION_COOLDOWN)
user.log_talk(input, LOG_SAY, tag="priority announcement")
message_admins("[ADMIN_LOOKUPFLW(user)] has made a priority announcement.")
+228
View File
@@ -0,0 +1,228 @@
#define TARGET_INDEX 1
#define IDENTIFIER_INDEX 2
#define START_TIME_INDEX 3
#define REQUEST_INDEX 4
#define MESSAGE_INDEX 5
SUBSYSTEM_DEF(tts)
name = "Text To Speech"
wait = 0.05 SECONDS
priority = FIRE_PRIORITY_TTS
init_order = INIT_ORDER_TTS
runlevels = RUNLEVEL_LOBBY | RUNLEVEL_SETUP | RUNLEVEL_GAME | RUNLEVEL_POSTGAME
/// Queued HTTP requests that have yet to be sent. TTS requests are handled as lists rather than datums.
/// It could be worth refactoring TTS messages to be datums instead to reduce complexity.
var/datum/heap/queued_tts_messages
/// HTTP requests currently in progress but not being processed yet
var/list/in_process_tts_messages = list()
/// HTTP requests that are being processed to see if they've been finished
var/list/current_processing_tts_messages = list()
/// A list of available speakers, which are string identifiers of the TTS voices that can be used to generate TTS messages.
var/list/available_speakers = list()
/// A list of current tts messages being processed, mapped by their sha1 identifier.
/// Used to prevent double processing of the same message, voice and filter, since we can just
/// cache extra requests to the current tts message being processed at once and play them upon request completion.
var/list/cached_voices = list()
/// Whether TTS is enabled or not
var/tts_enabled = FALSE
/// TTS messages won't play if requests took longer than this duration of time.
var/message_timeout = 7 SECONDS
/// Messages can be timed out earlier if the algorithm thinks that
/// it's going to take too long for their message to be processed.
/// This'll determine the minimum extent of how late it is allowed to begin timing messages out
var/message_timeout_early_minimum = 5 SECONDS
/// The max concurrent http requests that can be made at one time. Used to prevent 1 server from overloading the tts server
var/max_concurrent_requests = 4
/// Used to calculate the average time it takes for a tts message to be received from the http server
/// For tts messages which time out, it won't keep tracking the tts message and will just assume that the message took
/// 7 seconds (or whatever the value of message_timeout is) to receive back a response.
var/average_tts_messages_time = 0
/datum/controller/subsystem/tts/vv_edit_var(var_name, var_value)
// tts being enabled depends on whether it actually exists
if(NAMEOF(src, tts_enabled) == var_name)
return FALSE
return ..()
/datum/controller/subsystem/tts/stat_entry(msg)
msg = "Active:[length(in_process_tts_messages)]|Standby:[length(queued_tts_messages.L)]|Avg:[average_tts_messages_time]"
return ..()
/proc/cmp_word_length_asc(list/a, list/b)
return length(b[MESSAGE_INDEX]) - length(a[MESSAGE_INDEX])
/datum/controller/subsystem/tts/Initialize()
if(!CONFIG_GET(string/tts_http_url))
return SS_INIT_NO_NEED
queued_tts_messages = new /datum/heap(GLOBAL_PROC_REF(cmp_word_length_asc))
var/datum/http_request/request = new()
var/list/headers = list()
headers["Authorization"] = CONFIG_GET(string/tts_http_token)
request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts-voices", "", headers)
request.begin_async()
UNTIL(request.is_complete())
var/datum/http_response/response = request.into_response()
if(response.errored || response.status_code != 200)
stack_trace(response.error)
return SS_INIT_FAILURE
max_concurrent_requests = CONFIG_GET(number/tts_max_concurrent_requests)
available_speakers = json_decode(response.body)
tts_enabled = TRUE
rustg_file_write(json_encode(available_speakers), "data/cached_tts_voices.json")
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")
return SS_INIT_SUCCESS
/datum/controller/subsystem/tts/proc/play_tts(target, sound/audio, datum/language/language, local, range = 7)
if(local)
SEND_SOUND(target, audio)
return
var/turf/turf_source = get_turf(target)
if(!turf_source)
return
var/channel = SSsounds.random_available_channel()
var/listeners = get_hearers_in_view(range, turf_source)
for(var/mob/listening_mob in listeners | SSmobs.dead_players_by_zlevel[turf_source.z])//observers always hear through walls
var/datum/language_holder/holder = listening_mob.get_language_holder()
if(!listening_mob.client?.prefs.read_preference(/datum/preference/toggle/sound_tts))
continue
if(get_dist(listening_mob, turf_source) <= range && holder.has_language(language, spoken = FALSE))
listening_mob.playsound_local(
turf_source,
vol = (listening_mob == target)? 60 : 85,
falloff_exponent = SOUND_FALLOFF_EXPONENT,
channel = channel,
pressure_affected = TRUE,
sound_to_use = audio,
max_distance = SOUND_RANGE,
falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE,
distance_multiplier = 1,
use_reverb = TRUE
)
/datum/controller/subsystem/tts/proc/handle_request(list/entry)
var/timeout_time = entry[START_TIME_INDEX] + message_timeout
if(timeout_time < world.time)
cached_voices -= entry[IDENTIFIER_INDEX]
return
var/datum/http_request/request = entry[REQUEST_INDEX]
request.begin_async()
in_process_tts_messages += list(entry)
// Need to wait for all HTTP requests to complete here because of a rustg crash bug that causes crashes when dd restarts whilst HTTP requests are ongoing.
/datum/controller/subsystem/tts/Shutdown()
tts_enabled = FALSE
for(var/list/data in in_process_tts_messages)
var/datum/http_request/request = data[REQUEST_INDEX]
UNTIL(request.is_complete())
/datum/controller/subsystem/tts/fire(resumed)
if(!tts_enabled)
flags |= SS_NO_FIRE
return
if(!resumed)
while(length(in_process_tts_messages) < max_concurrent_requests && length(queued_tts_messages.L) > 0)
var/list/entry = queued_tts_messages.pop()
handle_request(entry)
current_processing_tts_messages = in_process_tts_messages.Copy()
// For speed
var/list/processing_messages = current_processing_tts_messages
while(processing_messages.len)
var/current_message = processing_messages[processing_messages.len]
processing_messages.len--
var/datum/http_request/request = current_message[REQUEST_INDEX]
if(!request.is_complete())
continue
var/datum/http_response/response = request.into_response()
in_process_tts_messages -= list(current_message)
average_tts_messages_time = MC_AVERAGE(average_tts_messages_time, world.time - current_message[START_TIME_INDEX])
// If it took too long to process, don't bother playing it
var/timeout_time = current_message[START_TIME_INDEX] + message_timeout
var/identifier = current_message[IDENTIFIER_INDEX]
cached_voices -= identifier
if(response.errored || timeout_time < world.time)
continue
var/sound/new_sound = new("tmp/tts/[identifier].ogg")
for(var/target in current_message[TARGET_INDEX])
play_tts(target["target"], new_sound, target["language"], target["local"], target["range"])
if(MC_TICK_CHECK)
return
#define ADD_TARGET_TO_STRUCT(tts_struct, target, language, local, range) ##tts_struct[TARGET_INDEX] += list(list("target" = ##target, "language" = ##language, "local" = ##local, "range" = ##range))
/datum/controller/subsystem/tts/proc/queue_tts_message(target, message, datum/language/language, speaker, filter, local = FALSE, message_range = 7)
if(!tts_enabled)
return
var/static/regex/contains_alphanumeric = regex("\[a-zA-Z0-9]")
// If there is no alphanumeric char, the output will usually be static, so
// don't bother sending
if(contains_alphanumeric.Find(message) == 0)
return
var/shell_scrubbed_input = tts_speech_filter(message)
shell_scrubbed_input = copytext(shell_scrubbed_input, 1, 300)
var/identifier = sha1(speaker + filter + shell_scrubbed_input)
var/cached_voice = cached_voices[identifier]
if(islist(cached_voice))
ADD_TARGET_TO_STRUCT(cached_voice, target, language, local, message_range)
return
else if(fexists("tmp/tts/[identifier].ogg"))
var/sound/new_sound = new("tmp/tts/[identifier].ogg")
play_tts(target, new_sound, language, local, message_range)
return
if(!(speaker in available_speakers))
return
var/list/headers = list()
headers["Content-Type"] = "application/json"
headers["Authorization"] = CONFIG_GET(string/tts_http_token)
var/datum/http_request/request = new()
var/file_name = "tmp/tts/[identifier].ogg"
request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts?voice=[speaker]&identifier=[identifier]&filter=[url_encode(filter)]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name)
// This'll probably be better off datumized in the future, but it's not necessary to do right now
var/list/data = list(
// TARGET_INDEX = 1
list(),
// IDENTIFIER_INDEX = 2
identifier,
// START_TIME_INDEX = 3
world.time,
// REQUEST_INDEX = 4
request,
// MESSAGE_INDEX = 5
shell_scrubbed_input,
)
ADD_TARGET_TO_STRUCT(data, target, language, local, message_range)
cached_voices[identifier] = data
if(length(in_process_tts_messages) < max_concurrent_requests)
request.begin_async()
in_process_tts_messages += list(data)
else
queued_tts_messages.insert(list(data))
#undef ADD_TARGET_TO_STRUCT
#undef TARGET_INDEX
#undef IDENTIFIER_INDEX
#undef START_TIME_INDEX
#undef REQUEST_INDEX
#undef MESSAGE_INDEX
+3 -1
View File
@@ -227,7 +227,9 @@
SIGNAL_HANDLER
if(check_signables_state() == SIGN_ONE_HAND)
message_args[TREAT_MESSAGE_MESSAGE] = stars(message_args[TREAT_MESSAGE_MESSAGE])
message_args[TREAT_MESSAGE_ARG] = stars(message_args[TREAT_MESSAGE_ARG])
message_args[TREAT_TTS_MESSAGE_ARG] = ""
/// Signal proc for [COMSIG_MOVABLE_SAY_QUOTE]
/// Removes exclamation/question marks.
+2 -2
View File
@@ -694,8 +694,8 @@
if(prob(max(5,(nearby_people*12.5*moodmod)))) //Minimum 1/20 chance of stutter
// Add a short stutter, THEN treat our word
quirker.adjust_stutter(0.5 SECONDS)
new_message += quirker.treat_message(word, capitalize_message = FALSE)
var/list/message_data = quirker.treat_message(word, capitalize_message = FALSE)
new_message += message_data["message"]
else
new_message += word
@@ -3,6 +3,9 @@
alert_type = null
remove_on_fullheal = TRUE
var/make_tts_message_original = FALSE
var/tts_filter = ""
/datum/status_effect/speech/on_creation(mob/living/new_owner, duration = 10 SECONDS)
src.duration = duration
return ..()
@@ -23,10 +26,15 @@
/datum/status_effect/speech/proc/handle_message(datum/source, list/message_args)
SIGNAL_HANDLER
var/phrase = html_decode(message_args[TREAT_MESSAGE_MESSAGE])
var/phrase = html_decode(message_args[TREAT_MESSAGE_ARG])
if(!length(phrase))
return
if(length(tts_filter) > 0)
message_args[TREAT_TTS_FILTER_ARG] += tts_filter
if(make_tts_message_original)
message_args[TREAT_TTS_MESSAGE_ARG] = message_args[TREAT_MESSAGE_ARG]
var/final_phrase = ""
var/original_char = ""
@@ -35,7 +43,7 @@
final_phrase += apply_speech(original_char, original_char)
message_args[TREAT_MESSAGE_MESSAGE] = sanitize(final_phrase)
message_args[TREAT_MESSAGE_ARG] = sanitize(final_phrase)
/**
* Applies the speech effects on the past character, changing
@@ -54,6 +62,9 @@
/// Regex of characters we won't apply a stutter to
var/static/regex/no_stutter
make_tts_message_original = TRUE
tts_filter = "tremolo=f=10:d=0.8,rubberband=tempo=0.5"
/datum/status_effect/speech/stutter/on_creation(mob/living/new_owner, ...)
. = ..()
if(!.)
@@ -83,7 +94,7 @@
/datum/status_effect/speech/stutter/derpspeech/handle_message(datum/source, list/message_args)
var/message = html_decode(message_args[TREAT_MESSAGE_MESSAGE])
var/message = html_decode(message_args[TREAT_MESSAGE_ARG])
message = replacetext(message, " am ", " ")
message = replacetext(message, " is ", " ")
@@ -100,7 +111,7 @@
message = uppertext(message)
message += "[apply_speech(exclamation, exclamation)]"
message_args[1] = message
message_args[TREAT_MESSAGE_ARG] = message
var/mob/living/living_source = source
if(!isliving(source) || living_source.has_status_effect(/datum/status_effect/speech/stutter))
@@ -229,6 +240,8 @@
doubletext_prob = 0
text_modification_file = "slurring_cult_text.json"
tts_filter = "rubberband=pitch=0.5,vibrato=5"
/datum/status_effect/speech/slurring/heretic
id = "heretic_slurring"
common_prob = 50
+7
View File
@@ -96,6 +96,13 @@
/// Whether a user will face atoms on entering them with a mouse. Despite being a mob variable, it is here for performances //SKYRAT EDIT ADDITION
var/face_mouse = FALSE //SKYRAT EDIT ADDITION
/// The voice that this movable makes when speaking
var/voice
/// The filter to apply to the voice when processing the TTS audio message.
var/voice_filter = ""
/// Value used to increment ex_act() if reactionary_explosions is on
/// How much we as a source block explosions by
/// Will not automatically apply to the turf below you, you need to apply /datum/element/block_explosives in conjunction with this
@@ -778,7 +778,8 @@ GLOBAL_VAR_INIT(cops_arrived, FALSE)
return
if(user.try_speak(input))
//Adds slurs and so on. Someone should make this use languages too.
input = user.treat_message(input)
var/list/input_data = user.treat_message(input)
input = input_data["message"]
else
//No cheating, mime/random mute guy!
input = "..."
+18 -1
View File
@@ -80,12 +80,29 @@ GLOBAL_LIST_INIT(freqtospan, list(
/atom/movable/proc/can_speak(allow_mimes = FALSE)
return TRUE
/atom/movable/proc/send_speech(message, range = 7, obj/source = src, bubble_type, list/spans, datum/language/message_language, list/message_mods = list(), forced = FALSE)
/atom/movable/proc/send_speech(message, range = 7, obj/source = src, bubble_type, list/spans, datum/language/message_language, list/message_mods = list(), forced = FALSE, tts_message, list/tts_filter)
var/found_client = FALSE
for(var/atom/movable/hearing_movable as anything in get_hearers_in_view(range, source))
if(!hearing_movable)//theoretically this should use as anything because it shouldnt be able to get nulls but there are reports that it does.
stack_trace("somehow theres a null returned from get_hearers_in_view() in send_speech!")
continue
hearing_movable.Hear(null, src, message_language, message, null, spans, message_mods, range)
if(!found_client && length(hearing_movable.client_mobs_in_contents))
found_client = TRUE
var/tts_message_to_use = tts_message
if(!tts_message_to_use)
tts_message_to_use = message
var/list/filter = list()
if(length(voice_filter) > 0)
filter += voice_filter
if(length(tts_filter) > 0)
filter += tts_filter.Join(",")
if(voice && found_client)
INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice, filter.Join(","), message_range = range)
/atom/movable/proc/compose_message(atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, list/message_mods = list(), face_name = FALSE)
//This proc uses text() because it is faster than appending strings. Thanks BYOND.
@@ -605,6 +605,8 @@
new_profile.worn_icon_vox_list[slot] = clothing_item.worn_icon_vox
new_profile.supports_variations_flags_list[slot] = clothing_item.supports_variations_flags
// SKYRAT EDIT END
new_profile.voice = target.voice
new_profile.voice_filter = target.voice_filter
return new_profile
@@ -799,6 +801,7 @@
user.dna.body_markings = chosen_dna.body_markings.Copy()
user.grad_style = LAZYLISTDUPLICATE(chosen_profile.grad_style)
user.grad_color = LAZYLISTDUPLICATE(chosen_profile.grad_color)
user.physique = chosen_profile.physique
qdel(user.selected_scream)
qdel(user.selected_laugh)
@@ -821,6 +824,8 @@
break
// SKYRAT EDIT END
user.voice = chosen_profile.voice
user.voice_filter = chosen_profile.voice_filter
chosen_dna.transfer_identity(user, TRUE)
@@ -982,6 +987,7 @@
var/emissive_eyes
var/list/grad_style = list("None", "None")
var/list/grad_color = list(null, null)
var/physique
var/list/worn_icon_digi_list = list()
var/list/worn_icon_monkey_list = list()
@@ -994,6 +1000,12 @@
var/list/quirks = list()
/// SKYRAT EDIT END
/// The TTS voice of the profile source
var/voice
/// The TTS filter of the profile filter
var/voice_filter = ""
/datum/changeling_profile/Destroy()
qdel(dna)
LAZYCLEARLIST(stored_scars)
@@ -1034,6 +1046,7 @@
new_profile.emissive_eyes = emissive_eyes
new_profile.grad_style = LAZYLISTDUPLICATE(grad_style)
new_profile.grad_color = LAZYLISTDUPLICATE(grad_color)
new_profile.physique = physique
new_profile.worn_icon_digi_list = worn_icon_digi_list.Copy()
new_profile.worn_icon_monkey_list = worn_icon_monkey_list.Copy()
@@ -1046,6 +1059,9 @@
new_profile.quirks = quirks.Copy()
// SKYRAT EDIT END
new_profile.voice = voice
new_profile.voice_filter = voice_filter
/datum/antagonist/changeling/roundend_report()
var/list/parts = list()
@@ -0,0 +1,16 @@
/// Middleware to handle quirks
/datum/preference_middleware/tts
/// Cooldown on requesting a TTS preview.
COOLDOWN_DECLARE(tts_test_cooldown)
action_delegations = list(
"play_voice" = PROC_REF(play_voice),
)
/datum/preference_middleware/tts/proc/play_voice(list/params, mob/user)
if(!COOLDOWN_FINISHED(src, tts_test_cooldown))
return TRUE
var/speaker = preferences.read_preference(/datum/preference/choiced/voice)
COOLDOWN_START(src, tts_test_cooldown, 0.5 SECONDS)
INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Hello, this is my voice.", speaker = speaker, local = TRUE)
return TRUE
@@ -28,6 +28,11 @@
savefile_key = "sound_instruments"
savefile_identifier = PREFERENCE_PLAYER
/datum/preference/toggle/sound_tts
category = PREFERENCE_CATEGORY_GAME_PREFERENCES
savefile_key = "sound_tts"
savefile_identifier = PREFERENCE_PLAYER
/// Controls hearing dance machines
/datum/preference/toggle/sound_jukebox
category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+26
View File
@@ -0,0 +1,26 @@
/// TTS voice preference
/datum/preference/choiced/voice
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "tts_voice"
category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
/datum/preference/choiced/voice/is_accessible(datum/preferences/preferences)
if(!SStts.tts_enabled)
return FALSE
return ..()
/datum/preference/choiced/voice/init_possible_values()
if(SStts.tts_enabled)
return SStts.available_speakers
if(fexists("data/cached_tts_voices.json"))
var/list/text_data = rustg_file_read("data/cached_tts_voices.json")
var/list/cached_data = json_decode(text_data)
if(!cached_data)
return list("invalid")
return cached_data
return list("invalid")
/datum/preference/choiced/voice/apply_to_human(mob/living/carbon/human/target, value)
if(SStts.tts_enabled && !(value in SStts.available_speakers))
value = pick(SStts.available_speakers) // As a failsafe
target.voice = value
+2 -2
View File
@@ -19,7 +19,7 @@
else
return ..()
/mob/living/brain/treat_message(message, capitalize_message = TRUE)
/mob/living/brain/treat_message(message, tts_message, tts_filter, capitalize_message = TRUE)
if(capitalize_message)
message = capitalize(message)
return message
return list(message = message, tts_message = message, tts_filter = list())
@@ -3,6 +3,9 @@
dna?.species?.on_owner_login(src)
if(SStts.tts_enabled && !voice)
voice = pick(SStts.available_speakers)
if(!LAZYLEN(afk_thefts))
return
+41 -9
View File
@@ -184,7 +184,10 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
else
log_talk(message, LOG_SAY, forced_by = forced, custom_say_emote = message_mods[MODE_CUSTOM_SAY_EMOTE])
message = treat_message(message) // unfortunately we still need this
var/list/message_data = treat_message(message) // unfortunately we still need this
message = message_data["message"]
var/tts_message = message_data["tts_message"]
var/list/tts_filter = message_data["tts_filter"]
spans |= speech_span
@@ -205,10 +208,14 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
if(!HAS_TRAIT(src, TRAIT_SIGN_LANG)) // if using sign language skip sending the say signal
// Make sure the arglist is passed exactly - don't pass a copy of it. Say signal handlers will modify some of the parameters.
var/last_message = message
var/sigreturn = SEND_SIGNAL(src, COMSIG_MOB_SAY, args)
if(last_message != message)
tts_message = message
if(sigreturn & COMPONENT_UPPERCASE_SPEECH)
message = uppertext(message)
if(!message)
if(succumbed)
succumb()
@@ -243,8 +250,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
if(pressure < ONE_ATMOSPHERE*0.4) //Thin air, let's italicise the message
spans |= SPAN_ITALICS
send_speech(message, message_range, src, bubble_type, spans, language, message_mods)//roughly 58% of living/say()'s total cost
send_speech(message, message_range, src, bubble_type, spans, language, message_mods, tts_message = tts_message, tts_filter = tts_filter)//roughly 58% of living/say()'s total cost
if(succumbed)
succumb(TRUE)
to_chat(src, compose_message(src, language, message, , spans, message_mods))
@@ -326,7 +332,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
show_message(message, MSG_AUDIBLE, deaf_message, deaf_type, avoid_highlight)
return message
/mob/living/send_speech(message_raw, message_range = 6, obj/source = src, bubble_type = bubble_icon, list/spans, datum/language/message_language = null, list/message_mods = list(), forced = null)
/mob/living/send_speech(message_raw, message_range = 6, obj/source = src, bubble_type = bubble_icon, list/spans, datum/language/message_language = null, list/message_mods = list(), forced = null, tts_message, list/tts_filter)
var/whisper_range = 0
var/is_speaker_whispering = FALSE
if(message_mods[WHISPER_MODE]) //If we're whispering
@@ -365,10 +371,27 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
//speech bubble
var/list/speech_bubble_recipients = list()
var/found_client = FALSE
var/talk_icon_state = say_test(message_raw)
for(var/mob/M in listening)
if(M.client && (!M.client.prefs.read_preference(/datum/preference/toggle/enable_runechat) || (SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES))))
speech_bubble_recipients.Add(M.client)
if(M.client)
if(!M.client.prefs.read_preference(/datum/preference/toggle/enable_runechat) || (SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES)))
speech_bubble_recipients.Add(M.client)
found_client = TRUE
if(voice && found_client && !message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] && !HAS_TRAIT(src, TRAIT_SIGN_LANG))
var/tts_message_to_use = tts_message
if(!tts_message_to_use)
tts_message_to_use = message_raw
var/list/filter = list()
if(length(voice_filter) > 0)
filter += voice_filter
if(length(tts_filter) > 0)
filter += tts_filter.Join(",")
INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), src, html_decode(tts_message_to_use), message_language, voice, filter.Join(","), message_range = message_range)
var/image/say_popup = image('icons/mob/effects/talk.dmi', src, "[bubble_type][talk_icon_state]", FLY_LAYER)
SET_PLANE_EXPLICIT(say_popup, ABOVE_GAME_PLANE, src)
@@ -421,16 +444,25 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
* message - The message to treat.
* capitalize_message - Whether we run capitalize() on the message after we're done.
*/
/mob/living/proc/treat_message(message, capitalize_message = TRUE)
/mob/living/proc/treat_message(message, tts_message, tts_filter, capitalize_message = TRUE)
if(HAS_TRAIT(src, TRAIT_UNINTELLIGIBLE_SPEECH))
message = unintelligize(message)
SEND_SIGNAL(src, COMSIG_LIVING_TREAT_MESSAGE, args)
tts_filter = list()
var/list/data = list(message, tts_message, tts_filter)
SEND_SIGNAL(src, COMSIG_LIVING_TREAT_MESSAGE, data)
message = data[TREAT_MESSAGE_ARG]
tts_message = data[TREAT_TTS_MESSAGE_ARG]
tts_filter = data[TREAT_TTS_FILTER_ARG]
if(!tts_message)
tts_message = message
if(capitalize_message)
message = capitalize(message)
tts_message = capitalize(tts_message)
return message
return list(message = message, tts_message = tts_message, tts_filter = tts_filter)
/mob/living/proc/radio(message, list/message_mods = list(), list/spans, language)
//SKYRAT EDIT ADDITION BEGIN
+6
View File
@@ -1,6 +1,12 @@
/mob/living/silicon/Login()
if(mind)
mind?.remove_antags_for_borging()
// SKYRAT EDIT BEGIN - Let people set a TTS voice for their silicon characters and pAIs
if(SStts.tts_enabled)
var/voice_to_use = client?.prefs.read_preference(/datum/preference/choiced/voice)
if(voice_to_use)
voice = voice_to_use
// SKYRAT EDIT END
return ..()
@@ -13,6 +13,7 @@
flags_1 = PREVENT_CONTENTS_EXPLOSION_1
examine_cursor_icon = null
fire_stack_decay_rate = -0.55
voice_filter = "afftfilt=real='hypot(re,im)*sin(0)':imag='hypot(re,im)*cos(0)':win_size=512:overlap=1,rubberband=pitch=0.8"
var/datum/ai_laws/laws = null//Now... THEY ALL CAN ALL HAVE LAWS
var/last_lawchange_announce = 0
var/list/alarms_to_show = list()
@@ -51,8 +52,11 @@
var/obj/item/modular_computer/pda/silicon/modularInterface
/mob/living/silicon/Initialize(mapload)
. = ..()
if(SStts.tts_enabled)
voice = pick(SStts.available_speakers)
GLOB.silicon_mobs += src
faction += FACTION_SILICON
if(ispath(radio))
@@ -82,7 +82,7 @@
if(bonus_overlays)
. += bonus_overlays
/mob/living/simple_animal/robot_customer/send_speech(message, message_range, obj/source, bubble_type, list/spans, datum/language/message_language, list/message_mods, forced)
/mob/living/simple_animal/robot_customer/send_speech(message, message_range, obj/source, bubble_type, list/spans, datum/language/message_language, list/message_mods, forced, tts_message, list/tts_filter)
. = ..()
var/datum/customer_data/customer_info = ai_controller.blackboard[BB_CUSTOMER_CUSTOMERINFO]
playsound(src, customer_info.speech_sound, 30, extrarange = MEDIUM_RANGE_SOUND_EXTRARANGE, falloff_distance = 5)
@@ -901,13 +901,18 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
speak = list("Poly wanna cracker!", ":e Check the crystal, you chucklefucks!",":e Wire the solars, you lazy bums!",":e WHO TOOK THE DAMN MODSUITS?",":e OH GOD ITS ABOUT TO DELAMINATE CALL THE SHUTTLE")
gold_core_spawnable = NO_SPAWN
speak_chance = 3
voice_filter = "rubberband=pitch=1.5"
var/memory_saved = FALSE
var/rounds_survived = 0
var/longest_survival = 0
var/longest_deathstreak = 0
/mob/living/simple_animal/parrot/poly/Initialize(mapload)
ears = new /obj/item/radio/headset/headset_eng(src)
if(SStts.tts_enabled)
voice = pick(SStts.available_speakers)
available_channels = list(":e")
Read_Memory()
if(rounds_survived == longest_survival)
+1
View File
@@ -29,6 +29,7 @@
/mob/Login()
if(!client)
return FALSE
canon_client = client
add_to_player_list()
lastKnownIP = client.address
+7 -1
View File
@@ -64,6 +64,7 @@
payment_department = ACCOUNT_SRV
light_power = 0.7
light_range = MINIMUM_USEFUL_LIGHT_RANGE
voice_filter = "aderivative"
/// Is the machine active (No sales pitches if off)!
var/active = 1
@@ -185,7 +186,6 @@
/// used for narcing on underages
var/obj/item/radio/sec_radio
/**
* Initialize the vending machine
*
@@ -208,6 +208,12 @@
. = ..()
wires = new /datum/wires/vending(src)
if(SStts.tts_enabled)
var/static/vendor_voice_by_type = list()
if(!vendor_voice_by_type[type])
vendor_voice_by_type[type] = pick(SStts.available_speakers)
voice = vendor_voice_by_type[type]
if(build_inv) //non-constructable vending machine
build_inventories()
+9
View File
@@ -665,6 +665,15 @@ PR_ANNOUNCEMENTS_PER_ROUND 5
## Uncomment to block granting profiling privileges to users with R_DEBUG, for performance purposes
#FORBID_ADMIN_PROFILING
## Link to a HTTP server that's been set up on a server. Docker-compose file can be found in tools/tts
#TTS_HTTP_URL http://localhost:5002
## Token that can be used to prevent misuse of your TTS server that you've set up.
#TTS_HTTP_TOKEN coolio
## The maximum number of concurrent tts http requests that can be made by the server at once.
#TTS_MAX_CONCURRENT_REQUESTS 4
## Comment to disable sending a toast notification on the host server when initializations complete.
## Even if this is enabled, a notification will only be sent if there are no clients connected.
TOAST_NOTIFICATION_ON_INIT
+8
View File
@@ -86,6 +86,14 @@ window "mainwindow"
anchor2 = -1,-1
is-visible = false
saved-params = ""
elem "html_audio_player"
type = BROWSER
pos = 0,0
size = 200x200
anchor1 = none
anchor2 = none
is-visible = false
saved-params = ""
elem "tooltip"
type = BROWSER
pos = 0,0
+4
View File
@@ -486,6 +486,7 @@
#include "code\__HELPERS\text.dm"
#include "code\__HELPERS\time.dm"
#include "code\__HELPERS\traits.dm"
#include "code\__HELPERS\tts.dm"
#include "code\__HELPERS\turfs.dm"
#include "code\__HELPERS\type2type.dm"
#include "code\__HELPERS\type_processing.dm"
@@ -695,6 +696,7 @@
#include "code\controllers\subsystem\timer.dm"
#include "code\controllers\subsystem\title.dm"
#include "code\controllers\subsystem\traitor.dm"
#include "code\controllers\subsystem\tts.dm"
#include "code\controllers\subsystem\tutorials.dm"
#include "code\controllers\subsystem\verb_manager.dm"
#include "code\controllers\subsystem\vis_overlays.dm"
@@ -3148,6 +3150,7 @@
#include "code\modules\client\preferences\ui_style.dm"
#include "code\modules\client\preferences\underwear_color.dm"
#include "code\modules\client\preferences\uplink_location.dm"
#include "code\modules\client\preferences\voice.dm"
#include "code\modules\client\preferences\widescreen.dm"
#include "code\modules\client\preferences\window_flashing.dm"
#include "code\modules\client\preferences\middleware\_middleware.dm"
@@ -3159,6 +3162,7 @@
#include "code\modules\client\preferences\middleware\quirks.dm"
#include "code\modules\client\preferences\middleware\random.dm"
#include "code\modules\client\preferences\middleware\species.dm"
#include "code\modules\client\preferences\middleware\tts.dm"
#include "code\modules\client\preferences\migrations\body_type_migration.dm"
#include "code\modules\client\preferences\migrations\convert_to_json_savefile.dm"
#include "code\modules\client\preferences\migrations\legacy_sound_toggles_migration.dm"
@@ -0,0 +1,29 @@
import { FeatureChoiced, FeatureChoicedServerData, FeatureDropdownInput, FeatureValueProps } from '../base';
import { Stack, Button } from '../../../../../components';
const FeatureTTSDropdownInput = (
props: FeatureValueProps<string, string, FeatureChoicedServerData>
) => {
return (
<Stack>
<Stack.Item grow>
<FeatureDropdownInput {...props} />
</Stack.Item>
<Stack.Item>
<Button
onClick={() => {
props.act('play_voice');
}}
icon="play"
width="100%"
height="100%"
/>
</Stack.Item>
</Stack>
);
};
export const tts_voice: FeatureChoiced = {
name: 'Voice',
component: FeatureTTSDropdownInput,
};
@@ -34,6 +34,13 @@ export const sound_instruments: FeatureToggle = {
component: CheckboxInput,
};
export const sound_tts: FeatureToggle = {
name: 'Enable TTS',
category: 'SOUND',
description: 'When enabled, be able to hear text-to-speech sounds in game.',
component: CheckboxInput,
};
export const sound_jukebox: FeatureToggle = {
name: 'Enable jukebox music',
category: 'SOUND',
+29
View File
@@ -0,0 +1,29 @@
services:
tts-api:
container_name: tts-api
build: ./tts-api
ports:
- "5002:5002"
restart: always
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:5002/health-check"]
interval: 1s
timeout: 1s
retries: 3
start_period: 30s
links:
- "tts:tts-container"
tts:
container_name: tts
build: ./tts
restart: always
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:5003/health-check"]
interval: 1s
timeout: 1s
retries: 3
start_period: 30s
volumes:
- ./tts/persistent_data/tts_models:/root/tts_data
+11
View File
@@ -0,0 +1,11 @@
## Basic documentation
To run, simply do `docker compose up -d` in the tts and ffmpeg folders.
This will build the container if it isn't build already, but if it is, then it'll re-use the built image.
To build the container after making any changes to the non-persistent files, you can do `docker compose build`
### If you are testing on local
Once it's running, edit your config so that `TTS_HTTP_URL` is set to http://localhost:5002 and `TTS_HTTP_TOKEN` is set to `coolio`
### If you are deploying to prod
Edit your config so that `TTS_HTTP_URL` is a http request to your TTS server (whether that be localhost, an ip address or a domain) on port 5002 and `TTS_HTTP_TOKEN` is set to a random string value. You'll also need to modify the `tts-api.py` file and set the `authorization_token` variable to whatever you've set your `TTS_HTTP_TOKEN` to. This is to prevent unauthorized requests.
+40
View File
@@ -0,0 +1,40 @@
# Grab debian-bullseye-slim
FROM debian:bullseye-slim
# install required packages
RUN apt-get update && apt-get upgrade && apt-get install -y ffmpeg wget curl &&\
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install Anaconda
ENV CONDA_DIR /opt/conda
RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-py310_22.11.1-1-Linux-x86_64.sh -O ~/miniconda.sh && \
/bin/bash ~/miniconda.sh -b -p /opt/conda
# Put conda in path so we can use conda
ENV PATH=$CONDA_DIR/bin:$PATH
# Enable intel packages
RUN conda config --add channels intel
# Setup intel enhanced python environment named 'intel'.
RUN conda create -n intel intelpython3_full python=3.9 numba=0.55.1 && conda clean --index-cache --tarballs --packages
# Use the intel enhanced python environment from now on.
SHELL ["conda", "run", "-n", "intel", "/bin/bash", "-c"]
# Setup python requirements and install the TTS python module into the new intel anaconda environment.
RUN pip install Flask &&\
pip install waitress &&\
pip cache purge
COPY . /root
RUN mkdir /tts_files
WORKDIR /root
# fix: `libstdc++.so.6: version `GLIBCXX_3.4.29' not found`
# tl;dr is we need to have /opt/conda/lib in the library path for python, but *not things ran by python*
# the python script will look for TTS_LD_LIBRARY_PATH and use it for things it runs.
RUN conda env config vars set TTS_LD_LIBRARY_PATH=$LD_LIBRARY_PATH
RUN conda env config vars set LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_DIR/lib
ENTRYPOINT ["conda", "run", "--no-capture-output", "-n", "intel", "python", "tts-api.py"]
+54
View File
@@ -0,0 +1,54 @@
import os
import io
import gc
import subprocess
import requests
from flask import Flask, request, send_file, abort
app = Flask(__name__)
authorization_token = os.get_env("TTS_AUTHORIZATION_TOKEN", "coolio")
@app.route("/tts")
def text_to_speech():
if authorization_token != request.headers.get("Authorization", ""):
abort(401)
voice = request.args.get("voice", '')
text = request.json.get("text", '')
filter_complex = request.args.get("filter", '')
filter_complex = filter_complex.replace("\"", "")
response = requests.get(f"http://tts-container:5003/generate-tts", json={ 'text': text, 'voice': voice })
if response.status_code != 200:
abort(500)
ffmpeg_result = None
if filter_complex != "":
ffmpeg_result = subprocess.run(["ffmpeg", "-f", "wav", "-i", "pipe:0", "-filter_complex", filter_complex, "-c:a", "libvorbis", "-b:a", "64k", "-f", "ogg", "pipe:1"], input=response.content, capture_output = True)
else:
ffmpeg_result = subprocess.run(["ffmpeg", "-f", "wav", "-i", "pipe:0", "-c:a", "libvorbis", "-b:a", "64k", "-f", "ogg", "pipe:1"], input=response.content, capture_output = True)
print(f"ffmpeg result size: {len(ffmpeg_result.stdout)} stderr = \n{ffmpeg_result.stderr.decode()}")
return send_file(io.BytesIO(ffmpeg_result.stdout), as_attachment=True, download_name='identifier.ogg', mimetype="audio/ogg")
@app.route("/tts-voices")
def voices_list():
if authorization_token != request.headers.get("Authorization", ""):
abort(401)
response = requests.get(f"http://tts-container:5003/tts-voices")
return response.content
@app.route("/health-check")
def tts_health_check():
gc.collect()
return "OK", 200
if __name__ == "__main__":
if os.getenv('TTS_LD_LIBRARY_PATH', "") != "":
os.putenv('LD_LIBRARY_PATH', os.getenv('TTS_LD_LIBRARY_PATH'))
from waitress import serve
serve(app, host="0.0.0.0", port=5002, threads=2, backlog=8, connection_limit=24, channel_timeout=10)
+1
View File
@@ -0,0 +1 @@
persistent_data
+43
View File
@@ -0,0 +1,43 @@
# Grab debian-bullseye-slim
FROM debian:bullseye-slim
# install required packages
RUN apt-get update && apt-get upgrade && apt-get install -y wget curl espeak-ng &&\
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install Anaconda
ENV CONDA_DIR /opt/conda
RUN wget --quiet https://repo.anaconda.com/miniconda/Miniconda3-py310_22.11.1-1-Linux-x86_64.sh -O ~/miniconda.sh && \
/bin/bash ~/miniconda.sh -b -p /opt/conda
# Put conda in path so we can use conda
ENV PATH=$CONDA_DIR/bin:$PATH
# Enable intel packages
RUN conda config --add channels intel
# Setup intel enhanced python environment named 'intel'.
RUN conda create -n intel intelpython3_full python=3.9 numba=0.55.1 && conda clean --index-cache --tarballs --packages
# Use the intel enhanced python environment from now on.
SHELL ["conda", "run", "-n", "intel", "/bin/bash", "-c"]
# Setup python requirements and install the TTS python module into the new intel anaconda environment.
RUN pip install Flask &&\
pip install waitress &&\
pip install llvmlite --ignore-installed &&\
pip install torch torchaudio --extra-index-url https://download.pytorch.org/whl/cu118 &&\
pip install TTS &&\
pip cache purge
COPY . /root
RUN mkdir /tts_data
WORKDIR /root
# fix: `libstdc++.so.6: version `GLIBCXX_3.4.29' not found`
# tl;dr is we need to have /opt/conda/lib in the library path for python, but *not things ran by python*
# the python script will look for TTS_LD_LIBRARY_PATH and use it for things it runs.
RUN conda env config vars set TTS_LD_LIBRARY_PATH=$LD_LIBRARY_PATH
RUN conda env config vars set LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$CONDA_DIR/lib
ENTRYPOINT ["conda", "run", "--no-capture-output", "-n", "intel", "python", "tts.py"]
+60
View File
@@ -0,0 +1,60 @@
import torch
from TTS.api import TTS
import os
import io
import json
import gc
from flask import Flask, request, send_file
tts = TTS("tts_models/en/vctk/vits", progress_bar=False, gpu=False)
app = Flask(__name__)
voice_name_mapping = {}
use_voice_name_mapping = True
with open("./tts_voices_mapping.json", "r") as file:
voice_name_mapping = json.load(file)
if len(voice_name_mapping) == 0:
use_voice_name_mapping = False
voice_name_mapping_reversed = {v: k for k, v in voice_name_mapping.items()}
request_count = 0
@app.route("/generate-tts")
def text_to_speech():
global request_count
text = request.json.get("text", "")
voice = request.json.get("voice", "")
if use_voice_name_mapping:
voice = voice_name_mapping_reversed[voice]
result = None
with io.BytesIO() as data_bytes:
with torch.no_grad():
tts.tts_to_file(text=text, speaker=voice, file_path=data_bytes)
result = send_file(io.BytesIO(data_bytes.getvalue()), mimetype="audio/wav")
request_count += 1
return result
@app.route("/tts-voices")
def voices_list():
if use_voice_name_mapping:
data = list(voice_name_mapping.values())
data.sort()
return json.dumps(data)
else:
return json.dumps(tts.voices)
@app.route("/health-check")
def tts_health_check():
gc.collect()
if request_count > 2048:
return f"EXPIRED: {request_count}", 500
return f"OK: {request_count}", 200
if __name__ == "__main__":
if os.getenv('TTS_LD_LIBRARY_PATH', "") != "":
os.putenv('LD_LIBRARY_PATH', os.getenv('TTS_LD_LIBRARY_PATH'))
from waitress import serve
serve(app, host="0.0.0.0", port=5003, threads=4, backlog=8, connection_limit=24, channel_timeout=10)
+67
View File
@@ -0,0 +1,67 @@
{
"p226": "Male 01",
"p228": "Male 02",
"p229": "Male 03",
"p230": "Male 04",
"p231": "Male 05",
"p232": "Male 06",
"p233": "Male 07",
"p234": "Male 08",
"p236": "Male 09",
"p238": "Male 10",
"p239": "Male 11",
"p241": "Male 12",
"p251": "Male 13",
"p252": "Male 14",
"p253": "Male 15",
"p254": "Male 16",
"p255": "Male 17",
"p256": "Male 18",
"p257": "Male 19",
"p258": "Male 20",
"p262": "Male 21",
"p264": "Male 22",
"p265": "Male 23",
"p266": "Male 24",
"p267": "Male 25",
"p269": "Male 26",
"p272": "Male 27",
"p279": "Male 28",
"p281": "Male 29",
"p285": "Male 30",
"p286": "Male 31",
"p287": "Male 32",
"p298": "Male 33",
"p225": "Female 01",
"p227": "Female 02",
"p237": "Female 03",
"p240": "Female 04",
"p243": "Female 05",
"p244": "Female 06",
"p245": "Female 07",
"p246": "Female 08",
"p247": "Female 09",
"p248": "Female 10",
"p249": "Female 11",
"p250": "Female 12",
"p259": "Female 13",
"p260": "Female 14",
"p261": "Female 15",
"p263": "Female 16",
"p270": "Female 17",
"p271": "Female 18",
"p273": "Female 19",
"p274": "Female 20",
"p275": "Female 21",
"p276": "Female 22",
"p277": "Female 23",
"p278": "Female 24",
"p280": "Female 25",
"p283": "Female 26",
"p284": "Female 27",
"p288": "Female 28",
"p293": "Female 29",
"p294": "Female 30",
"p295": "Female 31",
"p297": "Female 32"
}