Repository navigation
fix(tts): never leak temp files and run gTTS off the event loop - #200
Merged
Merged
Conversation
Say, TranslateSay and DECTalk now share one temp-file ownership scheme: the file (prefixed torchlight-tts-) is closed and removed on every failure path, and is only handed to the clip's Stop callback once Play() succeeded. That callback captures just the path string. gTTS synthesis runs in asyncio.to_thread so its HTTP round-trip no longer freezes the loop (stream stutter, queued RCON/SourceMod messages). If the task is cancelled while the worker runs, the file is removed when the worker finishes. DECTalk checks the say exit code, kills and reaps the process when communicate() fails or is cancelled, and no longer sets the chat cooldown twice. The Say coroutines are started with Utils.FireAndForget so unexpected errors are logged, and the player is told privately when TTS fails. Closes #183 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
16 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the temp-file and file-descriptor leaks in the TTS commands (
!say,!tsay,!dec) and moves gTTS synthesis off the event loop. Scope: theSay,TranslateSayandDECTalkclasses inCommands.py, plus a few module-level helpers placed betweenTranslateSayandDECTalk.One ownership scheme for TTS temp files
Why:
Say.SayandTranslateSay.Saycreated aNamedTemporaryFile(delete=False)and then calledgTTS.write_to_fp(). Any gTTS or HTTP error (rate limit, network down) left the handle open and the file on disk.DECTalk.Sayleaked the file when thesaybinary was missing orcommunicate()failed.Fix:
_write_gtts_filewrites insidewith temp_file:and removes the file if anything raises._play_tts_filehands the file to the clip'sStopcallback only afterPlay()succeeded. IfAudioClip()returnsNone,Play()returnsFalse, or either of them raises, the file is removed.Stopcallback captures only the path string, so no file object or other large object outlives the playback.torchlight-tts-prefix (.mp3for gTTS,.wavfor DECtalk), so leftovers are easy to spot.TranslateSay.Saynow callssuper().Say(...)after translating instead of keeping a copy of the synthesis code.gTTS no longer blocks the event loop
Why:
write_to_fp()is a synchronous HTTP call. While it ran, the whole loop was frozen: every playing stream stopped feeding the voice server, and RCON and SourceMod messages queued up.Fix:
gTTS(...)andwrite_to_fpnow run inasyncio.to_thread. The worker thread cannot be interrupted, so if the task is cancelled while it runs, a done-callback removes the file once the thread finishes.DECTalk hardening
Why: the
sayexit code was ignored, so a failed synthesis played an empty file. The process was not killed on error or cancellation._funcalso set the chat cooldown twice.Fix:
communicate()raises or is cancelled, the process is killed and reaped.SetPlayerCooldownis gone.Failures are logged and reported to the player
Why:
Say()was started with a bareasyncio.ensure_future, so its exceptions were never logged.Fix:
Say._func(also used byTranslateSay) andDECTalk._funcnow start the coroutine withUtils.FireAndForget(coro, self.logger).TTS failed: <error>, the same text!tsayalready used. DECtalk shows a plainTTS failed., because its errors can contain server paths.Everything else the player sees is unchanged.
Test plan
ruff check .,ruff format --check .,mypy: no new findings. The only remaining ones are the 3 known baseline errors fixed by CI: Python Linting is red on master since the Python 3.12 bump (ruff UP041, mypy arg-type) #178's PR.torchlight-tts-*files in the temp dir before and after each case:write_to_fpraises: no file left, player getsTTS failed: ..., returns 1AudioClip()returnsNone,Play()returnsFalse,AudioClip()raises: no file leftStopcallback runs, then it is removed. The callback closure holds only astr.TranslateSay: gTTS error,Play()fails, and success withStopTTS failed.-af volume=10dB, and the file is removed onStopDECTalk._func: goes throughFireAndForget(an unexpected exception is logged) and sets the cooldown once!say,!tsay,!decplay normally, and nothing stays in/tmpafter playback ends or is stoppedNo VERSION bump (batch policy, see #177).
Closes #183
Part of #177
🤖 Generated with Claude Code