From 6f3ab8cbb91b303bcd3892a704379390342d7d47 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:42:45 +0200 Subject: [PATCH 01/37] fix(say): Refactor !say and !tsay classes --- .github/workflows/ci.yml | 1 + src/torchlight/CommandHandler.py | 3 +- src/torchlight/Commands.py | 167 ++++++++----------------------- 3 files changed, 44 insertions(+), 127 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 419029f..f6099dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ on: branches: - 'main' - 'master' + - 'my-master' pull_request: branches: - master diff --git a/src/torchlight/CommandHandler.py b/src/torchlight/CommandHandler.py index 3cd5e0d..91848f4 100644 --- a/src/torchlight/CommandHandler.py +++ b/src/torchlight/CommandHandler.py @@ -7,7 +7,7 @@ from torchlight.AccessManager import AccessManager from torchlight.AudioManager import AudioManager -from torchlight.Commands import BaseCommand, VoiceTrigger +from torchlight.Commands import BaseCommand, Say, VoiceTrigger from torchlight.PlayerManager import Player, PlayerManager from torchlight.Torchlight import Torchlight from torchlight.TriggerManager import TriggerManager @@ -41,6 +41,7 @@ def Setup(self) -> None: subklasses: list[type[Any]] = [] subklasses.extend(BaseCommand.__subclasses__()) subklasses.extend(VoiceTrigger.__subclasses__()) + subklasses.extend(Say.__subclasses__()) for subklass in sorted(subklasses, key=lambda x: x.order, reverse=True): try: command = subklass( diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 7a7af92..1e29030 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -1006,18 +1006,7 @@ async def Say(self, player: Player, language: str, tld: str, message: str) -> in os.unlink(temp_file.name) return 1 - async def _func(self, message: list[str], player: Player) -> int: - self.logger.debug(sys._getframe().f_code.co_name + " " + str(message)) - - if self.check_disabled(player): - return -1 - - if self.check_chat_cooldown(player): - return -1 - - if not message[1]: - return 1 - + def HandleSay(self, message: list[str], player: Player) -> tuple[str, str] | None: language: str = "" tld: str = "com" @@ -1028,109 +1017,61 @@ async def _func(self, message: list[str], player: Player) -> int: if "tld" in command_config["parameters"]["default"]: tld = command_config["parameters"]["default"]["tld"] - if len(message[0]) > 4: - language = message[0][4:] + thisTrigger: str = "" + for trigger in self.triggers: + if message[0].lower().startswith(trigger): + thisTrigger = trigger + break + + if not thisTrigger: + return None + + if message[0].lower() == "!say" or message[0].lower() == "!tsay": + return None + + language = message[0][len(thisTrigger):] self.logger.debug(f"{language}: {self.VALID_LANGUAGES}") if len(language) <= 0 or language not in self.VALID_LANGUAGES: + return None + + return language, tld + + async def _func(self, message: list[str], player: Player) -> int: + self.logger.debug(sys._getframe().f_code.co_name + " " + str(message)) + + if self.check_disabled(player): + return -1 + + if self.check_chat_cooldown(player): + return -1 + + if not message[1]: return 1 - self.torchlight.SetPlayerCooldown(player, self.torchlight.config["AntiSpam"]["ChatCooldown"]) + res = self.HandleSay(message, player) + if not res: + return 1 + + language, tld = res + asyncio.ensure_future(self.Say(player, language, tld, message[1])) self.torchlight.SetPlayerCooldown(player, self.torchlight.config["AntiSpam"]["ChatCooldown"]) return 0 -class TranslateSay(BaseCommand): - try: - VALID_LANGUAGES = list(gtts.lang.tts_langs().keys()) - except Exception: - VALID_LANGUAGES = [ - "af", - "ar", - "bn", - "bs", - "ca", - "cs", - "cy", - "da", - "de", - "el", - "en", - "eo", - "es", - "et", - "fi", - "fr", - "gu", - "hi", - "hr", - "hu", - "hy", - "id", - "is", - "it", - "ja", - "jw", - "km", - "kn", - "ko", - "la", - "lv", - "mk", - "ml", - "mr", - "my", - "ne", - "nl", - "no", - "pl", - "pt", - "ro", - "ru", - "si", - "sk", - "sq", - "sr", - "su", - "sv", - "sw", - "ta", - "te", - "th", - "tl", - "tr", - "uk", - "ur", - "vi", - "zh-CN", - "zh-TW", - "zh", - ] - - def __init__( - self, - torchlight: Torchlight, - access_manager: AccessManager, - player_manager: PlayerManager, - audio_manager: AudioManager, - trigger_manager: TriggerManager, - ) -> None: - super().__init__(torchlight, access_manager, player_manager, audio_manager, trigger_manager) - +class TranslateSay(Say): + def _setup(self) -> None: self.translator = Translator() - self.triggers = [f"!tsay{lang}" for lang in self.VALID_LANGUAGES] - async def TranslateAndSay(self, player: Player, target_lang: str, tld: str, message: str) -> int: - supported_langs = gtts.lang.tts_langs() - - if target_lang not in supported_langs: - self.torchlight.SayPrivate(player, f"Sorry, TTS for '{target_lang}' is not supported.") + async def Say(self, player: Player, language: str, tld: str, message: str) -> int: + if language not in self.VALID_LANGUAGES: + self.torchlight.SayPrivate(player, f"Sorry, TTS for '{language}' is not supported.") return 1 try: translated = await asyncio.get_event_loop().run_in_executor( - None, lambda: self.translator.translate(message, target_lang) + None, lambda: self.translator.translate(message, language) ) translated_text = translated.result except Exception as e: @@ -1138,7 +1079,7 @@ async def TranslateAndSay(self, player: Player, target_lang: str, tld: str, mess return 1 try: - tts = gtts.gTTS(text=translated_text, lang=target_lang, tld=tld, lang_check=False) + tts = gtts.gTTS(text=translated_text, lang=language, tld=tld, lang_check=False) temp_file = tempfile.NamedTemporaryFile(delete=False) tts.write_to_fp(temp_file) temp_file.close() @@ -1159,32 +1100,6 @@ async def TranslateAndSay(self, player: Player, target_lang: str, tld: str, mess os.unlink(temp_file.name) return 1 - async def _func(self, message: list[str], player: Player) -> int: - self.logger.debug("_func " + str(message)) - - if self.check_disabled(player): - return -1 - - if not message[1]: - return 1 - - command = message[0] - - if not command.startswith("!tsay"): - return 1 - - target_lang = command[5:] - tld = "com" - - if target_lang not in self.VALID_LANGUAGES: - self.torchlight.SayPrivate( - player, f"{{darkred}}[TranslateSay]{{default}} Language '{target_lang}' not supported." - ) - return 1 - - asyncio.ensure_future(self.TranslateAndSay(player, target_lang, tld, message[1])) - return 0 - class DECTalk(BaseCommand): async def Say(self, player: Player, message: str) -> int: From cb4f5e7b20606b86f584990172316c31d859da97 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:09:11 +0200 Subject: [PATCH 02/37] some fix --- config/config.json | 8 ++++++++ src/torchlight/Commands.py | 14 ++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/config/config.json b/config/config.json index a7e258c..7dc21b3 100644 --- a/config/config.json +++ b/config/config.json @@ -197,6 +197,10 @@ { "command": "!say", "starts_with": true + }, + { + "command": "!speak", + "starts_with": true } ], "parameters": { @@ -213,6 +217,10 @@ { "command": "!tsay", "starts_with": true + }, + { + "command": "!translate", + "starts_with": true } ], "parameters": { diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 1e29030..3a72dfa 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -1019,10 +1019,16 @@ def HandleSay(self, message: list[str], player: Player) -> tuple[str, str] | Non thisTrigger: str = "" for trigger in self.triggers: - if message[0].lower().startswith(trigger): - thisTrigger = trigger - break - + if isinstance(trigger, tuple): + command_trigger, command_len = trigger + if message[0].lower().startswith(command_trigger): + thisTrigger = command_trigger + break + else: + if message[0].lower().startswith(trigger): + thisTrigger = trigger + break + if not thisTrigger: return None From ac7abaa3a69f2874fca41442699ea2409629b4bc Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:18:13 +0200 Subject: [PATCH 03/37] .. --- src/torchlight/Commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 3a72dfa..21174b3 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -1038,7 +1038,7 @@ def HandleSay(self, message: list[str], player: Player) -> tuple[str, str] | Non language = message[0][len(thisTrigger):] self.logger.debug(f"{language}: {self.VALID_LANGUAGES}") - if len(language) <= 0 or language not in self.VALID_LANGUAGES: + if language not in self.VALID_LANGUAGES: return None return language, tld From 7fe2eb52de0ba533961587e3b4e6c28b928b23d6 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:23:41 +0200 Subject: [PATCH 04/37] aa --- src/torchlight/Commands.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 21174b3..64e64b0 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -1035,10 +1035,14 @@ def HandleSay(self, message: list[str], player: Player) -> tuple[str, str] | Non if message[0].lower() == "!say" or message[0].lower() == "!tsay": return None - language = message[0][len(thisTrigger):] + _language = message[0][len(thisTrigger):] + if not _language: + language = language if language else "en" + else: + language = _language.lower() self.logger.debug(f"{language}: {self.VALID_LANGUAGES}") - if language not in self.VALID_LANGUAGES: + if language and language not in self.VALID_LANGUAGES: return None return language, tld From baa9078f6a5742ddfce7f924dd051e35043db88c Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:29:37 +0200 Subject: [PATCH 05/37] aa --- src/torchlight/Commands.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 64e64b0..7864a88 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -1033,6 +1033,10 @@ def HandleSay(self, message: list[str], player: Player) -> tuple[str, str] | Non return None if message[0].lower() == "!say" or message[0].lower() == "!tsay": + self.torchlight.SayPrivate( + player, + f"Usage: {message[0]}[language] [message], Example: !sayfr Hello World!", + ) return None _language = message[0][len(thisTrigger):] From 2a9501087258022d0d762abd48031d0e23e54d28 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:34:28 +0200 Subject: [PATCH 06/37] a --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6099dc..419029f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,6 @@ on: branches: - 'main' - 'master' - - 'my-master' pull_request: branches: - master From 1c2ff58c325ace90eec4b03fdfbc228747690633 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:25:46 +0200 Subject: [PATCH 07/37] aa --- src/torchlight/Commands.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 7864a88..f11667a 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -1021,13 +1021,12 @@ def HandleSay(self, message: list[str], player: Player) -> tuple[str, str] | Non for trigger in self.triggers: if isinstance(trigger, tuple): command_trigger, command_len = trigger - if message[0].lower().startswith(command_trigger): + if isinstance(command_trigger, str) and message[0].lower().startswith(command_trigger): thisTrigger = command_trigger break - else: - if message[0].lower().startswith(trigger): - thisTrigger = trigger - break + elif isinstance(trigger, str) and message[0].lower().startswith(trigger): + thisTrigger = trigger + break if not thisTrigger: return None @@ -1035,18 +1034,26 @@ def HandleSay(self, message: list[str], player: Player) -> tuple[str, str] | Non if message[0].lower() == "!say" or message[0].lower() == "!tsay": self.torchlight.SayPrivate( player, - f"Usage: {message[0]}[language] [message], Example: !sayfr Hello World!", + f"Usage: {message[0]}[language] [message], Example: {message[0]}fr Hello World!", ) return None - _language = message[0][len(thisTrigger):] + _language = message[0][len(thisTrigger) :] if not _language: language = language if language else "en" else: - language = _language.lower() + if "-" not in _language: + language = _language.lower() + else: + language = _language self.logger.debug(f"{language}: {self.VALID_LANGUAGES}") if language and language not in self.VALID_LANGUAGES: + self.torchlight.SayPrivate( + player, + f"Language '{language}' is not supported. Usage: {thisTrigger}[language] [message], " + f"Example: {thisTrigger}fr Hello World!", + ) return None return language, tld From fd3e4ffaafe11a0472550fa21238ac458119eb1b Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:29:52 +0200 Subject: [PATCH 08/37] bruh --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 419029f..f6099dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ on: branches: - 'main' - 'master' + - 'my-master' pull_request: branches: - master From 03e44dd2260402860f22985a3b3b7ed3f1908e7f Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:36:15 +0200 Subject: [PATCH 09/37] aa --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6099dc..eadf3f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ on: - 'main' - 'master' - 'my-master' + - 'main-dolly8' pull_request: branches: - master From 8eb6a832dd749b8e91761cb3f4826ec0d166f752 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:38:05 +0200 Subject: [PATCH 10/37] aa --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eadf3f1..6f2efec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ on: branches: - master - main + workflow_dispatch: env: REGISTRY: ghcr.io From 1f2df14c61c41da31db4f348b15578d91742b241 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:43:20 +0200 Subject: [PATCH 11/37] Create deploy-image --- .github/workflows/deploy-image | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/deploy-image diff --git a/.github/workflows/deploy-image b/.github/workflows/deploy-image new file mode 100644 index 0000000..fb73d39 --- /dev/null +++ b/.github/workflows/deploy-image @@ -0,0 +1,42 @@ +name: Create and publish a Docker image + +on: push + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file From 587c6b1225e64852b662b5ead46923240521b286 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:17:42 +0200 Subject: [PATCH 12/37] a --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f2efec..833e106 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ on: - 'master' - 'my-master' - 'main-dolly8' + - 'main-dolly15' pull_request: branches: - master From 2ff7011b5504a0169d83ffad5149b89354069e52 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sat, 7 Mar 2026 17:27:50 +0200 Subject: [PATCH 13/37] update --- src/torchlight/Commands.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index f11667a..d9db1c7 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -1031,16 +1031,29 @@ def HandleSay(self, message: list[str], player: Player) -> tuple[str, str] | Non if not thisTrigger: return None - if message[0].lower() == "!say" or message[0].lower() == "!tsay": - self.torchlight.SayPrivate( - player, - f"Usage: {message[0]}[language] [message], Example: {message[0]}fr Hello World!", - ) - return None + force_default: bool = True + for trigger in command_config.get("triggers", {}): + if not isinstance(trigger, dict): + continue + + myTrigger = trigger.get("command", "") + if not myTrigger: + continue + + if thisTrigger == myTrigger: + force_default = trigger.get("force_default", True) + break _language = message[0][len(thisTrigger) :] if not _language: - language = language if language else "en" + if force_default: + language = language if language else "en" + else: + self.torchlight.SayPrivate( + player, + f"Usage: {message[0]}[language] [message], Example: {message[0]}fr Hello World!", + ) + return None else: if "-" not in _language: language = _language.lower() From 5bf62a6c026189a021fb068c93eeff1ba2b9c8d2 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sat, 7 Mar 2026 19:10:54 +0200 Subject: [PATCH 14/37] aa --- src/torchlight/Commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index d9db1c7..f92daaf 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -1029,6 +1029,7 @@ def HandleSay(self, message: list[str], player: Player) -> tuple[str, str] | Non break if not thisTrigger: + self.logger.info("It's being caused from not thisTrigger") return None force_default: bool = True From 5e82a92d6472ccb7d250326c64bf6f9ec303b5a1 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 10:49:32 +0200 Subject: [PATCH 15/37] modifiers per trigger play --- config/config.json | 29 ++++++++++++++++++--- src/torchlight/AudioClip.py | 3 ++- src/torchlight/AudioManager.py | 40 +++++++++++++++++++++++++++++ src/torchlight/Commands.py | 21 ++++++++++----- src/torchlight/FFmpegAudioPlayer.py | 20 ++++++++++++--- 5 files changed, 97 insertions(+), 16 deletions(-) diff --git a/config/config.json b/config/config.json index 7dc21b3..d27a53f 100644 --- a/config/config.json +++ b/config/config.json @@ -29,10 +29,31 @@ "Host": "127.0.0.1", "Port": 27019, "SampleRate": 22050, - "Volume": 1.0, - "Speed": 1.0, - "Pitch": 1.0, - "Proxy": "" + "Proxy": "", + + "AudioParams": + { + "Volume": + { +\ "Default": 1.0, + "Min": 0.0, + "Max": 1.0 + }, + "Speed": + { +\ "Default": 1.0, + "Min": 0.5, + "Max": 2.0 + }, + "Pitch": + { +\ "Default": 1.0, + "Min": 0.5, + "Max": 2.0 + }, + + "backwards": true + } }, "GeoIP": diff --git a/src/torchlight/AudioClip.py b/src/torchlight/AudioClip.py index c9fa4a4..39fddde 100644 --- a/src/torchlight/AudioClip.py +++ b/src/torchlight/AudioClip.py @@ -40,8 +40,9 @@ def Play( volume: float | None = None, speed: float | None = None, pitch: float | None = None, + backwards: bool = False, ) -> bool: - return self.audio_player.PlayURI(self.uri, seconds, *args, volume=volume, speed=speed, pitch=pitch) + return self.audio_player.PlayURI(self.uri, seconds, *args, volume=volume, speed=speed, pitch=pitch, backwards=backwards) def Stop(self) -> bool: return self.audio_player.Stop() diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 77d8563..26dac72 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -21,6 +21,46 @@ def __init__(self, torchlight: Torchlight) -> None: def __del__(self) -> None: self.logger.info("~AudioManager()") + def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool]: + thisConfig = self.torchlight.config.get("AudioParams", {}) + if not thisConfig: + return params + + params: dict[str, float | bool] = {} + for param in thisConfig: + if isinstance(thisConfig[param], dict) and "Default" in thisConfig[param]: + if param in triggerParams and triggerParams[param]: + params[param] = float(triggerParams) + else: + params[param] = float(thisConfig[param]["Default"]) + elif isinstance(thisConfig[param], bool): + params[param] = bool(thisConfig[param]) + + msg_args = msg.split() + for arg in msg_args: + if "=" in arg: + key, value = arg.split("=", 1) + if not key or not value: + continue + + key = key.capitalize() + if key not in thisConfig: + continue + + if isinstance(thisConfig[key], dict): + val: float = params[key] + try: + val = float(value) + except ValueError: + continue + + val = max(thisConfig[key]["Min"], min(val, thisConfig[key]["Max"])) + params[key] = val + + elif isinstance(thisConfig[key], bool): + params[key] = bool(value) + return params + def CheckLimits(self, player: Player) -> bool: level = player.admin.level diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index f92daaf..e45f016 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -613,7 +613,11 @@ async def _func(self, message: list[str], player: Player) -> int: return -1 voice_trigger = message[0].lower() - trigger_number = message[1].lower() + trigger_number = 1 + if message[1]: + firstPart = message[1].split()[0] + if firstPart.isdigit(): + trigger_number = int(firstPart) sound = self.get_sound_path( player=player, @@ -638,13 +642,16 @@ async def _func(self, message: list[str], player: Player) -> int: self.torchlight.SayChat(f"Now playing {{olive}}{self.random_trigger_name}") voice_trigger = self.random_trigger_name - params = cast(dict, self.trigger_manager.voice_triggers[voice_trigger]["parameters"]) - volume = float(params["Volume"]) - speed = float(params["Speed"]) - pitch = float(params["Pitch"]) - self.torchlight.SetPlayerCooldown(player, self.torchlight.config["AntiSpam"]["ChatCooldown"]) - return audio_clip.Play(volume=volume, speed=speed, pitch=pitch) + + params = cast(dict, self.trigger_manager.voice_triggers[voice_trigger]["parameters"]) + modifiers = self.audio_manager.parse_params(params, message[1]) + + volume = modifiers["Volume"] + speed = modifiers["Speed"] + pitch = modifiers["Pitch"] + backwards = modifiers["backwards"] if "backwards" in modifiers else False + return audio_clip.Play(volume=volume, speed=speed, pitch=pitch, backwards=backwards) def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str) -> str | None: level = player.admin.level diff --git a/src/torchlight/FFmpegAudioPlayer.py b/src/torchlight/FFmpegAudioPlayer.py index 8ba88cf..b41d3f7 100644 --- a/src/torchlight/FFmpegAudioPlayer.py +++ b/src/torchlight/FFmpegAudioPlayer.py @@ -29,9 +29,9 @@ def __init__(self, torchlight: Torchlight) -> None: self.host = self.config["Host"] self.port = self.config["Port"] self.sample_rate = float(self.config["SampleRate"]) - self.volume = float(self.config["Volume"]) - self.speed = float(self.config["Speed"]) - self.pitch = float(self.config["Pitch"]) + self.volume = float(self.config.get("AudioParams", {}).get("Volume", {}).get("Default", 1.0)) + self.speed = float(self.config.get("AudioParams", {}).get("Speed", {}).get("Default", 1.0)) + self.pitch = float(self.config.get("AudioParams", {}).get("Pitch", {}).get("Default", 1.0)) self.proxy = self.config.get("Proxy", "") self.started_playing: float | None = None @@ -57,6 +57,7 @@ def PlayURI( volume: float | None = None, speed: float | None = None, pitch: float | None = None, + backwards: bool = False, ) -> bool: if volume is None: volume = self.volume @@ -89,6 +90,17 @@ def PlayURI( self.proxy, ] ) + + modifiers = [ + f"volume={float(volume)}", + f"rubberband=tempo={speed}:pitch={pitch}", + ] + + if backwards: + modifiers.append("areverse") + + modifiers_string = ",".join(modifiers) + ffmpeg_command = [ "/usr/bin/ffmpeg", "-i", @@ -100,7 +112,7 @@ def PlayURI( "-ar", str(int(self.sample_rate)), "-filter:a", - f"volume={float(volume)},rubberband=tempo={speed}:pitch={pitch}", + modifiers_string, "-f", "s16le", "-vn", From 8959e54b6b6fe0a6af15772f4924e617bd5b679c Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:09:32 +0200 Subject: [PATCH 16/37] should be good --- config/config.json | 8 ++++---- src/torchlight/AudioManager.py | 12 ++++++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/config/config.json b/config/config.json index d27a53f..a323bf7 100644 --- a/config/config.json +++ b/config/config.json @@ -35,24 +35,24 @@ { "Volume": { -\ "Default": 1.0, + "Default": 1.0, "Min": 0.0, "Max": 1.0 }, "Speed": { -\ "Default": 1.0, + "Default": 1.0, "Min": 0.5, "Max": 2.0 }, "Pitch": { -\ "Default": 1.0, + "Default": 1.0, "Min": 0.5, "Max": 2.0 }, - "backwards": true + "Backwards": true } }, diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 26dac72..1193bfc 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -22,7 +22,7 @@ def __del__(self) -> None: self.logger.info("~AudioManager()") def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool]: - thisConfig = self.torchlight.config.get("AudioParams", {}) + thisConfig = self.torchlight.config.config.get("AudioParams", {}) if not thisConfig: return params @@ -58,7 +58,15 @@ def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool] params[key] = val elif isinstance(thisConfig[key], bool): - params[key] = bool(value) + if key == "Backwards": + if params[key]: + params[key] = bool(value) + + if "Backwards" in thisConfig: + if isinstance(thisConfig["Backwards"], bool): + if thisConfig["Backwards"] == False and "Backwards" in params and params["Backwards"] == True: + params["Backwards"] = False + return params def CheckLimits(self, player: Player) -> bool: From 10d95d6008f1376e04b5cd12789d76ee419bb416 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:16:54 +0200 Subject: [PATCH 17/37] aa --- src/torchlight/AudioManager.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 1193bfc..2d027f6 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -24,17 +24,17 @@ def __del__(self) -> None: def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool]: thisConfig = self.torchlight.config.config.get("AudioParams", {}) if not thisConfig: - return params + return triggerParams params: dict[str, float | bool] = {} for param in thisConfig: - if isinstance(thisConfig[param], dict) and "Default" in thisConfig[param]: - if param in triggerParams and triggerParams[param]: - params[param] = float(triggerParams) - else: + if param in triggerParams and triggerParams[param]: + params[param] = float(triggerParams) + else: + if isinstance(thisConfig[param], dict) and "Default" in thisConfig[param]: params[param] = float(thisConfig[param]["Default"]) - elif isinstance(thisConfig[param], bool): - params[param] = bool(thisConfig[param]) + elif isinstance(thisConfig[param], bool): + params[param] = bool(thisConfig[param]) msg_args = msg.split() for arg in msg_args: From bc24f42806ecc7e4b6a4dc1afa0cd1620fffac49 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:21:18 +0200 Subject: [PATCH 18/37] adada --- src/torchlight/Commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index e45f016..a42c1a1 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -647,6 +647,7 @@ async def _func(self, message: list[str], player: Player) -> int: params = cast(dict, self.trigger_manager.voice_triggers[voice_trigger]["parameters"]) modifiers = self.audio_manager.parse_params(params, message[1]) + self.logger.info(modifiers) volume = modifiers["Volume"] speed = modifiers["Speed"] pitch = modifiers["Pitch"] From a64ce47bb83e843181b55aca111a50ff0a6a9c92 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:46:15 +0200 Subject: [PATCH 19/37] adada --- src/torchlight/AudioManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 2d027f6..62b3668 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -22,7 +22,7 @@ def __del__(self) -> None: self.logger.info("~AudioManager()") def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool]: - thisConfig = self.torchlight.config.config.get("AudioParams", {}) + thisConfig = self.torchlight.config.config.get("VoiceServer", {}).get("AudioParams", {}) if not thisConfig: return triggerParams From 2e6e23a6452022dba20208607f73a1a49fd8e410 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:50:47 +0200 Subject: [PATCH 20/37] aaaaaaaaaaaaaaaaaaaaaaa --- src/torchlight/AudioManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 62b3668..64c30ef 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -29,7 +29,7 @@ def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool] params: dict[str, float | bool] = {} for param in thisConfig: if param in triggerParams and triggerParams[param]: - params[param] = float(triggerParams) + params[param] = float(triggerParams[param]) else: if isinstance(thisConfig[param], dict) and "Default" in thisConfig[param]: params[param] = float(thisConfig[param]["Default"]) From 35faa07c4c0d4b8d4444ac1c14a0661898cc21eb Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:49:38 +0200 Subject: [PATCH 21/37] aaa --- src/torchlight/FFmpegAudioPlayer.py | 95 ++++++++++++++++------------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/src/torchlight/FFmpegAudioPlayer.py b/src/torchlight/FFmpegAudioPlayer.py index b41d3f7..a8bac59 100644 --- a/src/torchlight/FFmpegAudioPlayer.py +++ b/src/torchlight/FFmpegAudioPlayer.py @@ -68,32 +68,9 @@ def PlayURI( if pitch is None: pitch = self.pitch - curl_command = [ - "/usr/bin/curl", - "--silent", - "--show-error", - "--connect-timeout", - "1", - "--retry", - "2", - "--retry-delay", - "1", - "--output", - "-", - "-L", - uri, - ] - if self.proxy: - curl_command.extend( - [ - "-x", - self.proxy, - ] - ) - modifiers = [ f"volume={float(volume)}", - f"rubberband=tempo={speed}:pitch={pitch}", + f"rubberband=tempo={float(speed)}:pitch={float(pitch)}", ] if backwards: @@ -101,10 +78,57 @@ def PlayURI( modifiers_string = ",".join(modifiers) - ffmpeg_command = [ - "/usr/bin/ffmpeg", - "-i", - "pipe:0", + #ٌ Refactoring how curl and ffmpeg work together so it accepts reversed sounds (from local) + is_remote = uri.startswith(("http://", "https://")) + + if backwards and is_remote: + self.logger.error("Backwards playback is only supported for local files: %s", uri) + return False + + ffmpeg_command: list[str] = ["/usr/bin/ffmpeg", "-v", "error"] + + if position is not None: + pos_str = str(datetime.timedelta(seconds=position)) + ffmpeg_command.extend(["-ss", pos_str]) + self.position = position + + curl_command: list[str] | None = None + + # If the playback uri is not a local file and is a VALID URL, then use curl + ffmpeg together + if is_remote and not backwards: + curl_command = [ + "/usr/bin/curl", + "--silent", + "--show-error", + "--connect-timeout", + "1", + "--retry", + "2", + "--retry-delay", + "1", + "--output", + "-", + "-L", + uri, + ] + + if self.proxy: + curl_command.extend(["-x", self.proxy]) + + ffmpeg_command.extend([ + "-i", + "pipe:0", + ]) + # If the uri is a local file, then play it with ffmpeg only (let it access the URI) + else: + local_path = uri[7:] if uri.startswith("file://") else uri + + ffmpeg_command.extend([ + "-i", + local_path, + ]) + + ffmpeg_command.extend([ "-acodec", "pcm_s16le", "-ac", @@ -118,20 +142,7 @@ def PlayURI( "-vn", *args, "-", - ] - - if position is not None: - pos_str = str(datetime.timedelta(seconds=position)) - ffmpeg_command.extend( - [ - "-ss", - pos_str, - ] - ) - self.position = position - - self.logger.debug(curl_command) - self.logger.debug(ffmpeg_command) + ]) self.playing = True self.uri = uri From 1c947f5319ec6b9c8749ef06f9ce5a64e56cc655 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:52:22 +0200 Subject: [PATCH 22/37] adada --- src/torchlight/AudioManager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 64c30ef..4ab9be2 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -37,6 +37,7 @@ def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool] params[param] = bool(thisConfig[param]) msg_args = msg.split() + is_backwards: bool = False for arg in msg_args: if "=" in arg: key, value = arg.split("=", 1) @@ -61,11 +62,14 @@ def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool] if key == "Backwards": if params[key]: params[key] = bool(value) + is_backwards = params[key] if "Backwards" in thisConfig: if isinstance(thisConfig["Backwards"], bool): - if thisConfig["Backwards"] == False and "Backwards" in params and params["Backwards"] == True: + if thisConfig["Backwards"] == False: params["Backwards"] = False + elif is_backwards == True: + params["Backwards"] = True return params From 7683035d079460d2bdcfff925eaa7a98f76b8549 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:01:19 +0200 Subject: [PATCH 23/37] zczcz --- src/torchlight/FFmpegAudioPlayer.py | 53 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/torchlight/FFmpegAudioPlayer.py b/src/torchlight/FFmpegAudioPlayer.py index a8bac59..dfbddb7 100644 --- a/src/torchlight/FFmpegAudioPlayer.py +++ b/src/torchlight/FFmpegAudioPlayer.py @@ -295,33 +295,48 @@ async def _read_stream(self, stream: StreamReader | None, writer: StreamWriter) raise exc # @profile - async def _stream_subprocess(self, curl_command: list[str], ffmpeg_command: list[str]) -> None: + async def _stream_subprocess( + self, + curl_command: list[str] | None, + ffmpeg_command: list[str], + ) -> None: if not self.playing: return try: _, self.writer = await asyncio.open_connection(self.host, self.port) - self.curl_process = await asyncio.create_subprocess_exec( - *curl_command, - stdout=asyncio.subprocess.PIPE, - ) - - self.ffmpeg_process = await asyncio.create_subprocess_exec( - *ffmpeg_command, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.DEVNULL, + if curl_command is not None: + self.curl_process = await asyncio.create_subprocess_exec( + *curl_command, + stdout=asyncio.subprocess.PIPE, + ) + + self.ffmpeg_process = await asyncio.create_subprocess_exec( + *ffmpeg_command, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL, + ) + + asyncio.create_task(self._wait_for_process_exit(self.curl_process)) + asyncio.create_task( + self._write_stream(self.curl_process.stdout, self.ffmpeg_process.stdin) + ) + else: + self.curl_process = None + + self.ffmpeg_process = await asyncio.create_subprocess_exec( + *ffmpeg_command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL, + ) + + asyncio.create_task( + self._read_stream(self.ffmpeg_process.stdout, self.writer) ) - asyncio.create_task(self._wait_for_process_exit(self.curl_process)) - - asyncio.create_task(self._write_stream(self.curl_process.stdout, self.ffmpeg_process.stdin)) - - asyncio.create_task(self._read_stream(self.ffmpeg_process.stdout, self.writer)) - - if self.ffmpeg_process is not None: - await self.ffmpeg_process.wait() + await self.ffmpeg_process.wait() if self.seconds == 0.0: self.Stop() From beec06f137f147a32b8b79ef0acdbbdc930b80d8 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:19:34 +0200 Subject: [PATCH 24/37] aadada --- src/torchlight/AudioManager.py | 4 ++-- src/torchlight/FFmpegAudioPlayer.py | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 4ab9be2..7444ff3 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -68,8 +68,8 @@ def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool] if isinstance(thisConfig["Backwards"], bool): if thisConfig["Backwards"] == False: params["Backwards"] = False - elif is_backwards == True: - params["Backwards"] = True + else: + params["Backwards"] = is_backwards return params diff --git a/src/torchlight/FFmpegAudioPlayer.py b/src/torchlight/FFmpegAudioPlayer.py index dfbddb7..d525ebc 100644 --- a/src/torchlight/FFmpegAudioPlayer.py +++ b/src/torchlight/FFmpegAudioPlayer.py @@ -68,14 +68,17 @@ def PlayURI( if pitch is None: pitch = self.pitch - modifiers = [ - f"volume={float(volume)}", - f"rubberband=tempo={float(speed)}:pitch={float(pitch)}", - ] + modifiers = [] + modifiers = [] if backwards: modifiers.append("areverse") + modifiers.extend([ + f"rubberband=tempo={float(speed)}:pitch={float(pitch)}", + f"volume={float(volume)}", + ]) + modifiers_string = ",".join(modifiers) #ٌ Refactoring how curl and ffmpeg work together so it accepts reversed sounds (from local) From c78396a6469e215302597fdab84eda8345335934 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:28:41 +0200 Subject: [PATCH 25/37] yes --- src/torchlight/AudioManager.py | 17 +-- src/torchlight/Commands.py | 3 +- src/torchlight/FFmpegAudioPlayer.py | 161 +++++++++++----------------- 3 files changed, 62 insertions(+), 119 deletions(-) diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 7444ff3..9ff7ca1 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -21,7 +21,7 @@ def __init__(self, torchlight: Torchlight) -> None: def __del__(self) -> None: self.logger.info("~AudioManager()") - def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool]: + def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float]: thisConfig = self.torchlight.config.config.get("VoiceServer", {}).get("AudioParams", {}) if not thisConfig: return triggerParams @@ -33,8 +33,6 @@ def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool] else: if isinstance(thisConfig[param], dict) and "Default" in thisConfig[param]: params[param] = float(thisConfig[param]["Default"]) - elif isinstance(thisConfig[param], bool): - params[param] = bool(thisConfig[param]) msg_args = msg.split() is_backwards: bool = False @@ -58,19 +56,6 @@ def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float | bool] val = max(thisConfig[key]["Min"], min(val, thisConfig[key]["Max"])) params[key] = val - elif isinstance(thisConfig[key], bool): - if key == "Backwards": - if params[key]: - params[key] = bool(value) - is_backwards = params[key] - - if "Backwards" in thisConfig: - if isinstance(thisConfig["Backwards"], bool): - if thisConfig["Backwards"] == False: - params["Backwards"] = False - else: - params["Backwards"] = is_backwards - return params def CheckLimits(self, player: Player) -> bool: diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index a42c1a1..003971a 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -651,8 +651,7 @@ async def _func(self, message: list[str], player: Player) -> int: volume = modifiers["Volume"] speed = modifiers["Speed"] pitch = modifiers["Pitch"] - backwards = modifiers["backwards"] if "backwards" in modifiers else False - return audio_clip.Play(volume=volume, speed=speed, pitch=pitch, backwards=backwards) + return audio_clip.Play(volume=volume, speed=speed, pitch=pitch) def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str) -> str | None: level = player.admin.level diff --git a/src/torchlight/FFmpegAudioPlayer.py b/src/torchlight/FFmpegAudioPlayer.py index d525ebc..39eb5fc 100644 --- a/src/torchlight/FFmpegAudioPlayer.py +++ b/src/torchlight/FFmpegAudioPlayer.py @@ -57,7 +57,6 @@ def PlayURI( volume: float | None = None, speed: float | None = None, pitch: float | None = None, - backwards: bool = False, ) -> bool: if volume is None: volume = self.volume @@ -68,70 +67,32 @@ def PlayURI( if pitch is None: pitch = self.pitch - modifiers = [] - modifiers = [] - - if backwards: - modifiers.append("areverse") - - modifiers.extend([ - f"rubberband=tempo={float(speed)}:pitch={float(pitch)}", - f"volume={float(volume)}", - ]) - - modifiers_string = ",".join(modifiers) - - #ٌ Refactoring how curl and ffmpeg work together so it accepts reversed sounds (from local) - is_remote = uri.startswith(("http://", "https://")) - - if backwards and is_remote: - self.logger.error("Backwards playback is only supported for local files: %s", uri) - return False - - ffmpeg_command: list[str] = ["/usr/bin/ffmpeg", "-v", "error"] - - if position is not None: - pos_str = str(datetime.timedelta(seconds=position)) - ffmpeg_command.extend(["-ss", pos_str]) - self.position = position - - curl_command: list[str] | None = None - - # If the playback uri is not a local file and is a VALID URL, then use curl + ffmpeg together - if is_remote and not backwards: - curl_command = [ - "/usr/bin/curl", - "--silent", - "--show-error", - "--connect-timeout", - "1", - "--retry", - "2", - "--retry-delay", - "1", - "--output", - "-", - "-L", - uri, - ] - - if self.proxy: - curl_command.extend(["-x", self.proxy]) - - ffmpeg_command.extend([ - "-i", - "pipe:0", - ]) - # If the uri is a local file, then play it with ffmpeg only (let it access the URI) - else: - local_path = uri[7:] if uri.startswith("file://") else uri - - ffmpeg_command.extend([ - "-i", - local_path, - ]) - - ffmpeg_command.extend([ + curl_command = [ + "/usr/bin/curl", + "--silent", + "--show-error", + "--connect-timeout", + "1", + "--retry", + "2", + "--retry-delay", + "1", + "--output", + "-", + "-L", + uri, + ] + if self.proxy: + curl_command.extend( + [ + "-x", + self.proxy, + ] + ) + ffmpeg_command = [ + "/usr/bin/ffmpeg", + "-i", + "pipe:0", "-acodec", "pcm_s16le", "-ac", @@ -139,13 +100,26 @@ def PlayURI( "-ar", str(int(self.sample_rate)), "-filter:a", - modifiers_string, + f"volume={volume},rubberband=tempo={speed}:pitch={pitch}", "-f", "s16le", "-vn", *args, "-", - ]) + ] + + if position is not None: + pos_str = str(datetime.timedelta(seconds=position)) + ffmpeg_command.extend( + [ + "-ss", + pos_str, + ] + ) + self.position = position + + self.logger.debug(curl_command) + self.logger.debug(ffmpeg_command) self.playing = True self.uri = uri @@ -298,48 +272,33 @@ async def _read_stream(self, stream: StreamReader | None, writer: StreamWriter) raise exc # @profile - async def _stream_subprocess( - self, - curl_command: list[str] | None, - ffmpeg_command: list[str], - ) -> None: + async def _stream_subprocess(self, curl_command: list[str], ffmpeg_command: list[str]) -> None: if not self.playing: return try: _, self.writer = await asyncio.open_connection(self.host, self.port) - if curl_command is not None: - self.curl_process = await asyncio.create_subprocess_exec( - *curl_command, - stdout=asyncio.subprocess.PIPE, - ) - - self.ffmpeg_process = await asyncio.create_subprocess_exec( - *ffmpeg_command, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.DEVNULL, - ) - - asyncio.create_task(self._wait_for_process_exit(self.curl_process)) - asyncio.create_task( - self._write_stream(self.curl_process.stdout, self.ffmpeg_process.stdin) - ) - else: - self.curl_process = None - - self.ffmpeg_process = await asyncio.create_subprocess_exec( - *ffmpeg_command, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.DEVNULL, - ) - - asyncio.create_task( - self._read_stream(self.ffmpeg_process.stdout, self.writer) + self.curl_process = await asyncio.create_subprocess_exec( + *curl_command, + stdout=asyncio.subprocess.PIPE, + ) + + self.ffmpeg_process = await asyncio.create_subprocess_exec( + *ffmpeg_command, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL, ) - await self.ffmpeg_process.wait() + asyncio.create_task(self._wait_for_process_exit(self.curl_process)) + + asyncio.create_task(self._write_stream(self.curl_process.stdout, self.ffmpeg_process.stdin)) + + asyncio.create_task(self._read_stream(self.ffmpeg_process.stdout, self.writer)) + + if self.ffmpeg_process is not None: + await self.ffmpeg_process.wait() if self.seconds == 0.0: self.Stop() From ee417283e9f74ba422eee3c6d48979c7da6f7d4e Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:38:32 +0200 Subject: [PATCH 26/37] feat(triggers): Add sound modifiers for voicetriggers --- config/config.json | 27 +++++++++++++++++++++---- src/torchlight/AudioManager.py | 36 ++++++++++++++++++++++++++++++++++ src/torchlight/Commands.py | 19 ++++++++++++------ 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/config/config.json b/config/config.json index 766c77d..49238e5 100644 --- a/config/config.json +++ b/config/config.json @@ -29,10 +29,29 @@ "Host": "127.0.0.1", "Port": 27019, "SampleRate": 22050, - "Volume": 1.0, - "Speed": 1.0, - "Pitch": 1.0, - "Proxy": "" + "Proxy": "", + + "AudioParams": + { + "Volume": + { + "Default": 1.0, + "Min": 0.0, + "Max": 1.0 + }, + "Speed": + { + "Default": 1.0, + "Min": 0.5, + "Max": 2.0 + }, + "Pitch": + { + "Default": 1.0, + "Min": 0.5, + "Max": 2.0 + } + } }, "GeoIP": diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 77d8563..86d0da9 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -21,6 +21,42 @@ def __init__(self, torchlight: Torchlight) -> None: def __del__(self) -> None: self.logger.info("~AudioManager()") + def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float]: + thisConfig = self.torchlight.config.config.get("VoiceServer", {}).get("AudioParams", {}) + if not thisConfig: + return triggerParams + + params: dict[str, float | bool] = {} + for param in thisConfig: + if param in triggerParams and triggerParams[param]: + params[param] = float(triggerParams[param]) + else: + if isinstance(thisConfig[param], dict) and "Default" in thisConfig[param]: + params[param] = float(thisConfig[param]["Default"]) + + msg_args = msg.split() + for arg in msg_args: + if "=" in arg: + key, value = arg.split("=", 1) + if not key or not value: + continue + + key = key.capitalize() + if key not in thisConfig: + continue + + if isinstance(thisConfig[key], dict): + val: float = params[key] + try: + val = float(value) + except ValueError: + continue + + val = max(thisConfig[key]["Min"], min(val, thisConfig[key]["Max"])) + params[key] = val + + return params + def CheckLimits(self, player: Player) -> bool: level = player.admin.level diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index d9db1c7..5354b0f 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -613,7 +613,11 @@ async def _func(self, message: list[str], player: Player) -> int: return -1 voice_trigger = message[0].lower() - trigger_number = message[1].lower() + trigger_number = 1 + if message[1]: + firstPart = message[1].split()[0] + if firstPart.isdigit(): + trigger_number = int(firstPart) sound = self.get_sound_path( player=player, @@ -638,12 +642,15 @@ async def _func(self, message: list[str], player: Player) -> int: self.torchlight.SayChat(f"Now playing {{olive}}{self.random_trigger_name}") voice_trigger = self.random_trigger_name - params = cast(dict, self.trigger_manager.voice_triggers[voice_trigger]["parameters"]) - volume = float(params["Volume"]) - speed = float(params["Speed"]) - pitch = float(params["Pitch"]) - self.torchlight.SetPlayerCooldown(player, self.torchlight.config["AntiSpam"]["ChatCooldown"]) + + params = cast(dict, self.trigger_manager.voice_triggers[voice_trigger]["parameters"]) + modifiers = self.audio_manager.parse_params(params, message[1]) + + self.logger.info(modifiers) + volume = modifiers["Volume"] + speed = modifiers["Speed"] + pitch = modifiers["Pitch"] return audio_clip.Play(volume=volume, speed=speed, pitch=pitch) def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str) -> str | None: From faaa2acf3867b0fb7d8822c0fb197707f99082cb Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:40:52 +0200 Subject: [PATCH 27/37] fix linting and mypy issues --- src/torchlight/AudioManager.py | 2 +- src/torchlight/Commands.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 86d0da9..16872b4 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -26,7 +26,7 @@ def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float]: if not thisConfig: return triggerParams - params: dict[str, float | bool] = {} + params: dict[str, float] = {} for param in thisConfig: if param in triggerParams and triggerParams[param]: params[param] = float(triggerParams[param]) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 5354b0f..06bf704 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -646,7 +646,7 @@ async def _func(self, message: list[str], player: Player) -> int: params = cast(dict, self.trigger_manager.voice_triggers[voice_trigger]["parameters"]) modifiers = self.audio_manager.parse_params(params, message[1]) - + self.logger.info(modifiers) volume = modifiers["Volume"] speed = modifiers["Speed"] From 0a932a5c65655711fde94cfa77c285bd567c8237 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:45:07 +0200 Subject: [PATCH 28/37] mypy... --- src/torchlight/Commands.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 06bf704..2031c4a 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -613,7 +613,7 @@ async def _func(self, message: list[str], player: Player) -> int: return -1 voice_trigger = message[0].lower() - trigger_number = 1 + trigger_number: int | None = None if message[1]: firstPart = message[1].split()[0] if firstPart.isdigit(): @@ -653,7 +653,7 @@ async def _func(self, message: list[str], player: Player) -> int: pitch = modifiers["Pitch"] return audio_clip.Play(volume=volume, speed=speed, pitch=pitch) - def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str) -> str | None: + def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: int | None) -> str | None: level = player.admin.level if voice_trigger[0] != "!" and level < self.torchlight.config["Command"]["VoiceTriggerReserved"]["level"]: @@ -663,14 +663,9 @@ def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str sounds = self.trigger_manager.voice_triggers[voice_trigger]["sounds"] - try: - num = int(trigger_number) - except ValueError: - num = None - if isinstance(sounds, list): - if num and num > 0 and num <= len(sounds): - sound = sounds[num - 1] + if trigger_number and trigger_number > 0 and trigger_number <= len(sounds): + sound = sounds[trigger_number - 1] elif trigger_number: searching = trigger_number.startswith("?") @@ -702,7 +697,7 @@ def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str "Multiple matches: {}".format(", ".join(mlist)), ) - if not sound and not num: + if not sound and not trigger_number: if not searching: self.torchlight.SayPrivate( player, @@ -711,10 +706,10 @@ def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str self.torchlight.SayPrivate(player, ", ".join(names)) return None - elif num: + elif trigger_number: self.torchlight.SayPrivate( player, - f"Number {num} is out of bounds, max {len(sounds)}.", + f"Number {trigger_number} is out of bounds, max {len(sounds)}.", ) return None @@ -730,7 +725,7 @@ class Random(VoiceTrigger): def _setup(self) -> None: self.logger.debug(sys._getframe().f_code.co_name) - def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str) -> str | None: + def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: int | None) -> str | None: trigger_name, trigger = secrets.choice(list(self.trigger_manager.voice_triggers.items())) self.random_trigger_name = trigger_name From f3a43dddac1d5c7228a021aa873a13b791ddee86 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:48:19 +0200 Subject: [PATCH 29/37] aa --- src/torchlight/Commands.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 2031c4a..c824a42 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -613,11 +613,11 @@ async def _func(self, message: list[str], player: Player) -> int: return -1 voice_trigger = message[0].lower() - trigger_number: int | None = None + trigger_number: str = "" if message[1]: firstPart = message[1].split()[0] - if firstPart.isdigit(): - trigger_number = int(firstPart) + if firstPart.isdigit() or firstPart.startswith("?"): + trigger_number = firstPart sound = self.get_sound_path( player=player, @@ -663,9 +663,14 @@ def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: int sounds = self.trigger_manager.voice_triggers[voice_trigger]["sounds"] + try: + num = int(trigger_number) + except ValueError: + num = None + if isinstance(sounds, list): - if trigger_number and trigger_number > 0 and trigger_number <= len(sounds): - sound = sounds[trigger_number - 1] + if num and num > 0 and num <= len(sounds): + sound = sounds[num - 1] elif trigger_number: searching = trigger_number.startswith("?") @@ -697,7 +702,7 @@ def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: int "Multiple matches: {}".format(", ".join(mlist)), ) - if not sound and not trigger_number: + if not sound and not num: if not searching: self.torchlight.SayPrivate( player, @@ -706,10 +711,10 @@ def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: int self.torchlight.SayPrivate(player, ", ".join(names)) return None - elif trigger_number: + elif num: self.torchlight.SayPrivate( player, - f"Number {trigger_number} is out of bounds, max {len(sounds)}.", + f"Number {num} is out of bounds, max {len(sounds)}.", ) return None @@ -725,7 +730,7 @@ class Random(VoiceTrigger): def _setup(self) -> None: self.logger.debug(sys._getframe().f_code.co_name) - def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: int | None) -> str | None: + def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str) -> str | None: trigger_name, trigger = secrets.choice(list(self.trigger_manager.voice_triggers.items())) self.random_trigger_name = trigger_name From cca84befb8291ab6e2fcd9c8b8032f30bf313940 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 18:50:34 +0200 Subject: [PATCH 30/37] ... --- src/torchlight/Commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index c824a42..bba6a14 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -653,7 +653,7 @@ async def _func(self, message: list[str], player: Player) -> int: pitch = modifiers["Pitch"] return audio_clip.Play(volume=volume, speed=speed, pitch=pitch) - def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: int | None) -> str | None: + def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str) -> str | None: level = player.admin.level if voice_trigger[0] != "!" and level < self.torchlight.config["Command"]["VoiceTriggerReserved"]["level"]: From 0c35c9bfe89560d5b48eff0e50251114a32fde22 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:11:49 +0200 Subject: [PATCH 31/37] Remove logging of audio modifiers Remove logging of modifiers before playing audio clip. --- src/torchlight/Commands.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index bba6a14..ca4d1f9 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -647,7 +647,6 @@ async def _func(self, message: list[str], player: Player) -> int: params = cast(dict, self.trigger_manager.voice_triggers[voice_trigger]["parameters"]) modifiers = self.audio_manager.parse_params(params, message[1]) - self.logger.info(modifiers) volume = modifiers["Volume"] speed = modifiers["Speed"] pitch = modifiers["Pitch"] From 6fefbd33d4179eb0d8832e064060a8758a069698 Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:24:27 +0200 Subject: [PATCH 32/37] should be good --- src/torchlight/AudioManager.py | 26 +++++++++++++------------- src/torchlight/Commands.py | 6 +++--- src/torchlight/TriggerManager.py | 7 ++++--- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 16872b4..16070ad 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -21,18 +21,18 @@ def __init__(self, torchlight: Torchlight) -> None: def __del__(self) -> None: self.logger.info("~AudioManager()") - def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float]: - thisConfig = self.torchlight.config.config.get("VoiceServer", {}).get("AudioParams", {}) - if not thisConfig: - return triggerParams + def parse_params(self, trigger_params: dict, msg: str) -> dict[str, float]: + this_config = self.torchlight.config.config.get("VoiceServer", {}).get("AudioParams", {}) + if not this_config: + return trigger_params params: dict[str, float] = {} - for param in thisConfig: - if param in triggerParams and triggerParams[param]: - params[param] = float(triggerParams[param]) + for param in this_config: + if param in trigger_params and trigger_params[param] is not None: + params[param] = float(trigger_params[param]) else: - if isinstance(thisConfig[param], dict) and "Default" in thisConfig[param]: - params[param] = float(thisConfig[param]["Default"]) + if isinstance(this_config[param], dict) and "Default" in this_config[param]: + params[param] = float(this_config[param]["Default"]) msg_args = msg.split() for arg in msg_args: @@ -42,17 +42,17 @@ def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float]: continue key = key.capitalize() - if key not in thisConfig: + if key not in this_config: continue - if isinstance(thisConfig[key], dict): - val: float = params[key] + if isinstance(this_config[key], dict): + val: float = 1.0 try: val = float(value) except ValueError: continue - val = max(thisConfig[key]["Min"], min(val, thisConfig[key]["Max"])) + val = max(this_config[key]["Min"], min(val, this_config[key]["Max"])) params[key] = val return params diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index 1af6b8b..fa26404 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -615,9 +615,9 @@ async def _func(self, message: list[str], player: Player) -> int: voice_trigger = message[0].lower() trigger_number: str = "" if message[1]: - firstPart = message[1].split()[0] - if firstPart.isdigit() or firstPart.startswith("?"): - trigger_number = firstPart + first_part = message[1].split()[0] + if first_part.isdigit() or first_part.startswith("?"): + trigger_number = first_part sound = self.get_sound_path( player=player, diff --git a/src/torchlight/TriggerManager.py b/src/torchlight/TriggerManager.py index bf01776..e86ae6f 100644 --- a/src/torchlight/TriggerManager.py +++ b/src/torchlight/TriggerManager.py @@ -25,11 +25,12 @@ def __init__( def Load(self) -> None: self.logger.info(f"Loading triggers from {self.config_filepath}") + voice_server_params = self.config.config.get("VoiceServer", {}).get("AudioParams", {}) with open(self.config_filepath) as fp: default_parameters = { - "Volume": float(self.config.config.get("VoiceServer", {}).get("Volume", 1.0)), - "Speed": float(self.config.config.get("VoiceServer", {}).get("Speed", 1.0)), - "Pitch": float(self.config.config.get("VoiceServer", {}).get("Pitch", 1.0)), + "Volume": float(voice_server_params.get("Volume", {}).get("Default", 1.0)), + "Speed": float(voice_server_params.get("Speed", {}).get("Default", 1.0)), + "Pitch": float(voice_server_params.get("Pitch", {}).get("Default", 1.0)), } self.triggers_dict = json.load(fp, object_pairs_hook=OrderedDict) From d9cc35a68a33de258f6cb0e602d109f4bbb6c15c Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:28:53 +0200 Subject: [PATCH 33/37] Revert "Merge branch 'my-master' into main-dolly18" This reverts commit 128415a205ba28d1acb01d71e0edaca5e7f79660, reversing changes made to 0c35c9bfe89560d5b48eff0e50251114a32fde22. --- .github/workflows/ci.yml | 4 --- .github/workflows/deploy-image | 42 ----------------------------- src/torchlight/AudioClip.py | 3 +-- src/torchlight/AudioManager.py | 26 +++++++++--------- src/torchlight/Commands.py | 7 +++-- src/torchlight/FFmpegAudioPlayer.py | 8 +++--- src/torchlight/TriggerManager.py | 7 +++-- 7 files changed, 24 insertions(+), 73 deletions(-) delete mode 100644 .github/workflows/deploy-image diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 833e106..419029f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,14 +16,10 @@ on: branches: - 'main' - 'master' - - 'my-master' - - 'main-dolly8' - - 'main-dolly15' pull_request: branches: - master - main - workflow_dispatch: env: REGISTRY: ghcr.io diff --git a/.github/workflows/deploy-image b/.github/workflows/deploy-image deleted file mode 100644 index fb73d39..0000000 --- a/.github/workflows/deploy-image +++ /dev/null @@ -1,42 +0,0 @@ -name: Create and publish a Docker image - -on: push - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build-and-push-image: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=sha - - - name: Build and push Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/src/torchlight/AudioClip.py b/src/torchlight/AudioClip.py index 39fddde..c9fa4a4 100644 --- a/src/torchlight/AudioClip.py +++ b/src/torchlight/AudioClip.py @@ -40,9 +40,8 @@ def Play( volume: float | None = None, speed: float | None = None, pitch: float | None = None, - backwards: bool = False, ) -> bool: - return self.audio_player.PlayURI(self.uri, seconds, *args, volume=volume, speed=speed, pitch=pitch, backwards=backwards) + return self.audio_player.PlayURI(self.uri, seconds, *args, volume=volume, speed=speed, pitch=pitch) def Stop(self) -> bool: return self.audio_player.Stop() diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 16070ad..16872b4 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -21,18 +21,18 @@ def __init__(self, torchlight: Torchlight) -> None: def __del__(self) -> None: self.logger.info("~AudioManager()") - def parse_params(self, trigger_params: dict, msg: str) -> dict[str, float]: - this_config = self.torchlight.config.config.get("VoiceServer", {}).get("AudioParams", {}) - if not this_config: - return trigger_params + def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float]: + thisConfig = self.torchlight.config.config.get("VoiceServer", {}).get("AudioParams", {}) + if not thisConfig: + return triggerParams params: dict[str, float] = {} - for param in this_config: - if param in trigger_params and trigger_params[param] is not None: - params[param] = float(trigger_params[param]) + for param in thisConfig: + if param in triggerParams and triggerParams[param]: + params[param] = float(triggerParams[param]) else: - if isinstance(this_config[param], dict) and "Default" in this_config[param]: - params[param] = float(this_config[param]["Default"]) + if isinstance(thisConfig[param], dict) and "Default" in thisConfig[param]: + params[param] = float(thisConfig[param]["Default"]) msg_args = msg.split() for arg in msg_args: @@ -42,17 +42,17 @@ def parse_params(self, trigger_params: dict, msg: str) -> dict[str, float]: continue key = key.capitalize() - if key not in this_config: + if key not in thisConfig: continue - if isinstance(this_config[key], dict): - val: float = 1.0 + if isinstance(thisConfig[key], dict): + val: float = params[key] try: val = float(value) except ValueError: continue - val = max(this_config[key]["Min"], min(val, this_config[key]["Max"])) + val = max(thisConfig[key]["Min"], min(val, thisConfig[key]["Max"])) params[key] = val return params diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index fa26404..ca4d1f9 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -615,9 +615,9 @@ async def _func(self, message: list[str], player: Player) -> int: voice_trigger = message[0].lower() trigger_number: str = "" if message[1]: - first_part = message[1].split()[0] - if first_part.isdigit() or first_part.startswith("?"): - trigger_number = first_part + firstPart = message[1].split()[0] + if firstPart.isdigit() or firstPart.startswith("?"): + trigger_number = firstPart sound = self.get_sound_path( player=player, @@ -650,7 +650,6 @@ async def _func(self, message: list[str], player: Player) -> int: volume = modifiers["Volume"] speed = modifiers["Speed"] pitch = modifiers["Pitch"] - return audio_clip.Play(volume=volume, speed=speed, pitch=pitch) def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str) -> str | None: diff --git a/src/torchlight/FFmpegAudioPlayer.py b/src/torchlight/FFmpegAudioPlayer.py index 39eb5fc..8ba88cf 100644 --- a/src/torchlight/FFmpegAudioPlayer.py +++ b/src/torchlight/FFmpegAudioPlayer.py @@ -29,9 +29,9 @@ def __init__(self, torchlight: Torchlight) -> None: self.host = self.config["Host"] self.port = self.config["Port"] self.sample_rate = float(self.config["SampleRate"]) - self.volume = float(self.config.get("AudioParams", {}).get("Volume", {}).get("Default", 1.0)) - self.speed = float(self.config.get("AudioParams", {}).get("Speed", {}).get("Default", 1.0)) - self.pitch = float(self.config.get("AudioParams", {}).get("Pitch", {}).get("Default", 1.0)) + self.volume = float(self.config["Volume"]) + self.speed = float(self.config["Speed"]) + self.pitch = float(self.config["Pitch"]) self.proxy = self.config.get("Proxy", "") self.started_playing: float | None = None @@ -100,7 +100,7 @@ def PlayURI( "-ar", str(int(self.sample_rate)), "-filter:a", - f"volume={volume},rubberband=tempo={speed}:pitch={pitch}", + f"volume={float(volume)},rubberband=tempo={speed}:pitch={pitch}", "-f", "s16le", "-vn", diff --git a/src/torchlight/TriggerManager.py b/src/torchlight/TriggerManager.py index e86ae6f..bf01776 100644 --- a/src/torchlight/TriggerManager.py +++ b/src/torchlight/TriggerManager.py @@ -25,12 +25,11 @@ def __init__( def Load(self) -> None: self.logger.info(f"Loading triggers from {self.config_filepath}") - voice_server_params = self.config.config.get("VoiceServer", {}).get("AudioParams", {}) with open(self.config_filepath) as fp: default_parameters = { - "Volume": float(voice_server_params.get("Volume", {}).get("Default", 1.0)), - "Speed": float(voice_server_params.get("Speed", {}).get("Default", 1.0)), - "Pitch": float(voice_server_params.get("Pitch", {}).get("Default", 1.0)), + "Volume": float(self.config.config.get("VoiceServer", {}).get("Volume", 1.0)), + "Speed": float(self.config.config.get("VoiceServer", {}).get("Speed", 1.0)), + "Pitch": float(self.config.config.get("VoiceServer", {}).get("Pitch", 1.0)), } self.triggers_dict = json.load(fp, object_pairs_hook=OrderedDict) From c8d8bdfd5353b441cb1c74a9fbda48f130a5663c Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:34:19 +0200 Subject: [PATCH 34/37] ... --- src/torchlight/AudioManager.py | 26 +++++++++++++------------- src/torchlight/Commands.py | 7 ++++--- src/torchlight/TriggerManager.py | 7 ++++--- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 16872b4..16070ad 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -21,18 +21,18 @@ def __init__(self, torchlight: Torchlight) -> None: def __del__(self) -> None: self.logger.info("~AudioManager()") - def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float]: - thisConfig = self.torchlight.config.config.get("VoiceServer", {}).get("AudioParams", {}) - if not thisConfig: - return triggerParams + def parse_params(self, trigger_params: dict, msg: str) -> dict[str, float]: + this_config = self.torchlight.config.config.get("VoiceServer", {}).get("AudioParams", {}) + if not this_config: + return trigger_params params: dict[str, float] = {} - for param in thisConfig: - if param in triggerParams and triggerParams[param]: - params[param] = float(triggerParams[param]) + for param in this_config: + if param in trigger_params and trigger_params[param] is not None: + params[param] = float(trigger_params[param]) else: - if isinstance(thisConfig[param], dict) and "Default" in thisConfig[param]: - params[param] = float(thisConfig[param]["Default"]) + if isinstance(this_config[param], dict) and "Default" in this_config[param]: + params[param] = float(this_config[param]["Default"]) msg_args = msg.split() for arg in msg_args: @@ -42,17 +42,17 @@ def parse_params(self, triggerParams: dict, msg: str) -> dict[str, float]: continue key = key.capitalize() - if key not in thisConfig: + if key not in this_config: continue - if isinstance(thisConfig[key], dict): - val: float = params[key] + if isinstance(this_config[key], dict): + val: float = 1.0 try: val = float(value) except ValueError: continue - val = max(thisConfig[key]["Min"], min(val, thisConfig[key]["Max"])) + val = max(this_config[key]["Min"], min(val, this_config[key]["Max"])) params[key] = val return params diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index ca4d1f9..fa26404 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -615,9 +615,9 @@ async def _func(self, message: list[str], player: Player) -> int: voice_trigger = message[0].lower() trigger_number: str = "" if message[1]: - firstPart = message[1].split()[0] - if firstPart.isdigit() or firstPart.startswith("?"): - trigger_number = firstPart + first_part = message[1].split()[0] + if first_part.isdigit() or first_part.startswith("?"): + trigger_number = first_part sound = self.get_sound_path( player=player, @@ -650,6 +650,7 @@ async def _func(self, message: list[str], player: Player) -> int: volume = modifiers["Volume"] speed = modifiers["Speed"] pitch = modifiers["Pitch"] + return audio_clip.Play(volume=volume, speed=speed, pitch=pitch) def get_sound_path(self, player: Player, voice_trigger: str, trigger_number: str) -> str | None: diff --git a/src/torchlight/TriggerManager.py b/src/torchlight/TriggerManager.py index bf01776..e86ae6f 100644 --- a/src/torchlight/TriggerManager.py +++ b/src/torchlight/TriggerManager.py @@ -25,11 +25,12 @@ def __init__( def Load(self) -> None: self.logger.info(f"Loading triggers from {self.config_filepath}") + voice_server_params = self.config.config.get("VoiceServer", {}).get("AudioParams", {}) with open(self.config_filepath) as fp: default_parameters = { - "Volume": float(self.config.config.get("VoiceServer", {}).get("Volume", 1.0)), - "Speed": float(self.config.config.get("VoiceServer", {}).get("Speed", 1.0)), - "Pitch": float(self.config.config.get("VoiceServer", {}).get("Pitch", 1.0)), + "Volume": float(voice_server_params.get("Volume", {}).get("Default", 1.0)), + "Speed": float(voice_server_params.get("Speed", {}).get("Default", 1.0)), + "Pitch": float(voice_server_params.get("Pitch", {}).get("Default", 1.0)), } self.triggers_dict = json.load(fp, object_pairs_hook=OrderedDict) From 7455b1be06ce001d97bf9afc04b5e183b4576d1f Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 22:33:23 +0200 Subject: [PATCH 35/37] resolving copilot review --- src/torchlight/AudioManager.py | 5 ++++- src/torchlight/Commands.py | 14 ++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/torchlight/AudioManager.py b/src/torchlight/AudioManager.py index 16070ad..a45731b 100644 --- a/src/torchlight/AudioManager.py +++ b/src/torchlight/AudioManager.py @@ -21,7 +21,7 @@ def __init__(self, torchlight: Torchlight) -> None: def __del__(self) -> None: self.logger.info("~AudioManager()") - def parse_params(self, trigger_params: dict, msg: str) -> dict[str, float]: + def ParseParams(self, trigger_params: dict, msg: str) -> dict[str, float]: this_config = self.torchlight.config.config.get("VoiceServer", {}).get("AudioParams", {}) if not this_config: return trigger_params @@ -52,6 +52,9 @@ def parse_params(self, trigger_params: dict, msg: str) -> dict[str, float]: except ValueError: continue + if "Min" not in this_config[key] or "Max" not in this_config[key]: + continue + val = max(this_config[key]["Min"], min(val, this_config[key]["Max"])) params[key] = val diff --git a/src/torchlight/Commands.py b/src/torchlight/Commands.py index fa26404..4da549b 100644 --- a/src/torchlight/Commands.py +++ b/src/torchlight/Commands.py @@ -615,9 +615,15 @@ async def _func(self, message: list[str], player: Player) -> int: voice_trigger = message[0].lower() trigger_number: str = "" if message[1]: - first_part = message[1].split()[0] - if first_part.isdigit() or first_part.startswith("?"): - trigger_number = first_part + parts = message[1].split() + trigger_parts = [] + for part in parts: + if "=" in part: + break + trigger_parts.append(part) + + if trigger_parts: + trigger_number = " ".join(trigger_parts) sound = self.get_sound_path( player=player, @@ -645,7 +651,7 @@ async def _func(self, message: list[str], player: Player) -> int: self.torchlight.SetPlayerCooldown(player, self.torchlight.config["AntiSpam"]["ChatCooldown"]) params = cast(dict, self.trigger_manager.voice_triggers[voice_trigger]["parameters"]) - modifiers = self.audio_manager.parse_params(params, message[1]) + modifiers = self.audio_manager.ParseParams(params, message[1]) volume = modifiers["Volume"] speed = modifiers["Speed"] From 3727fe1d6ebf94d9c3287840e1a4071f34e57a3f Mon Sep 17 00:00:00 2001 From: Dolly132 <109222243+Dolly132@users.noreply.github.com> Date: Sun, 8 Mar 2026 23:28:11 +0200 Subject: [PATCH 36/37] a small fix --- src/torchlight/FFmpegAudioPlayer.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/torchlight/FFmpegAudioPlayer.py b/src/torchlight/FFmpegAudioPlayer.py index 8ba88cf..bacda0c 100644 --- a/src/torchlight/FFmpegAudioPlayer.py +++ b/src/torchlight/FFmpegAudioPlayer.py @@ -29,9 +29,12 @@ def __init__(self, torchlight: Torchlight) -> None: self.host = self.config["Host"] self.port = self.config["Port"] self.sample_rate = float(self.config["SampleRate"]) - self.volume = float(self.config["Volume"]) - self.speed = float(self.config["Speed"]) - self.pitch = float(self.config["Pitch"]) + + params = self.config.get("AudioParams", {}) + + self.volume = float(params.get("Volume", {}).get("Default", 1.0)) + self.speed = float(params.get("Speed", {}).get("Default", 1.0)) + self.pitch = float(params.get("Pitch", {}).get("Default", 1.0)) self.proxy = self.config.get("Proxy", "") self.started_playing: float | None = None From 787bc6f2c98d16d6aeef453da9e3d29b0355274f Mon Sep 17 00:00:00 2001 From: Rushaway Date: Mon, 9 Mar 2026 00:24:26 +0100 Subject: [PATCH 37/37] Bump version to 1.4.0 --- src/torchlight/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/torchlight/__init__.py b/src/torchlight/__init__.py index f708a9b..3e8d9f9 100644 --- a/src/torchlight/__init__.py +++ b/src/torchlight/__init__.py @@ -1 +1 @@ -__version__ = "1.3.2" +__version__ = "1.4.0"