In services that don't handle runtime settings updates—or don't handle them for *all* available settings—log a warning about which fields specifically aren't handled. Revert new apply-settings-updates logic across various services, to reduce PR testing scope. This logic can be added service by service gradually as future work.
Note that for services that previously handled applying updates (through methods like `set_model` and `set_language`), we're keeping the update-applying logic (some or most of which is already well-tested) and expanding it to cover all relevant settings fields. Services under this bucket are: - Deepgram STT - Deepgram Sagemaker STT - Elevenlabs STT - Google STT - Gradium STT - OpenAI STT - Speechmatics STT
This commit is contained in:
@@ -123,6 +123,20 @@ class AIService(FrameProcessor):
|
|||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
def _warn_unhandled_updated_settings(self, unhandled: Set[str]):
|
||||||
|
"""Log a warning for settings changes that won't take effect at runtime.
|
||||||
|
|
||||||
|
Convenience helper for ``_update_settings`` overrides. Call with the
|
||||||
|
set of field names that changed but that the service does not (yet)
|
||||||
|
apply at runtime.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
unhandled: Field names that changed but are not applied.
|
||||||
|
"""
|
||||||
|
if unhandled:
|
||||||
|
fields = ", ".join(sorted(unhandled))
|
||||||
|
logger.warning(f"{self.name}: runtime update of [{fields}] is not currently supported")
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
"""Process frames and handle service lifecycle.
|
"""Process frames and handle service lifecycle.
|
||||||
|
|
||||||
|
|||||||
@@ -185,10 +185,9 @@ class AssemblyAISTTService(WebsocketSTTService):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def _update_settings(self, update: STTSettings) -> set[str]:
|
async def _update_settings(self, update: STTSettings) -> set[str]:
|
||||||
"""Apply a settings update and reconnect if anything changed.
|
"""Apply a settings update.
|
||||||
|
|
||||||
Any change triggers a WebSocket reconnect since all connection
|
Settings are stored but not applied to the active connection.
|
||||||
parameters are encoded in the WebSocket URL.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
update: A :class:`STTSettings` (or ``AssemblyAISTTSettings``) delta.
|
update: A :class:`STTSettings` (or ``AssemblyAISTTSettings``) delta.
|
||||||
@@ -201,15 +200,18 @@ class AssemblyAISTTService(WebsocketSTTService):
|
|||||||
if not changed:
|
if not changed:
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
# Re-apply manual turn mode config if vad_force_turn_endpoint is active
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
# and connection_params were updated.
|
# Code might look something like the below:
|
||||||
if self._vad_force_turn_endpoint and "connection_params" in changed:
|
# # Re-apply manual turn mode config if vad_force_turn_endpoint is active
|
||||||
self._settings.connection_params = self._configure_manual_turn_mode(
|
# # and connection_params were updated.
|
||||||
self._settings.connection_params
|
# if self._vad_force_turn_endpoint and "connection_params" in changed:
|
||||||
)
|
# self._settings.connection_params = self._configure_manual_turn_mode(
|
||||||
|
# self._settings.connection_params
|
||||||
|
# )
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
await self._disconnect()
|
self._warn_unhandled_updated_settings(changed)
|
||||||
await self._connect()
|
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ from pipecat.processors.aggregators.openai_llm_context import (
|
|||||||
)
|
)
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
from pipecat.processors.frame_processor import FrameDirection
|
||||||
from pipecat.services.llm_service import LLMService
|
from pipecat.services.llm_service import LLMService
|
||||||
|
from pipecat.services.settings import LLMSettings
|
||||||
from pipecat.utils.time import time_now_iso8601
|
from pipecat.utils.time import time_now_iso8601
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -302,6 +303,29 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
with wave.open(file_path.open("rb"), "rb") as wav_file:
|
with wave.open(file_path.open("rb"), "rb") as wav_file:
|
||||||
self._assistant_response_trigger_audio = wav_file.readframes(wav_file.getnframes())
|
self._assistant_response_trigger_audio = wav_file.readframes(wav_file.getnframes())
|
||||||
|
|
||||||
|
#
|
||||||
|
# settings
|
||||||
|
#
|
||||||
|
|
||||||
|
async def _update_settings(self, update: LLMSettings) -> set[str]:
|
||||||
|
"""Apply a settings update.
|
||||||
|
|
||||||
|
Settings are stored but not applied to the active connection.
|
||||||
|
"""
|
||||||
|
changed = await super()._update_settings(update)
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return changed
|
||||||
|
|
||||||
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
|
# Code might look something like the below:
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
#
|
#
|
||||||
# standard AIService frame handling
|
# standard AIService frame handling
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -141,16 +141,22 @@ class AWSTranscribeSTTService(WebsocketSTTService):
|
|||||||
return encoding_map.get(encoding, encoding)
|
return encoding_map.get(encoding, encoding)
|
||||||
|
|
||||||
async def _update_settings(self, update: STTSettings) -> set[str]:
|
async def _update_settings(self, update: STTSettings) -> set[str]:
|
||||||
"""Apply a settings update, reconnecting if needed.
|
"""Apply a settings update.
|
||||||
|
|
||||||
Any change to connection-relevant settings (model, language, etc.)
|
Settings are stored but not applied to the active connection.
|
||||||
triggers a WebSocket reconnect so the new configuration takes effect.
|
|
||||||
"""
|
"""
|
||||||
changed = await super()._update_settings(update)
|
changed = await super()._update_settings(update)
|
||||||
|
|
||||||
if changed and self._websocket:
|
if not changed:
|
||||||
await self._disconnect()
|
return changed
|
||||||
await self._connect()
|
|
||||||
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
|
# Code might look something like the below:
|
||||||
|
# if changed and self._websocket:
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|||||||
@@ -124,25 +124,28 @@ class AzureSTTService(STTService):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def _update_settings(self, update: STTSettings) -> set[str]:
|
async def _update_settings(self, update: STTSettings) -> set[str]:
|
||||||
"""Apply a settings update, reconfiguring the recognizer if needed.
|
"""Apply a settings update.
|
||||||
|
|
||||||
When ``language`` changes the ``SpeechConfig`` is updated and the
|
Settings are stored but not applied to the active recognizer.
|
||||||
speech recognizer is restarted so that the new language takes effect.
|
|
||||||
"""
|
"""
|
||||||
changed = await super()._update_settings(update)
|
changed = await super()._update_settings(update)
|
||||||
|
|
||||||
if "language" in changed:
|
if "language" in changed:
|
||||||
# Convert Language enum to Azure language code if needed.
|
# Convert Language enum to Azure language code for consistency.
|
||||||
lang = self._settings.language
|
lang = self._settings.language
|
||||||
if isinstance(lang, Language):
|
if isinstance(lang, Language):
|
||||||
lang = language_to_azure_language(lang)
|
lang = language_to_azure_language(lang)
|
||||||
self._settings.language = lang
|
self._settings.language = lang
|
||||||
self._speech_config.speech_recognition_language = lang
|
|
||||||
|
|
||||||
# Restart the recognizer with the new config.
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
if self._speech_recognizer:
|
# Code might look something like the below:
|
||||||
self._speech_recognizer.stop_continuous_recognition_async()
|
# if "language" in changed:
|
||||||
self._speech_recognizer.start_continuous_recognition_async()
|
# self._speech_config.speech_recognition_language = self._settings.language
|
||||||
|
# if self._speech_recognizer:
|
||||||
|
# self._speech_recognizer.stop_continuous_recognition_async()
|
||||||
|
# self._speech_recognizer.start_continuous_recognition_async()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ class CartesiaSTTService(WebsocketSTTService):
|
|||||||
await self._disconnect_websocket()
|
await self._disconnect_websocket()
|
||||||
|
|
||||||
async def _update_settings(self, update: STTSettings) -> set[str]:
|
async def _update_settings(self, update: STTSettings) -> set[str]:
|
||||||
"""Apply a settings update and reconnect if anything changed.
|
"""Apply a settings update.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
update: A :class:`STTSettings` (or ``CartesiaSTTSettings``) delta.
|
update: A :class:`STTSettings` (or ``CartesiaSTTSettings``) delta.
|
||||||
@@ -304,9 +304,15 @@ class CartesiaSTTService(WebsocketSTTService):
|
|||||||
Set of field names whose values actually changed.
|
Set of field names whose values actually changed.
|
||||||
"""
|
"""
|
||||||
changed = await super()._update_settings(update)
|
changed = await super()._update_settings(update)
|
||||||
if changed:
|
|
||||||
await self._disconnect()
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
await self._connect()
|
# Code might look something like the below:
|
||||||
|
# if changed:
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
async def _connect_websocket(self):
|
async def _connect_websocket(self):
|
||||||
|
|||||||
@@ -344,6 +344,25 @@ class CartesiaTTSService(AudioContextWordTTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def _update_settings(self, update: TTSSettings) -> set[str]:
|
||||||
|
"""Apply a settings update.
|
||||||
|
|
||||||
|
Settings are stored but not applied to the active connection.
|
||||||
|
"""
|
||||||
|
changed = await super()._update_settings(update)
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return changed
|
||||||
|
|
||||||
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
|
# Code might look something like the below:
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to Cartesia language format.
|
"""Convert a Language enum to Cartesia language format.
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from pipecat.frames.frames import (
|
|||||||
UserStartedSpeakingFrame,
|
UserStartedSpeakingFrame,
|
||||||
UserStoppedSpeakingFrame,
|
UserStoppedSpeakingFrame,
|
||||||
)
|
)
|
||||||
|
from pipecat.services.settings import STTSettings
|
||||||
from pipecat.services.stt_service import WebsocketSTTService
|
from pipecat.services.stt_service import WebsocketSTTService
|
||||||
from pipecat.transcriptions.language import Language
|
from pipecat.transcriptions.language import Language
|
||||||
from pipecat.utils.time import time_now_iso8601
|
from pipecat.utils.time import time_now_iso8601
|
||||||
@@ -329,6 +330,25 @@ class DeepgramFluxSTTService(WebsocketSTTService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def _update_settings(self, update: STTSettings) -> set[str]:
|
||||||
|
"""Apply a settings update.
|
||||||
|
|
||||||
|
Settings are stored but not applied to the active connection.
|
||||||
|
"""
|
||||||
|
changed = await super()._update_settings(update)
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return changed
|
||||||
|
|
||||||
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
|
# Code might look something like the below:
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
"""Start the Deepgram Flux STT service.
|
"""Start the Deepgram Flux STT service.
|
||||||
|
|
||||||
|
|||||||
@@ -516,6 +516,12 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
|
|||||||
await self.push_error(error_msg=f"Unknown error occurred: {e}", exception=e)
|
await self.push_error(error_msg=f"Unknown error occurred: {e}", exception=e)
|
||||||
self._context_id = None
|
self._context_id = None
|
||||||
|
|
||||||
|
if not url_changed:
|
||||||
|
# Reconnect applies all settings; only warn about fields not handled
|
||||||
|
# by voice settings or URL changes.
|
||||||
|
handled = ElevenLabsTTSSettings.URL_FIELDS | ElevenLabsTTSSettings.VOICE_SETTINGS_FIELDS
|
||||||
|
self._warn_unhandled_updated_settings(changed - handled)
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
|
|||||||
@@ -382,8 +382,7 @@ class GladiaSTTService(WebsocketSTTService):
|
|||||||
async def _update_settings(self, update: GladiaSTTSettings) -> set[str]:
|
async def _update_settings(self, update: GladiaSTTSettings) -> set[str]:
|
||||||
"""Apply settings update.
|
"""Apply settings update.
|
||||||
|
|
||||||
Gladia sessions are fixed at creation time, so any change requires
|
Settings are stored but not applied to the active session.
|
||||||
a full session teardown and reconnect.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
update: A settings delta.
|
update: A settings delta.
|
||||||
@@ -396,11 +395,14 @@ class GladiaSTTService(WebsocketSTTService):
|
|||||||
if not changed:
|
if not changed:
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
# Gladia sessions are fixed — need to tear down and recreate
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
self._session_url = None
|
# Code might look something like the below:
|
||||||
self._session_id = None
|
# self._session_url = None
|
||||||
await self._disconnect()
|
# self._session_id = None
|
||||||
await self._connect()
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|||||||
@@ -804,6 +804,25 @@ class GeminiLiveLLMService(LLMService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def _update_settings(self, update: LLMSettings) -> set[str]:
|
||||||
|
"""Apply a settings update.
|
||||||
|
|
||||||
|
Settings are stored but not applied to the active connection.
|
||||||
|
"""
|
||||||
|
changed = await super()._update_settings(update)
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return changed
|
||||||
|
|
||||||
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
|
# Code might look something like the below:
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
def set_audio_input_paused(self, paused: bool):
|
def set_audio_input_paused(self, paused: bool):
|
||||||
"""Set the audio input pause state.
|
"""Set the audio input pause state.
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ class GradiumTTSService(InterruptibleWordTTSService):
|
|||||||
if self._voice_id != prev_voice:
|
if self._voice_id != prev_voice:
|
||||||
await self._disconnect()
|
await self._disconnect()
|
||||||
await self._connect()
|
await self._connect()
|
||||||
|
else:
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
def _build_msg(self, text: str = "") -> dict:
|
def _build_msg(self, text: str = "") -> dict:
|
||||||
|
|||||||
@@ -165,6 +165,25 @@ class InworldHttpTTSService(WordTTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def _update_settings(self, update: TTSSettings) -> set[str]:
|
||||||
|
"""Apply a settings update.
|
||||||
|
|
||||||
|
Settings are stored but not applied to the active connection.
|
||||||
|
"""
|
||||||
|
changed = await super()._update_settings(update)
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return changed
|
||||||
|
|
||||||
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
|
# Code might look something like the below:
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
"""Start the Inworld TTS service.
|
"""Start the Inworld TTS service.
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,25 @@ class PlayHTTTSService(InterruptibleTTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def _update_settings(self, update: TTSSettings) -> set[str]:
|
||||||
|
"""Apply a settings update.
|
||||||
|
|
||||||
|
Settings are stored but not applied to the active connection.
|
||||||
|
"""
|
||||||
|
changed = await super()._update_settings(update)
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return changed
|
||||||
|
|
||||||
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
|
# Code might look something like the below:
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
|
return changed
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to PlayHT service language format.
|
"""Convert a Language enum to PlayHT service language format.
|
||||||
|
|
||||||
|
|||||||
@@ -279,6 +279,8 @@ class RimeTTSService(AudioContextWordTTSService):
|
|||||||
self._settings.speaker = self._voice_id
|
self._settings.speaker = self._voice_id
|
||||||
await self._disconnect()
|
await self._disconnect()
|
||||||
await self._connect()
|
await self._connect()
|
||||||
|
else:
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
def _build_msg(self, text: str = "") -> dict:
|
def _build_msg(self, text: str = "") -> dict:
|
||||||
|
|||||||
@@ -338,11 +338,16 @@ class SarvamSTTService(STTService):
|
|||||||
|
|
||||||
changed = await super()._update_settings(update)
|
changed = await super()._update_settings(update)
|
||||||
|
|
||||||
if not changed:
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
return changed
|
# Code might look something like the below:
|
||||||
|
# if not changed:
|
||||||
|
# return changed
|
||||||
|
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
await self._disconnect()
|
|
||||||
await self._connect()
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
async def set_prompt(self, prompt: Optional[str]):
|
async def set_prompt(self, prompt: Optional[str]):
|
||||||
|
|||||||
@@ -958,6 +958,7 @@ class SarvamTTSService(InterruptibleTTSService):
|
|||||||
changed = await super()._update_settings(update)
|
changed = await super()._update_settings(update)
|
||||||
if "voice" in changed:
|
if "voice" in changed:
|
||||||
await self._send_config()
|
await self._send_config()
|
||||||
|
self._warn_unhandled_updated_settings(changed - {"voice"})
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
async def _connect(self):
|
async def _connect(self):
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class SonioxSTTService(WebsocketSTTService):
|
|||||||
``input_params`` is given, its ``model`` is propagated *up* to the
|
``input_params`` is given, its ``model`` is propagated *up* to the
|
||||||
top-level field.
|
top-level field.
|
||||||
|
|
||||||
Any change triggers a WebSocket reconnect.
|
Settings are stored but not applied to the active connection.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
update: A settings delta.
|
update: A settings delta.
|
||||||
@@ -249,8 +249,12 @@ class SonioxSTTService(WebsocketSTTService):
|
|||||||
self._settings.model = self._settings.input_params.model
|
self._settings.model = self._settings.input_params.model
|
||||||
self.set_model_name(self._settings.model)
|
self.set_model_name(self._settings.model)
|
||||||
|
|
||||||
await self._disconnect()
|
# TODO: someday we could reconnect here to apply updated settings.
|
||||||
await self._connect()
|
# Code might look something like the below:
|
||||||
|
# await self._disconnect()
|
||||||
|
# await self._connect()
|
||||||
|
|
||||||
|
self._warn_unhandled_updated_settings(changed)
|
||||||
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|||||||
@@ -329,6 +329,7 @@ class UltravoxRealtimeLLMService(LLMService):
|
|||||||
changed = await super()._update_settings(update)
|
changed = await super()._update_settings(update)
|
||||||
if "output_medium" in changed:
|
if "output_medium" in changed:
|
||||||
await self._update_output_medium(self._settings.output_medium)
|
await self._update_output_medium(self._settings.output_medium)
|
||||||
|
self._warn_unhandled_updated_settings(changed - {"output_medium"})
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
Reference in New Issue
Block a user