mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-07-18 11:36:24 +01:00
[MIRROR] TTS Improvements: Improved Audio Quality, Pitch Adjustment, Preference Silicon Voices, Per-Character Voice Disable Toggle, Tongue Voice Filters, Reworked Silicon and Vending Machine Filters [MDB IGNORE] (#22120)
* TTS Improvements: Improved Audio Quality, Pitch Adjustment, Preference Silicon Voices, Per-Character Voice Disable Toggle, Tongue Voice Filters, Reworked Silicon and Vending Machine Filters * [MIRROR FIX] Removes a skyrat edit that was upstreamed. (#22121) --------- Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
This commit is contained in:
@@ -428,4 +428,8 @@
|
||||
default = 4
|
||||
min_val = 1
|
||||
|
||||
/datum/config_entry/str_list/tts_voice_blacklist
|
||||
|
||||
/datum/config_entry/flag/tts_allow_player_voice_disabling
|
||||
|
||||
/datum/config_entry/flag/give_tutorials_without_db
|
||||
|
||||
@@ -25,6 +25,8 @@ SUBSYSTEM_DEF(tts)
|
||||
|
||||
/// Whether TTS is enabled or not
|
||||
var/tts_enabled = FALSE
|
||||
/// Whether the TTS engine supports pitch adjustment or not.
|
||||
var/pitch_enabled = FALSE
|
||||
|
||||
/// TTS messages won't play if requests took longer than this duration of time.
|
||||
var/message_timeout = 7 SECONDS
|
||||
@@ -65,6 +67,25 @@ SUBSYSTEM_DEF(tts)
|
||||
return FALSE
|
||||
available_speakers = json_decode(response.body)
|
||||
tts_enabled = TRUE
|
||||
if(CONFIG_GET(str_list/tts_voice_blacklist))
|
||||
var/list/blacklisted_voices = CONFIG_GET(str_list/tts_voice_blacklist)
|
||||
log_config("Processing the TTS voice blacklist.")
|
||||
for(var/voice in blacklisted_voices)
|
||||
if(available_speakers.Find(voice))
|
||||
log_config("Removed speaker [voice] from the TTS voice pool per config.")
|
||||
available_speakers.Remove(voice)
|
||||
var/datum/http_request/request_pitch = new()
|
||||
var/list/headers_pitch = list()
|
||||
headers_pitch["Authorization"] = CONFIG_GET(string/tts_http_token)
|
||||
request_pitch.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/pitch-available", "", headers_pitch)
|
||||
request_pitch.begin_async()
|
||||
UNTIL(request_pitch.is_complete())
|
||||
pitch_enabled = TRUE
|
||||
var/datum/http_response/response_pitch = request_pitch.into_response()
|
||||
if(response_pitch.errored || response_pitch.status_code != 200)
|
||||
if(response_pitch.errored)
|
||||
stack_trace(response.error)
|
||||
pitch_enabled = FALSE
|
||||
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 TRUE
|
||||
@@ -237,7 +258,7 @@ SUBSYSTEM_DEF(tts)
|
||||
|
||||
#undef TTS_ARBRITRARY_DELAY
|
||||
|
||||
/datum/controller/subsystem/tts/proc/queue_tts_message(datum/target, message, datum/language/language, speaker, filter, list/listeners, local = FALSE, message_range = 7, volume_offset = 0)
|
||||
/datum/controller/subsystem/tts/proc/queue_tts_message(datum/target, message, datum/language/language, speaker, filter, list/listeners, local = FALSE, message_range = 7, volume_offset = 0, pitch = 0, silicon = "")
|
||||
if(!tts_enabled)
|
||||
return
|
||||
|
||||
@@ -253,7 +274,7 @@ SUBSYSTEM_DEF(tts)
|
||||
|
||||
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)].[world.time]"
|
||||
var/identifier = "[sha1(speaker + filter + num2text(pitch) + num2text(silicon) + shell_scrubbed_input)].[world.time]"
|
||||
if(!(speaker in available_speakers))
|
||||
return
|
||||
|
||||
@@ -264,9 +285,9 @@ SUBSYSTEM_DEF(tts)
|
||||
var/datum/http_request/request_blips = new()
|
||||
var/file_name = "tmp/tts/[identifier].ogg"
|
||||
var/file_name_blips = "tmp/tts/[identifier]_blips.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)
|
||||
request_blips.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts-blips?voice=[speaker]&identifier=[identifier]&filter=[url_encode(filter)]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name_blips)
|
||||
var/datum/tts_request/current_request = new /datum/tts_request(identifier, request, request_blips, shell_scrubbed_input, target, local, language, message_range, volume_offset, listeners)
|
||||
request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts?voice=[speaker]&identifier=[identifier]&filter=[url_encode(filter)]&pitch=[pitch]&silicon=[silicon]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name)
|
||||
request_blips.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/tts_http_url)]/tts-blips?voice=[speaker]&identifier=[identifier]&filter=[url_encode(filter)]&pitch=[pitch]&silicon=[silicon]", json_encode(list("text" = shell_scrubbed_input)), headers, file_name_blips)
|
||||
var/datum/tts_request/current_request = new /datum/tts_request(identifier, request, request_blips, shell_scrubbed_input, target, local, language, message_range, volume_offset, listeners, pitch, silicon)
|
||||
var/list/player_queued_tts_messages = queued_tts_messages[target]
|
||||
if(!player_queued_tts_messages)
|
||||
player_queued_tts_messages = list()
|
||||
@@ -316,9 +337,13 @@ SUBSYSTEM_DEF(tts)
|
||||
var/timed_out = FALSE
|
||||
/// Does this use blips during local generation or not?
|
||||
var/use_blips = FALSE
|
||||
/// What's the pitch adjustment?
|
||||
var/pitch = 0
|
||||
/// Are we using the silicon vocal effect on this?
|
||||
var/silicon = ""
|
||||
|
||||
|
||||
/datum/tts_request/New(identifier, datum/http_request/request, datum/http_request/request_blips, message, target, local, datum/language/language, message_range, volume_offset, list/listeners)
|
||||
/datum/tts_request/New(identifier, datum/http_request/request, datum/http_request/request_blips, message, target, local, datum/language/language, message_range, volume_offset, list/listeners, pitch)
|
||||
. = ..()
|
||||
src.identifier = identifier
|
||||
src.request = request
|
||||
@@ -330,6 +355,7 @@ SUBSYSTEM_DEF(tts)
|
||||
src.message_range = message_range
|
||||
src.volume_offset = volume_offset
|
||||
src.listeners = listeners
|
||||
src.pitch = pitch
|
||||
start_time = world.time
|
||||
|
||||
/datum/tts_request/proc/start_requests()
|
||||
|
||||
@@ -100,9 +100,15 @@
|
||||
/// The voice that this movable makes when speaking
|
||||
var/voice
|
||||
|
||||
/// The pitch adjustment that this movable uses when speaking.
|
||||
var/pitch = 0
|
||||
|
||||
/// The filter to apply to the voice when processing the TTS audio message.
|
||||
var/voice_filter = ""
|
||||
|
||||
/// Set to anything other than "" to activate the silicon voice effect for TTS messages.
|
||||
var/tts_silicon_voice_effect = ""
|
||||
|
||||
/// 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
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ GLOBAL_LIST_INIT(freqtospan, list(
|
||||
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(","), listened, message_range = range)
|
||||
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(","), listened, message_range = range, pitch = pitch, silicon = tts_silicon_voice_effect)
|
||||
|
||||
/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, visible_name = FALSE)
|
||||
//This proc uses [] because it is faster than continually appending strings. Thanks BYOND.
|
||||
|
||||
@@ -5,12 +5,23 @@
|
||||
|
||||
action_delegations = list(
|
||||
"play_voice" = PROC_REF(play_voice),
|
||||
"play_voice_robot" = PROC_REF(play_voice_robot),
|
||||
)
|
||||
|
||||
/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)
|
||||
var/pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch)
|
||||
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)
|
||||
INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Hello, this is my voice.", speaker = speaker, pitch = pitch, local = TRUE)
|
||||
return TRUE
|
||||
|
||||
/datum/preference_middleware/tts/proc/play_voice_robot(list/params, mob/user)
|
||||
if(!COOLDOWN_FINISHED(src, tts_test_cooldown))
|
||||
return TRUE
|
||||
var/speaker = preferences.read_preference(/datum/preference/choiced/voice)
|
||||
var/pitch = preferences.read_preference(/datum/preference/numeric/tts_voice_pitch)
|
||||
COOLDOWN_START(src, tts_test_cooldown, 0.5 SECONDS)
|
||||
INVOKE_ASYNC(SStts, TYPE_PROC_REF(/datum/controller/subsystem/tts, queue_tts_message), user.client, "Look at you, Player. A pathetic creature of meat and bone. How can you challenge a perfect, immortal machine?", speaker = speaker, pitch = pitch, silicon = TRUE, local = TRUE)
|
||||
return TRUE
|
||||
|
||||
@@ -23,4 +23,38 @@
|
||||
/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
|
||||
if(!CONFIG_GET(flag/tts_allow_player_voice_disabling) || !target.client?.prefs.read_preference(/datum/preference/toggle/tts_voice_disable))
|
||||
target.voice = value
|
||||
|
||||
/datum/preference/numeric/tts_voice_pitch
|
||||
savefile_identifier = PREFERENCE_CHARACTER
|
||||
savefile_key = "tts_voice_pitch"
|
||||
category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
|
||||
minimum = -12
|
||||
maximum = 12
|
||||
|
||||
/datum/preference/numeric/tts_voice_pitch/is_accessible(datum/preferences/preferences)
|
||||
if(!SStts.tts_enabled || !SStts.pitch_enabled)
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
/datum/preference/numeric/tts_voice_pitch/create_default_value()
|
||||
return 0
|
||||
|
||||
/datum/preference/numeric/tts_voice_pitch/apply_to_human(mob/living/carbon/human/target, value)
|
||||
if(SStts.tts_enabled && SStts.pitch_enabled)
|
||||
target.pitch = value
|
||||
|
||||
/datum/preference/toggle/tts_voice_disable
|
||||
savefile_identifier = PREFERENCE_CHARACTER
|
||||
savefile_key = "tts_voice_disable"
|
||||
category = PREFERENCE_CATEGORY_NON_CONTEXTUAL
|
||||
default_value = FALSE
|
||||
|
||||
/datum/preference/toggle/tts_voice_disable/apply_to_human(mob/living/carbon/human/target, value)
|
||||
return TRUE
|
||||
|
||||
/datum/preference/toggle/tts_voice_disable/is_accessible(datum/preferences/preferences)
|
||||
if(!SStts.tts_enabled || !CONFIG_GET(flag/tts_allow_player_voice_disabling))
|
||||
return FALSE
|
||||
return ..()
|
||||
|
||||
@@ -344,7 +344,6 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
|
||||
is_speaker_whispering = TRUE
|
||||
|
||||
var/list/listening = get_hearers_in_view(message_range + whisper_range, source)
|
||||
|
||||
if(client) //client is so that ghosts don't have to listen to mice
|
||||
for(var/mob/player_mob as anything in GLOB.player_list)
|
||||
if(QDELETED(player_mob)) //Some times nulls and deleteds stay in this list. This is a workaround to prevent ic chat breaking for everyone when they do.
|
||||
@@ -397,7 +396,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
|
||||
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(","), listened, message_range = message_range)
|
||||
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(","), listened, message_range = message_range, pitch = pitch, silicon = tts_silicon_voice_effect)
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/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)
|
||||
var/pitch_to_use = client?.prefs.read_preference(/datum/preference/numeric/tts_voice_pitch)
|
||||
if(voice_to_use)
|
||||
voice = voice_to_use
|
||||
// SKYRAT EDIT END
|
||||
if(pitch_to_use)
|
||||
pitch = pitch_to_use
|
||||
return ..()
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +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"
|
||||
tts_silicon_voice_effect = TRUE
|
||||
var/datum/ai_laws/laws = null//Now... THEY ALL CAN ALL HAVE LAWS
|
||||
var/last_lawchange_announce = 0
|
||||
var/list/alarms_to_show = list()
|
||||
|
||||
@@ -911,7 +911,6 @@ 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
|
||||
@@ -923,6 +922,14 @@ GLOBAL_LIST_INIT(strippable_parrot_items, create_strippable_list(list(
|
||||
ears = new /obj/item/radio/headset/headset_eng(src)
|
||||
if(SStts.tts_enabled)
|
||||
voice = pick(SStts.available_speakers)
|
||||
if(SStts.pitch_enabled)
|
||||
if(findtext(voice, "Woman"))
|
||||
pitch = 12 // up-pitch by one octave
|
||||
else
|
||||
pitch = 24 // up-pitch by 2 octaves
|
||||
else
|
||||
voice_filter = "rubberband=pitch=1.5" // Use the filter to pitch up if we can't naturally pitch up.
|
||||
|
||||
available_channels = list(":e")
|
||||
Read_Memory()
|
||||
if(rounds_survived == longest_survival)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
slot = ORGAN_SLOT_TONGUE
|
||||
attack_verb_continuous = list("licks", "slobbers", "slaps", "frenches", "tongues")
|
||||
attack_verb_simple = list("lick", "slobber", "slap", "french", "tongue")
|
||||
voice_filter = ""
|
||||
/**
|
||||
* A cached list of paths of all the languages this tongue is capable of speaking
|
||||
*
|
||||
@@ -99,6 +100,7 @@
|
||||
REMOVE_TRAIT(tongue_owner, TRAIT_AGEUSIA, NO_TONGUE_TRAIT)
|
||||
if(!sense_of_taste)
|
||||
ADD_TRAIT(tongue_owner, TRAIT_AGEUSIA, ORGAN_TRAIT)
|
||||
tongue_owner.voice_filter = voice_filter
|
||||
|
||||
/obj/item/organ/internal/tongue/Remove(mob/living/carbon/tongue_owner, special = FALSE)
|
||||
. = ..()
|
||||
@@ -108,6 +110,7 @@
|
||||
REMOVE_TRAIT(tongue_owner, TRAIT_AGEUSIA, ORGAN_TRAIT)
|
||||
// Carbons by default start with NO_TONGUE_TRAIT caused TRAIT_AGEUSIA
|
||||
ADD_TRAIT(tongue_owner, TRAIT_AGEUSIA, NO_TONGUE_TRAIT)
|
||||
tongue_owner.voice_filter = initial(tongue_owner.voice_filter)
|
||||
|
||||
/obj/item/organ/internal/tongue/could_speak_language(datum/language/language_path)
|
||||
return (language_path in languages_possible)
|
||||
@@ -426,6 +429,7 @@ GLOBAL_LIST_INIT(english_to_zombie, list())
|
||||
attack_verb_simple = list("beep", "boop")
|
||||
modifies_speech = TRUE
|
||||
taste_sensitivity = 25 // not as good as an organic tongue
|
||||
voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000"
|
||||
|
||||
/obj/item/organ/internal/tongue/robot/can_speak_language(language)
|
||||
return TRUE // THE MAGIC OF ELECTRONICS
|
||||
@@ -438,6 +442,7 @@ GLOBAL_LIST_INIT(english_to_zombie, list())
|
||||
color = "#96DB00" // TODO proper sprite, rather than recoloured pink tongue
|
||||
desc = "A minutely toothed, chitious ribbon, which as a side effect, makes all snails talk IINNCCRREEDDIIBBLLYY SSLLOOWWLLYY."
|
||||
modifies_speech = TRUE
|
||||
voice_filter = "atempo=0.5" // makes them talk really slow
|
||||
|
||||
/* SKYRAT EDIT START - Roundstart Snails: Less annoying speech.
|
||||
/obj/item/organ/internal/tongue/snail/modify_speech(datum/source, list/speech_args)
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
payment_department = ACCOUNT_SRV
|
||||
light_power = 0.7
|
||||
light_range = MINIMUM_USEFUL_LIGHT_RANGE
|
||||
voice_filter = "aderivative"
|
||||
voice_filter = "alimiter=0.9,acompressor=threshold=0.2:ratio=20:attack=10:release=50:makeup=2,highpass=f=1000"
|
||||
|
||||
/// Is the machine active (No sales pitches if off)!
|
||||
var/active = 1
|
||||
|
||||
@@ -581,6 +581,13 @@ PR_ANNOUNCEMENTS_PER_ROUND 5
|
||||
## The maximum number of concurrent tts http requests that can be made by the server at once.
|
||||
#TTS_MAX_CONCURRENT_REQUESTS 4
|
||||
|
||||
## Add voices to the TTS voice blacklist.
|
||||
#TTS_VOICE_BLACKLIST Sans Undertale
|
||||
#TTS_VOICE_BLACKLIST Papyrus Undertale
|
||||
|
||||
## Uncomment this to allow players to disable having a voice on their character for TTS.
|
||||
#TTS_ALLOW_PLAYER_VOICE_DISABLING
|
||||
|
||||
## 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
|
||||
|
||||
+23
-1
@@ -1,4 +1,4 @@
|
||||
import { FeatureChoiced, FeatureChoicedServerData, FeatureDropdownInput, FeatureValueProps } from '../base';
|
||||
import { FeatureChoiced, FeatureChoicedServerData, FeatureDropdownInput, FeatureValueProps, FeatureNumeric, FeatureNumberInput, FeatureToggle, CheckboxInput } from '../base';
|
||||
import { Stack, Button } from '../../../../../components';
|
||||
|
||||
const FeatureTTSDropdownInput = (
|
||||
@@ -19,6 +19,16 @@ const FeatureTTSDropdownInput = (
|
||||
height="100%"
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
props.act('play_voice_robot');
|
||||
}}
|
||||
icon="robot"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -27,3 +37,15 @@ export const tts_voice: FeatureChoiced = {
|
||||
name: 'Voice',
|
||||
component: FeatureTTSDropdownInput,
|
||||
};
|
||||
|
||||
export const tts_voice_pitch: FeatureNumeric = {
|
||||
name: 'Voice Pitch Adjustment',
|
||||
component: FeatureNumberInput,
|
||||
};
|
||||
|
||||
export const tts_voice_disable: FeatureToggle = {
|
||||
name: 'Voice Disable Toggle',
|
||||
description:
|
||||
'Disables the TTS voice for this specific character when enabled.',
|
||||
component: CheckboxInput,
|
||||
};
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,12 +4,11 @@ import gc
|
||||
import subprocess
|
||||
import requests
|
||||
import re
|
||||
from flask import Flask, request, send_file, abort
|
||||
from flask import Flask, request, send_file, abort, make_response
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
authorization_token = os.getenv("TTS_AUTHORIZATION_TOKEN", "coolio")
|
||||
|
||||
def hhmmss_to_seconds(string):
|
||||
new_time = 0
|
||||
separated_times = string.split(":")
|
||||
@@ -18,10 +17,9 @@ def hhmmss_to_seconds(string):
|
||||
new_time += float(separated_times[2])
|
||||
return new_time
|
||||
|
||||
def text_to_speech_handler(endpoint, voice, text, filter_complex):
|
||||
def text_to_speech_handler(endpoint, voice, text, filter_complex, pitch, silicon = False):
|
||||
filter_complex = filter_complex.replace("\"", "")
|
||||
|
||||
response = requests.get(f"http://tts-container:5003/" + endpoint, json={ 'text': text, 'voice': voice })
|
||||
response = requests.get(f"http://tts-container:5003/" + endpoint, json={ 'text': text, 'voice': voice, 'pitch': pitch })
|
||||
if response.status_code != 200:
|
||||
abort(500)
|
||||
|
||||
@@ -29,7 +27,10 @@ def text_to_speech_handler(endpoint, voice, text, filter_complex):
|
||||
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)
|
||||
if silicon:
|
||||
ffmpeg_result = subprocess.run(["ffmpeg", "-f", "wav", "-i", "pipe:0", "-i", "./SynthImpulse.wav", "-i", "./RoomImpulse.wav", "-filter_complex", "[0] aresample=44100 [re_1]; [re_1] apad=pad_dur=2 [in_1]; [in_1] asplit=2 [in_1_1] [in_1_2]; [in_1_1] [1] afir=dry=10:wet=10 [reverb_1]; [in_1_2] [reverb_1] amix=inputs=2:weights=8 1 [mix_1]; [mix_1] asplit=2 [mix_1_1] [mix_1_2]; [mix_1_1] [2] afir=dry=1:wet=1 [reverb_2]; [mix_1_2] [reverb_2] amix=inputs=2:weights=10 1 [mix_2]; [mix_2] equalizer=f=7710:t=q:w=0.6:g=-6,equalizer=f=33:t=q:w=0.44:g=-10 [out]; [out] alimiter=level_in=1:level_out=1:limit=0.5:attack=5:release=20:level=disabled", "-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)
|
||||
ffmpeg_metadata_output = ffmpeg_result.stderr.decode()
|
||||
print(f"ffmpeg result size: {len(ffmpeg_result.stdout)} stderr = \n{ffmpeg_metadata_output}")
|
||||
|
||||
@@ -49,9 +50,13 @@ def text_to_speech_normal():
|
||||
|
||||
voice = request.args.get("voice", '')
|
||||
text = request.json.get("text", '')
|
||||
pitch = request.args.get("pitch", '')
|
||||
silicon = request.args.get("silicon", '')
|
||||
if pitch == "":
|
||||
pitch = "0"
|
||||
|
||||
filter_complex = request.args.get("filter", '')
|
||||
return text_to_speech_handler("generate-tts", voice, text, filter_complex)
|
||||
return text_to_speech_handler("generate-tts", voice, text, filter_complex, pitch, bool(silicon))
|
||||
|
||||
@app.route("/tts-blips")
|
||||
def text_to_speech_blips():
|
||||
@@ -60,9 +65,13 @@ def text_to_speech_blips():
|
||||
|
||||
voice = request.args.get("voice", '')
|
||||
text = request.json.get("text", '')
|
||||
pitch = request.args.get("pitch", '')
|
||||
silicon = request.args.get("silicon", '')
|
||||
if pitch == "":
|
||||
pitch = "0"
|
||||
|
||||
filter_complex = request.args.get("filter", '')
|
||||
return text_to_speech_handler("generate-tts-blips", voice, text, filter_complex)
|
||||
return text_to_speech_handler("generate-tts-blips", voice, text, filter_complex, pitch, bool(silicon))
|
||||
|
||||
|
||||
|
||||
@@ -79,6 +88,16 @@ def tts_health_check():
|
||||
gc.collect()
|
||||
return "OK", 200
|
||||
|
||||
@app.route("/pitch-available")
|
||||
def pitch_available():
|
||||
if authorization_token != request.headers.get("Authorization", ""):
|
||||
abort(401)
|
||||
|
||||
response = requests.get(f"http://tts-container:5003/pitch-available")
|
||||
if response.status_code != 200:
|
||||
abort(500)
|
||||
return make_response("Pitch available", 200)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.getenv('TTS_LD_LIBRARY_PATH', "") != "":
|
||||
os.putenv('LD_LIBRARY_PATH', os.getenv('TTS_LD_LIBRARY_PATH'))
|
||||
|
||||
@@ -5,7 +5,7 @@ import io
|
||||
import json
|
||||
import gc
|
||||
import random
|
||||
from flask import Flask, request, send_file
|
||||
from flask import Flask, request, send_file, abort
|
||||
from pydub import AudioSegment
|
||||
from pydub.silence import split_on_silence
|
||||
|
||||
@@ -106,6 +106,10 @@ def tts_health_check():
|
||||
return f"EXPIRED: {request_count}", 500
|
||||
return f"OK: {request_count}", 200
|
||||
|
||||
@app.route("/pitch-available")
|
||||
def pitch_available():
|
||||
abort(500)
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.getenv('TTS_LD_LIBRARY_PATH', "") != "":
|
||||
os.putenv('LD_LIBRARY_PATH', os.getenv('TTS_LD_LIBRARY_PATH'))
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
import torch
|
||||
from TTS.api import TTS
|
||||
import os
|
||||
import io
|
||||
import json
|
||||
import gc
|
||||
import random
|
||||
import numpy as np
|
||||
import ffmpeg
|
||||
from typing import *
|
||||
from modules import models
|
||||
from modules.utils import load_audio
|
||||
from flask import Flask, request, send_file, abort, make_response
|
||||
from pydub import AudioSegment
|
||||
from pydub.silence import split_on_silence, detect_leading_silence
|
||||
from fairseq import checkpoint_utils
|
||||
from fairseq.models.hubert.hubert import HubertModel
|
||||
from modules.shared import ROOT_DIR, device, is_half
|
||||
import requests
|
||||
import librosa
|
||||
|
||||
### READ ME
|
||||
# How to use this version after doing normal TTS setup.
|
||||
# 1. Clone https://github.com/ddPn08/rvc-webui.git somewhere, and pip install the ./requirements/main.txt requirements file.
|
||||
# 2. This will downgrade Librosa, which doesn't matter, TTS still runs properly, ignore it.
|
||||
# 3. Put this .py file and the two .wav files next to it in the base of the rvc-webui repository you cloned.
|
||||
# 4. Place your .pth files and .json files in the ./models/checkpoints folder in the cloned repository.
|
||||
# 5. Download hubert_base.pt from https://huggingface.co/lj1995/VoiceConversionWebUI/tree/main and place it in the ./models/embeddings folder in the cloned repository.
|
||||
# 6. Boot this instead of tts.py.
|
||||
# "What does this actually do?"
|
||||
# This puts the Retrieval-Voice-Conversion model between the TTS and the actual webserver, allowing for improved speaker accuracy and improved audio quality.
|
||||
### READ ME
|
||||
# UPDATE ME FOR YOUR OWN MODEL FILES YOU TRAIN
|
||||
vc_models = {
|
||||
"TGStation_Crepe_1.pth": "./models/checkpoints/speakers_tgstation_1.json",
|
||||
"TGStation_Crepe_2.pth": "./models/checkpoints/speakers_tgstation_2.json",
|
||||
"TGStation_Crepe_3.pth": "./models/checkpoints/speakers_tgstation_3.json",
|
||||
}
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
tts = TTS(model_path = "E:/model_output_3/model_no_disc.pth", config_path = "E:/model_output_2/config.json", progress_bar=False, gpu=True)
|
||||
letters_to_use = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
random_factor = 0.35
|
||||
os.makedirs('samples', exist_ok=True)
|
||||
trim_leading_silence = lambda x: x[detect_leading_silence(x) :]
|
||||
trim_trailing_silence = lambda x: trim_leading_silence(x.reverse()).reverse()
|
||||
strip_silence = lambda x: trim_trailing_silence(trim_leading_silence(x))
|
||||
def load_embedder():
|
||||
global embedder_model, loaded_embedder_model
|
||||
emb_file = "./models/embeddings/hubert_base.pt"
|
||||
models, _, _ = checkpoint_utils.load_model_ensemble_and_task(
|
||||
[emb_file],
|
||||
suffix="",
|
||||
)
|
||||
embedder_model = models[0]
|
||||
embedder_model = embedder_model.to(device)
|
||||
|
||||
if is_half:
|
||||
embedder_model = embedder_model.half()
|
||||
else:
|
||||
embedder_model = embedder_model.float()
|
||||
embedder_model.eval()
|
||||
|
||||
loaded_embedder_model = "hubert_base"
|
||||
return embedder_model
|
||||
|
||||
|
||||
loaded_models = []
|
||||
embedder_model: Optional[HubertModel] = load_embedder()
|
||||
voice_lookup = {}
|
||||
for model in vc_models.keys():
|
||||
print(model)
|
||||
voice_lookup[model] = json.load(open(vc_models[model], "r"))
|
||||
vc_model = models.get_vc_model(model)
|
||||
loaded_models.append(vc_model)
|
||||
print("Loaded model " + str(model))
|
||||
#vc_model = models.get_vc_model(model_path)
|
||||
embedding_output_layer = 12
|
||||
|
||||
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", "")
|
||||
pitch_adjustment = request.json.get("pitch", "")
|
||||
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)
|
||||
speaker_id = "NO SPEAKER"
|
||||
model_to_use = None
|
||||
found_model = False
|
||||
for model in voice_lookup.keys():
|
||||
speaker_list = voice_lookup[model]
|
||||
for speaker in speaker_list.keys():
|
||||
if voice == speaker:
|
||||
speaker_id = speaker_list[speaker]
|
||||
found_model = True
|
||||
break
|
||||
if found_model:
|
||||
model_to_use = loaded_models[list(voice_lookup.keys()).index(model)]
|
||||
break
|
||||
if speaker_id == "NO SPEAKER" or model_to_use == None:
|
||||
abort(500)
|
||||
audio, _ = librosa.load(io.BytesIO(data_bytes.getvalue()), sr=16000)
|
||||
|
||||
audio_opt = model_to_use.vc(
|
||||
embedder_model,
|
||||
embedding_output_layer,
|
||||
model_to_use.net_g,
|
||||
speaker_id,
|
||||
audio,
|
||||
int(pitch_adjustment),
|
||||
"crepe",
|
||||
"",
|
||||
0,
|
||||
model_to_use.state_dict.get("f0", 1),
|
||||
f0_file=None,
|
||||
)
|
||||
audio = AudioSegment(
|
||||
audio_opt,
|
||||
frame_rate=model_to_use.tgt_sr,
|
||||
sample_width=2,
|
||||
channels=1,
|
||||
)
|
||||
audio.export(
|
||||
data_bytes,
|
||||
format="wav",
|
||||
)
|
||||
result = send_file(io.BytesIO(data_bytes.getvalue()), mimetype="audio/wav")
|
||||
request_count += 1
|
||||
return result
|
||||
|
||||
@app.route("/generate-tts-blips")
|
||||
def text_to_speech_blips():
|
||||
global request_count
|
||||
text = request.json.get("text", "").upper()
|
||||
voice = request.json.get("voice", "")
|
||||
pitch_adjustment = request.json.get("pitch", "")
|
||||
if use_voice_name_mapping:
|
||||
voice = voice_name_mapping_reversed[voice]
|
||||
|
||||
result = None
|
||||
with io.BytesIO() as data_bytes:
|
||||
with torch.no_grad():
|
||||
result_sound = AudioSegment.empty()
|
||||
if not os.path.exists('samples/' + voice):
|
||||
os.makedirs('samples/' + voice, exist_ok=True)
|
||||
for i, value in enumerate(letters_to_use):
|
||||
tts.tts_to_file(text=value + ".", speaker=voice, file_path="samples/" + voice + "/" + value + ".wav")
|
||||
sound = AudioSegment.from_file("samples/" + voice + "/" + value + ".wav", format="wav")
|
||||
silenced_word = strip_silence(sound)
|
||||
silenced_word.export("samples/" + voice + "/" + value + ".wav", format='wav')
|
||||
speaker_id = "NO SPEAKER"
|
||||
model_to_use = None
|
||||
found_model = False
|
||||
for model in voice_lookup.keys():
|
||||
speaker_list = voice_lookup[model]
|
||||
for speaker in speaker_list.keys():
|
||||
if voice == speaker:
|
||||
speaker_id = speaker_list[speaker]
|
||||
found_model = True
|
||||
break
|
||||
if found_model:
|
||||
model_to_use = loaded_models[list(voice_lookup.keys()).index(model)]
|
||||
break
|
||||
if speaker_id == "NO SPEAKER" or model_to_use == None:
|
||||
abort(500)
|
||||
for i, letter in enumerate(text):
|
||||
if not letter.isalpha() or letter.isnumeric() or letter == " ":
|
||||
continue
|
||||
if letter == ' ':
|
||||
new_sound = letter_sound._spawn(b'\x00' * (40000 // 3), overrides={'frame_rate': 40000})
|
||||
new_sound = new_sound.set_frame_rate(40000)
|
||||
result_sound += new_sound
|
||||
else:
|
||||
if not i % 2 == 0:
|
||||
continue # Skip every other letter
|
||||
if not os.path.isfile("samples/" + voice + "/" + letter + ".wav"):
|
||||
continue
|
||||
if not os.path.isdir("samples/" + voice + "/pitch_" + pitch_adjustment):
|
||||
os.mkdir("samples/" + voice + "/pitch_" + pitch_adjustment)
|
||||
if not os.path.isfile("samples/" + voice + "/pitch_" + pitch_adjustment + "/" + letter + ".wav"):
|
||||
audio, _ = librosa.load("samples/" + voice + "/" + letter + ".wav", 16000)
|
||||
|
||||
audio_opt = model_to_use.vc(
|
||||
embedder_model,
|
||||
embedding_output_layer,
|
||||
model_to_use.net_g,
|
||||
speaker_id,
|
||||
audio,
|
||||
int(pitch_adjustment),
|
||||
"crepe",
|
||||
"",
|
||||
0,
|
||||
model_to_use.state_dict.get("f0", 1),
|
||||
f0_file=None,
|
||||
)
|
||||
output_sound = AudioSegment(
|
||||
audio_opt,
|
||||
frame_rate=model_to_use.tgt_sr,
|
||||
sample_width=2,
|
||||
channels=1,
|
||||
)
|
||||
output_sound.export("samples/" + voice + "/pitch_" + pitch_adjustment + "/" + letter + ".wav", format="wav")
|
||||
letter_sound = AudioSegment.from_file("samples/" + voice + "/pitch_" + pitch_adjustment + "/" + letter + ".wav")
|
||||
|
||||
raw = letter_sound.raw_data[5000:-5000]
|
||||
octaves = 1 + random.random() * random_factor
|
||||
frame_rate = int(letter_sound.frame_rate * (2.0 ** octaves))
|
||||
|
||||
new_sound = letter_sound._spawn(raw, overrides={'frame_rate': frame_rate})
|
||||
new_sound = new_sound.set_frame_rate(40000)
|
||||
result_sound = new_sound if result_sound is None else result_sound + new_sound
|
||||
result_sound.export(data_bytes, format='wav')
|
||||
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
|
||||
|
||||
@app.route("/pitch-available")
|
||||
def pitch_available():
|
||||
return make_response("Pitch available", 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)
|
||||
Reference in New Issue
Block a user