mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-07-17 11:05:16 +01:00
[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:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
/mob/Login()
|
||||
if(!client)
|
||||
return FALSE
|
||||
|
||||
canon_client = client
|
||||
add_to_player_list()
|
||||
lastKnownIP = client.address
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
+29
@@ -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,
|
||||
};
|
||||
+7
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
persistent_data
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user