Skip to content

New tts#31

Open
ReWAFFlution wants to merge 6 commits intomasterfrom
New-TTS
Open

New tts#31
ReWAFFlution wants to merge 6 commits intomasterfrom
New-TTS

Conversation

@ReWAFFlution
Copy link
Copy Markdown
Member

@ReWAFFlution ReWAFFlution commented Mar 29, 2026

Краткое описание

Почему мы должны добавить это?

Медиа (Видео/Скриншоты)

Проверочный пункт

  • Перед публикацией/запросом на проверку PR, я убедился что изменения работают.
  • Я добавил скриншоты/видео изменений, если только этот PR не изменит внутриигровую механику.
  • Я подтверждаю, что мои изменения лицензированы в соответствии с лицензией Open Space Лицензия и предоставляю разрешение на их использование в этом репозитории в соответствии с его условиями.

Changelog

🆑 GqXgji, Al_S

  • add: Добавлен TTS (провайдер /N/TTS)

Summary by CodeRabbit

  • Новые функции
    • Полноценный синтез речи (TTS) в игре: генерация и воспроизведение голосов.
    • Выбор голоса персонажа и сохранение его в профиле.
    • Предпрослушивание голоса в редакторе персонажа и в окнах масок голоса.
    • Радио и голосовые маски теперь поддерживают воспроизведение синтезированной речи.
    • Параметры TTS в настройках: включение/отключение и регулятор громкости.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 29, 2026

📝 Walkthrough

Walkthrough

Добавлена полноценная система TTS: UI для выбора и превью голосов, клиентская и серверная логика воспроизведения (включая радио/шепот), серверная генерация через HTTP API с кешем, текстовая санитизация, CVars и миграции БД для сохранения выбранного голоса.

Changes

Cohort / File(s) Summary
UI элементы для выбора голоса
Content.Client/Lobby/UI/HumanoidProfileEditor.xaml, Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs, Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml, Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
Добавлены скрытые контейнеры TTS/Voice с OptionButton и Play кнопкой; видимость управляется CVar TTSClientEnabled.
Логика профиля персонажа (клиент)
Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs, Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs
Вызывается обновление контролов голосов при смене пола; добавлены инициализация голосов, список голосов, выбор и превью (PlayPreviewTTS), и установка голоса в профиль.
Опции звука (клиент)
Content.Client/Options/UI/Tabs/AudioTab.xaml, Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
Добавлены слайдер громкости TTS и чекбокс включения TTS; связаны с ArtCVars (TTSVolume, TTSClientEnabled).
Клиентская аудио/TTS подсистема
Content.Client/_Art/TTS/TTSSystem.cs, Content.Client/_Art/TTS/ContentAudioSystem.cs
Реализовано стриминговое воспроизведение TTS, очередь по автору, управление шепотом/дистанцией, очистка завершённых воспроизведений и API запроса превью.
VoiceMask — клиент
Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs, Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
UI принимает и отображает выбор голоса; состояние теперь включает поле Voice, отправляются сообщения смены голоса.
Миграции БД и модель
Content.Server.Database/Migrations/..., Content.Server.Database/Model.cs, PostgresServerDbContextModelSnapshot.cs, SqliteServerDbContextModelSnapshot.cs
Добавлен новый столбец voice в таблицу profile в миграциях Postgres/SQLite; модель Profile получил свойство Voice.
Загрузка/конвертация профилей (сервер)
Content.Server/Database/ServerDbBase.cs, Content.Server/Preferences/Managers/ServerPreferencesManager.cs
При конвертации профиля сохраняется/передаётся поле голоса; при пустом значении используется default по полу.
Серверная генерация TTS
Content.Server/_Art/TTS/TTSManager.cs, Content.Server/_Art/TTS/TTSSystem.cs
Добавлен TTSManager для запросов к HTTP API с кешированием, семафорами по ключу; TTSSystem генерирует/распространяет аудио, обрабатывает запрос превью и опции клиентов.
Сервер: санитизация и число→текст
Content.Server/_Art/TTS/TTSSystem.Sanitize.cs, Content.Server/_Art/TTS/TTSSystem.SSML.cs
Добавлена сложная санитизация текста, транслитерация, замены, конвертация чисел в слова и SSML-обёртка для управляемой интонации.
Сервер: рейт-лимит TTS
Content.Server/_Art/TTS/TTSSystem.RateLimit.cs
Регистрация и проверка рейт-лимита по игроку для TTS с уведомлением в чат при превышении.
VoiceMask — сервер
Content.Server/VoiceMask/VoiceMaskSystem.cs, Content.Server/_Art/TTS/VoiceMaskSystem.TTS.cs
Инициализация TTS в системе маски, обработка событий изменений голоса и синхронизация VoiceId в компоненте.
Интеграция с радио
Content.Server/Radio/RadioSystem.cs, Content.Server/Radio/RadioEvent.cs, Content.Server/Radio/EntitySystems/HeadsetSystem.cs
RadioReceiveEvent получил поле Voice; при приёме радиосообщения генерируется TTSRadioPlayEvent для воспроизведения голосом.
Инициализация и DI (сервер)
Content.Server/Entry/EntryPoint.cs, Content.Server/IoC/ServerContentIoC.cs
Зарегистрирован и инициализирован TTSManager в старте сервера/контейнере зависимостей.
Сетевые события TTS
Content.Shared/_Art/TTS/PlayTTSEvent.cs, RequestPreviewTTSEvent.cs, ClientOptionTTSEvent.cs, TTSRadioPlayEvent.cs, SharedVoiceMaskSystem.cs
Добавлены сетевые/локальные события для воспроизведения TTS, запроса превью, опций клиента и передачи голосов маски.
Компоненты и конфиги
Content.Shared/Humanoid/HumanoidProfileComponent.cs, Content.Shared/_Art/TTS/TTSComponent.cs, Content.Shared/_Art/TTS/TTSConfig.cs, Content.Shared/_Art/CVars/ArtCVars.cs
Профиль получил поле Voice; добавлен TTSComponent для предметов-источников речи; добавлены CVars и константы TTS (диапазоны, defaults).
Профили и прототипы
Content.Shared/Humanoid/HumanoidProfileExportV1.cs, Content.Shared/Preferences/HumanoidCharacterProfile.cs, Content.Shared/_Art/TTS/TTSVoicePrototype.cs, Content.Shared/VoiceMask/VoiceMaskComponent.cs, Content.Shared/Inventory/InventorySystem.Relay.cs
HumanoidCharacterProfile расширен полем Voice и методами WithVoice; добавлен прототип ttsVoice и реле события TransformSpeakerVoiceEvent.
Ресурсы — список голосов
Resources/_Art/TTS/tts-voices.yml
Добавлен большой YAML-каталог доступных голосов (ttsVoice) с метаданными (name, speaker, id, gender).
Тесты
Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
Тестовый профиль получил присвоение Voice в хелпере для интеграционных тестов.

Sequence Diagram(s)

sequenceDiagram
    participant Player as Игрок(UI)
    participant Client as Клиент
    participant Server as Сервер
    participant TTSMgr as TTSManager
    participant TTSApi as TTS API
    participant Audio as AudioSystem

    Player->>Client: Нажать "Play Preview"
    Client->>Server: RequestPreviewTTSEvent(voiceId)
    Server->>Server: Проверка рейт-лимита
    Server->>TTSMgr: ConvertTextToSpeech(voiceId, sample)
    TTSMgr->>TTSMgr: Проверить кеш
    alt В кеше
        TTSMgr-->>Server: byte[] audio
    else Не в кеше
        TTSMgr->>TTSApi: HTTP GET /generate?speaker=...&text=...
        TTSApi-->>TTSMgr: byte[] (WAV)
        TTSMgr->>TTSMgr: Сохранить в кеш
        TTSMgr-->>Server: byte[] audio
    end
    Server->>Client: PlayTTSEvent(data, source)
    Client->>Audio: Воспроизвести поток
    Audio-->>Player: Звук
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Пришёл голос в лобби, шепчет слово,
Кеш хранит то, что было снова,
Выбери, нажми — и звук идёт,
Сервер шепчет, клиент поёт,
В мире TTS — тепла перевод. 🎙️✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning Заголовок «New tts» не раскрывает основной смысл изменений в pull request — добавление полноценной системы TTS (Text-to-Speech) с поддержкой голосов, UI-элементов, конфигурации и интеграцией с радио и голосовыми масками. Замените заголовок на более описательный, например: «Add Text-to-Speech (TTS) system with voice profiles and audio synthesis» или «Implement TTS support with voice selection and preview functionality»
Docstring Coverage ⚠️ Warning Docstring coverage is 15.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch New-TTS

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (10)
Content.Shared/_Art/TTS/TTSConfig.cs (1)

8-13: Сделайте таблицу дефолтных голосов неизменяемой.

public static readonly защищает только ссылку. Сейчас любой внешний код может изменить содержимое DefaultSexVoice и сломать глобальные дефолты во всём процессе. Лучше отдать IReadOnlyDictionary или ImmutableDictionary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/_Art/TTS/TTSConfig.cs` around lines 8 - 13, DefaultSexVoice is
currently a public static readonly Dictionary which allows external mutation;
change its type to an immutable/read-only collection (e.g. public static
readonly IReadOnlyDictionary<Sex,string> DefaultSexVoice or
ImmutableDictionary<Sex,string>) and initialize it with an immutable instance
(use ImmutableDictionary.CreateRange or call .ToImmutableDictionary() on the
literal) so callers cannot modify entries; update any call sites that rely on
mutability to treat it as read-only and keep the symbol name DefaultSexVoice and
the Sex enum unchanged.
Content.Server/VoiceMask/VoiceMaskSystem.cs (1)

195-195: Рекомендация: рассмотреть форматирование длинной строки.

Строка содержит много параметров и становится трудночитаемой. Можно рассмотреть разбиение на несколько строк для улучшения читаемости.

♻️ Предложение по форматированию
-            _uiSystem.SetUiState(entity.Owner, VoiceMaskUIKey.Key, new VoiceMaskBuiState(GetCurrentVoiceName(entity), entity.Comp.VoiceId, entity.Comp.VoiceMaskSpeechVerb, entity.Comp.Active, entity.Comp.AccentHide )); // Art-TTS
+            _uiSystem.SetUiState(
+                entity.Owner,
+                VoiceMaskUIKey.Key,
+                new VoiceMaskBuiState(
+                    GetCurrentVoiceName(entity),
+                    entity.Comp.VoiceId, // Art-TTS
+                    entity.Comp.VoiceMaskSpeechVerb,
+                    entity.Comp.Active,
+                    entity.Comp.AccentHide));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/VoiceMask/VoiceMaskSystem.cs` at line 195, The long
single-line call to _uiSystem.SetUiState with VoiceMaskUIKey.Key and a new
VoiceMaskBuiState(...) is hard to read; reformat it by breaking the
VoiceMaskBuiState construction across multiple lines (one argument per line or
grouped logically) and align the parameters, referencing the call site using
_uiSystem.SetUiState, VoiceMaskUIKey.Key, and the VoiceMaskBuiState constructor
(which uses GetCurrentVoiceName(entity), entity.Comp.VoiceId,
entity.Comp.VoiceMaskSpeechVerb, entity.Comp.Active, entity.Comp.AccentHide) so
the line fits the style guide and is easier to scan.
Content.Client/_Art/TTS/ContentAudioSystem.cs (1)

1-8: Добавьте документацию для константы TtsMultiplier.

Файл находится в Content.Client/_Art/TTS/, но пространство имён — Content.Client.Audio. Это приемлемо для частичного класса (partial class), однако константа TtsMultiplier = 3f используется в Content.Client/Options/UI/Tabs/AudioTab.xaml.cs без объяснения своего назначения. Добавьте XML-комментарий, поясняющий, почему используется коэффициент 3.0 и где применяется это масштабирование.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/_Art/TTS/ContentAudioSystem.cs` around lines 1 - 8, Добавьте
XML-документацию над константой TtsMultiplier в partial классе
ContentAudioSystem, кратко объяснив, что это множитель громкости/интенсивности
для TTS (текст-в-речь), почему выбран коэффициент 3.0 (например, для приведения
уровня TTS к уровню других звуков в клиенте) и где он применяется (используется
в AudioTab.xaml.cs для масштабирования громкости/предпрослушивания TTS);
пометьте комментарий примерным контекстом использования и возможными побочными
эффектами, чтобы будущие разработчики понимали назначение и область применения
константы.
Resources/_Art/TTS/tts-voices.yml (1)

1-38: Уберите генераторные скрипты из production YAML.

Большой закомментированный блок в начале файла затрудняет ревью и сопровождение каталога голосов.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/_Art/TTS/tts-voices.yml` around lines 1 - 38, Удалите большой
закомментированный генераторный код из файла tts-voices.yml: уберите блок Emacs
Lisp (функция f) и закомментированный Python-скрипт (строка "#!/usr/bin/env
python3" и последующий JSON-парсер), оставив только валидный YAML с описаниями
голосов; если нужен исходник генерации — перенесите эти скрипты в отдельную
утилиту/папку scripts или docs и добавьте краткую ссылку/комментарий внизу YAML
вместо вставки полного кода.
Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs (1)

9-19: Сделайте payload события иммутабельным.

Публичные изменяемые поля в EntityEventArgs повышают риск побочных мутаций между обработчиками.

♻️ Предлагаемый фикс
 public sealed class TTSRadioPlayEvent : EntityEventArgs
 {
-    public string Message;
-    public string Voice;
-    public NetEntity? Source;
-    public NetEntity? Author;
+    public readonly string Message;
+    public readonly string Voice;
+    public readonly NetEntity? Source;
+    public readonly NetEntity? Author;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs` around lines 9 - 19, Сделайте
полезную нагрузку события иммутабельной: замените публичные изменяемые поля
Message, Voice, Source, Author в классе TTSRadioPlayEvent на readonly-поля или
публичные свойства только для чтения (get; без set; или init-only), присваивая
значения в конструкторе TTSRadioPlayEvent(string message, string voice,
NetEntity? source, NetEntity? author); убедитесь, что класс по-прежнему
наследует нужный тип (EntityEventArgs) и что нигде в коде не производится
мутация этих полей после создания события.
Content.Shared/_Art/TTS/PlayTTSEvent.cs (1)

9-9: Рассмотрите добавление валидации для Data.

Поле Data содержит аудио данные и передаётся по сети. Отсутствует проверка на null или пустой массив в конструкторе. Также стоит учитывать потенциальный размер данных для сетевой передачи.

♻️ Предлагаемое улучшение
     public PlayTTSEvent(byte[] data, NetEntity? sourceUid = null, bool isWhisper = false, NetEntity? author = null)
     {
+        ArgumentNullException.ThrowIfNull(data);
         Data = data;
         SourceUid = sourceUid;
         IsWhisper = isWhisper;
         Author = author ?? sourceUid;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/_Art/TTS/PlayTTSEvent.cs` at line 9, В конструкторе класса
PlayTTSEvent добавьте валидацию для параметра, инициализирующего свойство Data:
проверьте, что массив не null и не пуст (и опционально не превышает разумного
максимального размера для сетевой передачи), выбрасывая ArgumentNullException
или ArgumentException с понятным сообщением; при присвоении используйте копию
массива (Array.Copy или .ToArray()) чтобы сохранить неизменяемость внутреннего
состояния; обновите соответствующий конструктор и документацию для свойства
Data.
Content.Shared/Humanoid/HumanoidProfileExportV1.cs (1)

65-68: Рассмотрите добавление значения по умолчанию для поля Voice.

Поле Voice объявлено без значения по умолчанию. При импорте старых профилей V1, не содержащих данных о голосе, значение будет пустым, что может привести к проблемам при конвертации в V2, если конструктор HumanoidCharacterProfile не обрабатывает пустые значения.

♻️ Предлагаемое исправление
     // Art-TTS Start
     [DataField]
-    public ProtoId<TTSVoicePrototype> Voice;
+    public ProtoId<TTSVoicePrototype> Voice = string.Empty;
     // Art-TTS End
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/Humanoid/HumanoidProfileExportV1.cs` around lines 65 - 68, The
Voice field (ProtoId<TTSVoicePrototype> Voice) in HumanoidProfileExportV1 is
declared without a default, so older V1 profiles may deserialize empty and break
conversion; set a safe default value for Voice (e.g., a known default TTS
prototype id or an explicit empty/none sentinel) on the field declaration and
update the HumanoidCharacterProfile constructor/ conversion code that consumes
HumanoidProfileExportV1 to treat missing/empty Voice as that default; ensure
references to Voice and the HumanoidCharacterProfile constructor handle the
default consistently.
Content.Server/_Art/TTS/TTSSystem.Sanitize.cs (3)

20-20: Нестандартная запись символьного класса [1-90].

Паттерн [1-90] технически работает, но выглядит нетипично. Рекомендуется использовать стандартный [0-9] или \d для улучшения читаемости.

♻️ Предлагаемое исправление
-    private static readonly Regex DecimalSep = new Regex(@"(?<=[1-90])(\.|,)(?=[1-90])", RegexOptions.Compiled);
+    private static readonly Regex DecimalSep = new Regex(@"(?<=[0-9])(\.|,)(?=[0-9])", RegexOptions.Compiled);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/_Art/TTS/TTSSystem.Sanitize.cs` at line 20, The Regex field
DecimalSep uses a nonstandard character class `[1-90]`; update the pattern to
use a standard digit class for clarity and correctness — e.g. replace `[1-90]`
with `[0-9]` or `\d` in the DecimalSep initialization (private static readonly
Regex DecimalSep) so the pattern becomes @"(?<=[0-9])(\.|,)(?=[0-9])" or use
`\d` equivalents while preserving RegexOptions.Compiled.

36-48: Рекомендуется использовать ToLowerInvariant() вместо ToLower().

Метод ToLower() зависит от текущей культуры и может вести себя неожиданно в некоторых локалях (например, турецкий İi). Для транслитерации латиницы безопаснее использовать ToLowerInvariant().

♻️ Предлагаемое исправление
     private string ReplaceLat2Cyr(Match oneChar)
     {
-        if (ReverseTranslit.TryGetValue(oneChar.Value.ToLower(), out var replace))
+        if (ReverseTranslit.TryGetValue(oneChar.Value.ToLowerInvariant(), out var replace))
             return replace;
         return oneChar.Value;
     }

     private string ReplaceMatchedWord(Match word)
     {
-        if (WordReplacement.TryGetValue(word.Value.ToLower(), out var replace))
+        if (WordReplacement.TryGetValue(word.Value.ToLowerInvariant(), out var replace))
             return replace;
         return word.Value;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/_Art/TTS/TTSSystem.Sanitize.cs` around lines 36 - 48, Replace
culture-dependent ToLower() calls with ToLowerInvariant() in the two helper
methods: in ReplaceLat2Cyr use oneChar.Value.ToLowerInvariant() when looking up
ReverseTranslit, and in ReplaceMatchedWord use word.Value.ToLowerInvariant()
when looking up WordReplacement; keep the existing lookup and return logic
otherwise.

172-174: Избыточные записи в словаре транслитерации.

Записи {"й", "й"}, {"ъ", "ъ"}, {"ь", "ь"} никогда не используются, так как регулярное выражение Latin обрабатывает только латинские символы.

♻️ Предлагаемое исправление
     private static readonly IReadOnlyDictionary<string, string> ReverseTranslit =
         new Dictionary<string, string>()
         {
-            {"й", "й"},
-            {"ъ", "ъ"},
-            {"ь", "ь"},
             {"a", "а"},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/_Art/TTS/TTSSystem.Sanitize.cs` around lines 172 - 174, В
словаре транслитерации в TTSSystem.Sanitize удалите из инициализации словаря
записи {"й","й"}, {"ъ","ъ"}, {"ь","ь"}, так как они никогда не используются
(регулярное выражение Latin обрабатывает только латинские символы); подправьте
инициализацию соответствующего transliteration dictionary в классе/методе
TTSSystem.Sanitize, удалив эти ключи-значения, и сохраните остальную структуру
словаря без других изменений.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs`:
- Around line 14-29: InitializeVoice currently adds anonymous handlers every
call causing duplicate event firing; fix by extracting the handlers into named
methods (e.g., OnVoiceSelected which calls VoiceButton.SelectId(...) and
SetVoice(_voiceList[args.Id].ID), and OnVoicePlayPressed which calls
PlayPreviewTTS), then subscribe those methods once (VoiceButton.OnItemSelected
+= OnVoiceSelected; VoicePlayButton.OnPressed += OnVoicePlayPressed) or
explicitly unsubscribe before subscribing (VoiceButton.OnItemSelected -=
OnVoiceSelected; VoiceButton.OnItemSelected += OnVoiceSelected; same for
VoicePlayButton) so repeated InitializeVoice calls won't attach duplicate
delegates; leave the _voiceList population as-is.

In `@Content.Client/_Art/TTS/TTSSystem.cs`:
- Around line 96-102: The code currently dequeues elem before verifying its
SourceUid, causing queued TTS to be lost when TryGetEntity(elem.Source, ...)
returns false; change the logic to peek at the next queue item (use TryPeek or
equivalent) and only call queue.TryDequeue when TryGetEntity(elem.Source, out
local_source) succeeds, or if Source is invalid but Author is present route the
item to the global fallback playback instead of dropping it; update the block
that sets _playing[author] = PlayTTSFromUid(...) (and the analogous block around
lines 147-171) so dequeue happens after entity validation or fallback dispatch.

In `@Content.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.cs`:
- Around line 17-40: ReloadVoices is not idempotent because it keeps +=
registering the same handler and keeps appending items; before adding the
handler and items remove any previous handler and clear the selector: store the
selection delegate in a private field (e.g. private Action<YourArgsType>
_voiceSelectionHandler or EventHandler-like _voiceSelectionHandler), call
VoiceSelector.OnItemSelected -= _voiceSelectionHandler (or set to null if the
control allows) before assigning `_voiceSelectionHandler = args => {
VoiceSelector.SelectId(args.Id); if (VoiceSelector.SelectedMetadata != null)
OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata); };` and then
`VoiceSelector.OnItemSelected += _voiceSelectionHandler`; also clear existing
entries before repopulating the list (e.g. call VoiceSelector.ClearItems() or
VoiceSelector.Items.Clear()) so the loop that calls VoiceSelector.AddItem /
SetItemMetadata does not duplicate entries; keep the rest of the logic (_voices
population, SelectId usage, OnVoiceChange invoke) unchanged.

In `@Content.Client/Options/UI/Tabs/AudioTab.xaml`:
- Line 11: Replace the hard-coded Title text on the new UI controls (e.g., the
OptionSlider named SliderVolumeTts and the other control added at the same
location) with the localization helper (Loc) so strings flow through the i18n
pipeline; update the XAML Title attributes to use the Loc markup extension or
binding (e.g., Title="{Loc Key=...}" or equivalent used elsewhere) and add
matching resource keys for those strings to the shared resource file so the UI
reads localized text instead of literal Russian text.

In `@Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs`:
- Around line 89-93: Добавьте в класс
Content.Client.VoiceMask.VoiceMaskNameChangeWindow поле List<TTSVoicePrototype>
_voices и реализуйте метод ReloadVoices() — метод должен заполнять _voices
(например, из того же источника, что и в Content.Client._Art.TTS версия класса),
обновлять элементы селектора VoiceSelector (Items/ItemsSource) и корректно
выставлять текущий выбранный элемент; убедитесь, что добавлены нужные using
(System.Collections.Generic и пространство имён, где объявлен TTSVoicePrototype)
и что имена методов/полей соответствуют существующему коду (включая _voices,
ReloadVoices, VoiceSelector, VoiceMaskNameChangeWindow).

In `@Content.Server.Database/Migrations/Sqlite/20260327081359_AddTtsVoice.cs`:
- Around line 13-18: The migration adds a NOT NULL profile.voice column with
defaultValue: "" which produces invalid empty IDs; update the migration so
existing rows get a valid voice ID or perform a safe backfill: either set
migrationBuilder.AddColumn's defaultValue to a known valid voice ID (replace ""
with the concrete ID), or change the migration to add the column as nullable,
run a backfill that sets a valid voice for existing profiles, then alter the
column to be NOT NULL; reference the AddColumn call for "voice" on table
"profile" when making the change.

In `@Content.Server/_Art/TTS/TTSManager.cs`:
- Around line 74-99: The logs currently emit raw user text and the full
requestUrl (see _sawmill.Verbose/Debug calls around cacheKey, speaker, effect
and the requestUrl before calling _httpClient.GetAsync), so change those log
statements to avoid printing the user text and full URL: log cacheKey, speaker,
effect (or sanitized effect), and text length instead of the text content, and
remove the full requestUrl from Debug/Error logs; after the HTTP call log only
the response.StatusCode and any error message (not the URL or body). Ensure you
update all related messages in the TTS flow (the uses around
_sawmill.Verbose/Debug/Error in TTSManager methods that reference text or
requestUrl) to follow this pattern.
- Around line 40-42: Protect the non-thread-safe fields _cache, _cacheKeysSeq
and _semaphores used in ConvertTextToSpeech by introducing a single private sync
object (e.g., a readonly object _lock) or replacing them with thread-safe
collections (ConcurrentDictionary for _cache/_semaphores and a thread-safe queue
or ConcurrentQueue for cache keys) and use that synchronization around all
accesses in ConvertTextToSpeech and ResetCache (ensure reads/writes and
contains/add/remove on _semaphores and updates to _cache/_cacheKeysSeq are
atomic); change the semaphore removal logic so you Release() the SemaphoreSlim
before removing it (or remove it only while holding the same lock) to avoid the
race where another thread creates a new semaphore for the same cacheKey;
URL-encode or build the request URL via UriBuilder/HttpUtility.UrlEncode for
speaker, text and effect when creating requestUrl; and stop logging raw user
text/sensitive params in ConvertTextToSpeech and ResetCache—log only
non-sensitive identifiers (e.g., hash of text or masked speaker) and avoid
including the full query string.
- Around line 94-99: The code in TTSManager.cs builds requestUrl by
concatenating raw text, speaker and effect into the query string (see variables
text, speaker, effect and requestUrl) which breaks when text contains SSML
characters; encode each parameter before composing the URL (e.g. use
Uri.EscapeDataString or an equivalent URL-encoding helper on speaker, text and
effect) and then build requestUrl with the encoded values before calling
_httpClient.GetAsync; keep the existing null/default handling for effect but
apply encoding to the final value so special characters like <, >, & are safe.

In `@Content.Server/_Art/TTS/TTSSystem.cs`:
- Around line 151-153: В методе HandleWhisper() переменная distance уже содержит
линейное расстояние (вычислена через Length()), но код сравнивает её с
SharedChatSystem.VoiceRange * SharedChatSystem.VoiceRange, что неверно;
исправьте проверку либо на использование LengthSquared() при вычислении distance
и оставить сравнение с VoiceRange * VoiceRange, либо сохранить Length() и
сравнивать distance с SharedChatSystem.VoiceRange; см. переменные/вызовы
HandleWhisper(), distance, _xforms.GetWorldPosition(...) и
SharedChatSystem.VoiceRange чтобы внести одно из двух корректных изменений.

In `@Content.Server/_Art/TTS/TTSSystem.Sanitize.cs`:
- Around line 247-248: The current check in TTSSystem that returns String.Empty
when value >= (long)Math.Pow(10, 15) silently drops large numbers; change the
behavior in the Sanitize routine so it does not return an empty string—either
log a warning via the existing logger and return value.ToString() or, if
preserving original numeric format is desired, return the original numeric
string representation instead of String.Empty; update the branch containing "if
(value >= (long)Math.Pow(10, 15)) return String.Empty;" to perform the chosen
action and ensure the change uses the same method (e.g., the
Sanitize/SanitizeNumber method) and logging utilities already available in
TTSSystem.
- Around line 311-322: The declension logic is broken because NumberToText(long
value, ...) calls GetDeclension((int)(value % 10), ...) which hides numbers
11-14; change NumberToText to pass the full value (cast to int) into
GetDeclension (e.g., GetDeclension((int)value, valueDeclensionFor1,
valueDeclensionFor2, valueDeclensionFor5)) so GetDeclension can correctly
evaluate val % 100 for teens; update the call in the NumberToText method
accordingly.

In `@Content.Server/_Art/TTS/VoiceMaskSystem.TTS.cs`:
- Around line 21-30: OnChangeVoice currently allows msg.Voice == null to proceed
and sets entity.Comp.VoiceId to null, causing silent failures downstream; fix by
handling null explicitly: in OnChangeVoice check for msg.Voice == null and
either reject it (return early and show a failure popup via
_popupSystem.PopupEntity) or treat it as a reset and set entity.Comp.VoiceId =
TTSConfig.DefaultVoice before calling UpdateUI; keep the existing prototype
validation using _proto.HasIndex<TTSVoicePrototype>(id) for non-null ids and
ensure TTSSystem.TryIndex<TTSVoicePrototype>(voiceId) will never receive null.

In `@Content.Server/Entry/EntryPoint.cs`:
- Line 2: Добавить в класс TTSManager реализацию корректного освобождения
ресурсов: либо реализовать метод Shutdown() который закрывает/отменяет и
диспоузит внутренний HttpClient (и прочие ресурсы), либо реализовать
IDisposable.Dispose() и перенести туда логику очистки; затем в
EntryPoint.Dispose() вызвать _ttsManager.Shutdown() (или _ttsManager.Dispose()
если выбран IDisposable) рядом с вызовами для _dbManager, _playTimeTracking,
_serverApi и _discordLink, чтобы гарантировать корректное завершение TTSManager.

In `@Content.Shared/_Art/CVars/ArtCVars.cs`:
- Around line 20-21: The TTSClientEnabled CVar (public static readonly
CVarDef<bool> TTSClientEnabled) is declared with server/replicated flags but
should be a per-client opt-out setting; change its flags from CVar.SERVER |
CVar.REPLICATED | CVar.ARCHIVE to CVar.CLIENTONLY | CVar.ARCHIVE (matching
TTSVolume) so the client-local setting isn't overridden by server replication.

In `@Content.Shared/Humanoid/HumanoidProfileComponent.cs`:
- Around line 30-31: The new public field Voice on HumanoidProfileComponent is
missing the AutoNetworkedField attribute so it won't be included in the
AutoGenerateComponentState(true) network state; add the [AutoNetworkedField]
attribute to the Voice field (matching how Gender, Sex, Age, Species are
annotated) so it is included in the auto-generated network state and will sync
to clients.

In `@Content.Shared/Preferences/HumanoidCharacterProfile.cs`:
- Around line 39-40: Update the HumanoidCharacterProfile equality and hash code
implementations so the Voice property is considered: modify MemberwiseEquals
(the method that compares profile fields) to include a comparison of this.Voice
with other.Voice (using the same string comparison semantics used elsewhere,
e.g., String.Equals(...)), and update GetHashCode to incorporate Voice into the
computed hash (e.g., include Voice in HashCode.Combine or mix its string hash
into the existing hash), ensuring null-safe handling. Repeat the same change for
any analogous MemberwiseEquals/GetHashCode pairs in the other profile types
referenced (so a change of Voice will flip equality and the dirty-check/save
logic).

---

Nitpick comments:
In `@Content.Client/_Art/TTS/ContentAudioSystem.cs`:
- Around line 1-8: Добавьте XML-документацию над константой TtsMultiplier в
partial классе ContentAudioSystem, кратко объяснив, что это множитель
громкости/интенсивности для TTS (текст-в-речь), почему выбран коэффициент 3.0
(например, для приведения уровня TTS к уровню других звуков в клиенте) и где он
применяется (используется в AudioTab.xaml.cs для масштабирования
громкости/предпрослушивания TTS); пометьте комментарий примерным контекстом
использования и возможными побочными эффектами, чтобы будущие разработчики
понимали назначение и область применения константы.

In `@Content.Server/_Art/TTS/TTSSystem.Sanitize.cs`:
- Line 20: The Regex field DecimalSep uses a nonstandard character class
`[1-90]`; update the pattern to use a standard digit class for clarity and
correctness — e.g. replace `[1-90]` with `[0-9]` or `\d` in the DecimalSep
initialization (private static readonly Regex DecimalSep) so the pattern becomes
@"(?<=[0-9])(\.|,)(?=[0-9])" or use `\d` equivalents while preserving
RegexOptions.Compiled.
- Around line 36-48: Replace culture-dependent ToLower() calls with
ToLowerInvariant() in the two helper methods: in ReplaceLat2Cyr use
oneChar.Value.ToLowerInvariant() when looking up ReverseTranslit, and in
ReplaceMatchedWord use word.Value.ToLowerInvariant() when looking up
WordReplacement; keep the existing lookup and return logic otherwise.
- Around line 172-174: В словаре транслитерации в TTSSystem.Sanitize удалите из
инициализации словаря записи {"й","й"}, {"ъ","ъ"}, {"ь","ь"}, так как они
никогда не используются (регулярное выражение Latin обрабатывает только
латинские символы); подправьте инициализацию соответствующего transliteration
dictionary в классе/методе TTSSystem.Sanitize, удалив эти ключи-значения, и
сохраните остальную структуру словаря без других изменений.

In `@Content.Server/VoiceMask/VoiceMaskSystem.cs`:
- Line 195: The long single-line call to _uiSystem.SetUiState with
VoiceMaskUIKey.Key and a new VoiceMaskBuiState(...) is hard to read; reformat it
by breaking the VoiceMaskBuiState construction across multiple lines (one
argument per line or grouped logically) and align the parameters, referencing
the call site using _uiSystem.SetUiState, VoiceMaskUIKey.Key, and the
VoiceMaskBuiState constructor (which uses GetCurrentVoiceName(entity),
entity.Comp.VoiceId, entity.Comp.VoiceMaskSpeechVerb, entity.Comp.Active,
entity.Comp.AccentHide) so the line fits the style guide and is easier to scan.

In `@Content.Shared/_Art/TTS/PlayTTSEvent.cs`:
- Line 9: В конструкторе класса PlayTTSEvent добавьте валидацию для параметра,
инициализирующего свойство Data: проверьте, что массив не null и не пуст (и
опционально не превышает разумного максимального размера для сетевой передачи),
выбрасывая ArgumentNullException или ArgumentException с понятным сообщением;
при присвоении используйте копию массива (Array.Copy или .ToArray()) чтобы
сохранить неизменяемость внутреннего состояния; обновите соответствующий
конструктор и документацию для свойства Data.

In `@Content.Shared/_Art/TTS/TTSConfig.cs`:
- Around line 8-13: DefaultSexVoice is currently a public static readonly
Dictionary which allows external mutation; change its type to an
immutable/read-only collection (e.g. public static readonly
IReadOnlyDictionary<Sex,string> DefaultSexVoice or
ImmutableDictionary<Sex,string>) and initialize it with an immutable instance
(use ImmutableDictionary.CreateRange or call .ToImmutableDictionary() on the
literal) so callers cannot modify entries; update any call sites that rely on
mutability to treat it as read-only and keep the symbol name DefaultSexVoice and
the Sex enum unchanged.

In `@Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs`:
- Around line 9-19: Сделайте полезную нагрузку события иммутабельной: замените
публичные изменяемые поля Message, Voice, Source, Author в классе
TTSRadioPlayEvent на readonly-поля или публичные свойства только для чтения
(get; без set; или init-only), присваивая значения в конструкторе
TTSRadioPlayEvent(string message, string voice, NetEntity? source, NetEntity?
author); убедитесь, что класс по-прежнему наследует нужный тип (EntityEventArgs)
и что нигде в коде не производится мутация этих полей после создания события.

In `@Content.Shared/Humanoid/HumanoidProfileExportV1.cs`:
- Around line 65-68: The Voice field (ProtoId<TTSVoicePrototype> Voice) in
HumanoidProfileExportV1 is declared without a default, so older V1 profiles may
deserialize empty and break conversion; set a safe default value for Voice
(e.g., a known default TTS prototype id or an explicit empty/none sentinel) on
the field declaration and update the HumanoidCharacterProfile constructor/
conversion code that consumes HumanoidProfileExportV1 to treat missing/empty
Voice as that default; ensure references to Voice and the
HumanoidCharacterProfile constructor handle the default consistently.

In `@Resources/_Art/TTS/tts-voices.yml`:
- Around line 1-38: Удалите большой закомментированный генераторный код из файла
tts-voices.yml: уберите блок Emacs Lisp (функция f) и закомментированный
Python-скрипт (строка "#!/usr/bin/env python3" и последующий JSON-парсер),
оставив только валидный YAML с описаниями голосов; если нужен исходник генерации
— перенесите эти скрипты в отдельную утилиту/папку scripts или docs и добавьте
краткую ссылку/комментарий внизу YAML вместо вставки полного кода.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ee91980a-977f-4e9e-b7d7-9a8cfd232263

📥 Commits

Reviewing files that changed from the base of the PR and between 8afe04e and eaf589c.

📒 Files selected for processing (49)
  • Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs
  • Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
  • Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
  • Content.Client/Options/UI/Tabs/AudioTab.xaml
  • Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
  • Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
  • Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml
  • Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
  • Content.Client/_Art/TTS/ContentAudioSystem.cs
  • Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs
  • Content.Client/_Art/TTS/TTSSystem.cs
  • Content.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.cs
  • Content.Server.Database/Migrations/Postgres/20260327081423_AddTtsVoice.Designer.cs
  • Content.Server.Database/Migrations/Postgres/20260327081423_AddTtsVoice.cs
  • Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs
  • Content.Server.Database/Migrations/Sqlite/20260327081359_AddTtsVoice.Designer.cs
  • Content.Server.Database/Migrations/Sqlite/20260327081359_AddTtsVoice.cs
  • Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs
  • Content.Server.Database/Model.cs
  • Content.Server/Database/ServerDbBase.cs
  • Content.Server/Entry/EntryPoint.cs
  • Content.Server/IoC/ServerContentIoC.cs
  • Content.Server/Preferences/Managers/ServerPreferencesManager.cs
  • Content.Server/Radio/EntitySystems/HeadsetSystem.cs
  • Content.Server/Radio/EntitySystems/RadioSystem.cs
  • Content.Server/Radio/RadioEvent.cs
  • Content.Server/VoiceMask/VoiceMaskSystem.cs
  • Content.Server/_Art/TTS/TTSManager.cs
  • Content.Server/_Art/TTS/TTSSystem.RateLimit.cs
  • Content.Server/_Art/TTS/TTSSystem.SSML.cs
  • Content.Server/_Art/TTS/TTSSystem.Sanitize.cs
  • Content.Server/_Art/TTS/TTSSystem.cs
  • Content.Server/_Art/TTS/VoiceMaskSystem.TTS.cs
  • Content.Shared/Humanoid/HumanoidProfileComponent.cs
  • Content.Shared/Humanoid/HumanoidProfileExportV1.cs
  • Content.Shared/Inventory/InventorySystem.Relay.cs
  • Content.Shared/Preferences/HumanoidCharacterProfile.cs
  • Content.Shared/VoiceMask/SharedVoiceMaskSystem.cs
  • Content.Shared/VoiceMask/VoiceMaskComponent.cs
  • Content.Shared/_Art/CVars/ArtCVars.cs
  • Content.Shared/_Art/TTS/ClientOptionTTSEvent.cs
  • Content.Shared/_Art/TTS/PlayTTSEvent.cs
  • Content.Shared/_Art/TTS/RequestPreviewTTSEvent.cs
  • Content.Shared/_Art/TTS/SharedVoiceMaskSystem.cs
  • Content.Shared/_Art/TTS/TTSComponent.cs
  • Content.Shared/_Art/TTS/TTSConfig.cs
  • Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs
  • Content.Shared/_Art/TTS/TTSVoicePrototype.cs
  • Resources/_Art/TTS/tts-voices.yml

Comment on lines +14 to +29
private void InitializeVoice()
{
_voiceList = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.OrderBy(o => Loc.GetString(o.Name))
.OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
.ToList();

VoiceButton.OnItemSelected += args =>
{
VoiceButton.SelectId(args.Id);
SetVoice(_voiceList[args.Id].ID);
};

VoicePlayButton.OnPressed += _ => PlayPreviewTTS();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

InitializeVoice() не должен повторно навешивать обработчики.

Если этот метод вызывается больше одного раза, VoiceButton.OnItemSelected и VoicePlayButton.OnPressed начнут срабатывать по нескольку раз. Для preview это ещё и быстро упрётся в серверный rate limit.

💡 Предлагаемое исправление
+    private bool _voiceHandlersHooked;
+
     private void InitializeVoice()
     {
         _voiceList = _prototypeManager
             .EnumeratePrototypes<TTSVoicePrototype>()
             .OrderBy(o => Loc.GetString(o.Name))
             .OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
             .ToList();
 
-        VoiceButton.OnItemSelected += args =>
+        if (_voiceHandlersHooked)
+            return;
+
+        VoiceButton.OnItemSelected += args =>
         {
             VoiceButton.SelectId(args.Id);
             SetVoice(_voiceList[args.Id].ID);
         };
 
         VoicePlayButton.OnPressed += _ => PlayPreviewTTS();
+        _voiceHandlersHooked = true;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void InitializeVoice()
{
_voiceList = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.OrderBy(o => Loc.GetString(o.Name))
.OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
.ToList();
VoiceButton.OnItemSelected += args =>
{
VoiceButton.SelectId(args.Id);
SetVoice(_voiceList[args.Id].ID);
};
VoicePlayButton.OnPressed += _ => PlayPreviewTTS();
}
private bool _voiceHandlersHooked;
private void InitializeVoice()
{
_voiceList = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.OrderBy(o => Loc.GetString(o.Name))
.OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
.ToList();
if (_voiceHandlersHooked)
return;
VoiceButton.OnItemSelected += args =>
{
VoiceButton.SelectId(args.Id);
SetVoice(_voiceList[args.Id].ID);
};
VoicePlayButton.OnPressed += _ => PlayPreviewTTS();
_voiceHandlersHooked = true;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs` around lines 14 - 29,
InitializeVoice currently adds anonymous handlers every call causing duplicate
event firing; fix by extracting the handlers into named methods (e.g.,
OnVoiceSelected which calls VoiceButton.SelectId(...) and
SetVoice(_voiceList[args.Id].ID), and OnVoicePlayPressed which calls
PlayPreviewTTS), then subscribe those methods once (VoiceButton.OnItemSelected
+= OnVoiceSelected; VoicePlayButton.OnPressed += OnVoicePlayPressed) or
explicitly unsubscribe before subscribing (VoiceButton.OnItemSelected -=
OnVoiceSelected; VoiceButton.OnItemSelected += OnVoiceSelected; same for
VoicePlayButton) so repeated InitializeVoice calls won't attach duplicate
delegates; leave the _voiceList population as-is.

Comment on lines +96 to +102
if (_playing.ContainsKey(author)) continue; // If author is still talking right now.
if (!queue.TryDequeue(out var elem)) continue; // Just in case if queue cleared.
if (!TryGetEntity(elem.Source, out var local_source))
{ // If entity is outside PVS.
continue;
}
_playing[author] = PlayTTSFromUid(local_source, elem.Audio, elem.IsWhisper);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Не теряйте queued TTS, если SourceUid недоступен на клиенте.

Сейчас элемент сначала снимается с очереди, а потом безусловно выбрасывается, если TryGetEntity(elem.Source, ...) вернул false. Для событий с валидным Author, но null/invalid SourceUid, это даёт тихую потерю реплики вместо нормального глобального fallback.

💡 Предлагаемое исправление
-            if (!TryGetEntity(elem.Source, out var local_source))
-            { // If entity is outside PVS.
-                continue;
-            }
-            _playing[author] = PlayTTSFromUid(local_source, elem.Audio, elem.IsWhisper);
+            if (!TryGetEntity(elem.Source, out var localSource))
+            {
+                _playing[author] = PlayTTSFromUid(null, elem.Audio, elem.IsWhisper);
+                continue;
+            }
+
+            _playing[author] = PlayTTSFromUid(localSource, elem.Audio, elem.IsWhisper);

Also applies to: 147-171

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/_Art/TTS/TTSSystem.cs` around lines 96 - 102, The code
currently dequeues elem before verifying its SourceUid, causing queued TTS to be
lost when TryGetEntity(elem.Source, ...) returns false; change the logic to peek
at the next queue item (use TryPeek or equivalent) and only call
queue.TryDequeue when TryGetEntity(elem.Source, out local_source) succeeds, or
if Source is invalid but Author is present route the item to the global fallback
playback instead of dropping it; update the block that sets _playing[author] =
PlayTTSFromUid(...) (and the analogous block around lines 147-171) so dequeue
happens after entity validation or fallback dispatch.

Comment on lines +17 to +40
private void ReloadVoices()
{
if (_cfg is null)
return;
TTSContainer.Visible = _cfg.GetCVar(ArtCVars.TTSClientEnabled);
if (!_cfg.GetCVar(ArtCVars.TTSClientEnabled))
return;
VoiceSelector.OnItemSelected += args =>
{
VoiceSelector.SelectId(args.Id);
if (VoiceSelector.SelectedMetadata != null)
OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata);
};
_voices = _proto
.EnumeratePrototypes<TTSVoicePrototype>()
.OrderBy(o => Loc.GetString(o.Name))
.OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
.ToList();
for (var i = 0; i < _voices.Count; i++)
{
var name = Loc.GetString(_voices[i].Name);
VoiceSelector.AddItem(name);
VoiceSelector.SetItemMetadata(i, _voices[i].ID);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Сделайте ReloadVoices() идемпотентным.

Повторный вызов этого метода сейчас навешивает ещё один OnItemSelected и снова добавляет те же пункты в VoiceSelector. В итоге одно действие пользователя начнёт вызывать OnVoiceChange несколько раз, а список будет разрастаться дублями.

💡 Предлагаемое исправление
+    private bool _voiceSelectorHooked;
+
     private void ReloadVoices()
     {
         if (_cfg is null)
             return;
         TTSContainer.Visible = _cfg.GetCVar(ArtCVars.TTSClientEnabled);
         if (!_cfg.GetCVar(ArtCVars.TTSClientEnabled))
             return;
-        VoiceSelector.OnItemSelected += args =>
-        {
-            VoiceSelector.SelectId(args.Id);
-            if (VoiceSelector.SelectedMetadata != null)
-                OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata);
-        };
+        if (!_voiceSelectorHooked)
+        {
+            VoiceSelector.OnItemSelected += args =>
+            {
+                VoiceSelector.SelectId(args.Id);
+                if (VoiceSelector.SelectedMetadata is string voiceId)
+                    OnVoiceChange?.Invoke(voiceId);
+            };
+            _voiceSelectorHooked = true;
+        }
+
+        VoiceSelector.Clear();
         _voices = _proto
             .EnumeratePrototypes<TTSVoicePrototype>()
             .OrderBy(o => Loc.GetString(o.Name))
             .OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
             .ToList();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void ReloadVoices()
{
if (_cfg is null)
return;
TTSContainer.Visible = _cfg.GetCVar(ArtCVars.TTSClientEnabled);
if (!_cfg.GetCVar(ArtCVars.TTSClientEnabled))
return;
VoiceSelector.OnItemSelected += args =>
{
VoiceSelector.SelectId(args.Id);
if (VoiceSelector.SelectedMetadata != null)
OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata);
};
_voices = _proto
.EnumeratePrototypes<TTSVoicePrototype>()
.OrderBy(o => Loc.GetString(o.Name))
.OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
.ToList();
for (var i = 0; i < _voices.Count; i++)
{
var name = Loc.GetString(_voices[i].Name);
VoiceSelector.AddItem(name);
VoiceSelector.SetItemMetadata(i, _voices[i].ID);
}
private bool _voiceSelectorHooked;
private void ReloadVoices()
{
if (_cfg is null)
return;
TTSContainer.Visible = _cfg.GetCVar(ArtCVars.TTSClientEnabled);
if (!_cfg.GetCVar(ArtCVars.TTSClientEnabled))
return;
if (!_voiceSelectorHooked)
{
VoiceSelector.OnItemSelected += args =>
{
VoiceSelector.SelectId(args.Id);
if (VoiceSelector.SelectedMetadata is string voiceId)
OnVoiceChange?.Invoke(voiceId);
};
_voiceSelectorHooked = true;
}
VoiceSelector.Clear();
_voices = _proto
.EnumeratePrototypes<TTSVoicePrototype>()
.OrderBy(o => Loc.GetString(o.Name))
.OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
.ToList();
for (var i = 0; i < _voices.Count; i++)
{
var name = Loc.GetString(_voices[i].Name);
VoiceSelector.AddItem(name);
VoiceSelector.SetItemMetadata(i, _voices[i].ID);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.cs` around lines 17 -
40, ReloadVoices is not idempotent because it keeps += registering the same
handler and keeps appending items; before adding the handler and items remove
any previous handler and clear the selector: store the selection delegate in a
private field (e.g. private Action<YourArgsType> _voiceSelectionHandler or
EventHandler-like _voiceSelectionHandler), call VoiceSelector.OnItemSelected -=
_voiceSelectionHandler (or set to null if the control allows) before assigning
`_voiceSelectionHandler = args => { VoiceSelector.SelectId(args.Id); if
(VoiceSelector.SelectedMetadata != null)
OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata); };` and then
`VoiceSelector.OnItemSelected += _voiceSelectionHandler`; also clear existing
entries before repopulating the list (e.g. call VoiceSelector.ClearItems() or
VoiceSelector.Items.Clear()) so the loop that calls VoiceSelector.AddItem /
SetItemMetadata does not duplicate entries; keep the rest of the logic (_voices
population, SelectId usage, OnVoiceChange invoke) unchanged.

<BoxContainer Orientation="Vertical" Margin="0 3 0 0">
<ui:OptionSlider Name="SliderVolumeMaster" Title="{Loc 'ui-options-master-volume'}"
Margin="0 0 0 8" />
<ui:OptionSlider Name="SliderVolumeTts" Title="Громкость TTS:"/> <!-- Art-TTS -->
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Локализуйте новые UI-строки через Loc.

На Line 11 и Line 19 добавлены хардкод-строки, что ломает единый i18n-поток для интерфейса настроек.

💡 Предлагаемый фикс
-                <ui:OptionSlider Name="SliderVolumeTts" Title="Громкость TTS:"/> <!-- Art-TTS  -->
+                <ui:OptionSlider Name="SliderVolumeTts" Title="{Loc 'ui-options-tts-volume'}"/> <!-- Art-TTS  -->
...
-                <CheckBox Name="TtsClientCheckBox" Text="Включить ТТС"/> <!-- Art-TTS -->
+                <CheckBox Name="TtsClientCheckBox" Text="{Loc 'ui-options-tts-enabled'}"/> <!-- Art-TTS -->

Also applies to: 19-19

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/Options/UI/Tabs/AudioTab.xaml` at line 11, Replace the
hard-coded Title text on the new UI controls (e.g., the OptionSlider named
SliderVolumeTts and the other control added at the same location) with the
localization helper (Loc) so strings flow through the i18n pipeline; update the
XAML Title attributes to use the Loc markup extension or binding (e.g.,
Title="{Loc Key=...}" or equivalent used elsewhere) and add matching resource
keys for those strings to the shared resource file so the UI reads localized
text instead of literal Russian text.

Comment on lines +89 to +93
// Art-TTS Start
var voiceIdx = _voices.FindIndex(v => v.ID == voice);
if (voiceIdx != -1)
VoiceSelector.Select(voiceIdx);
// Art-TTS End
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Поиск объявления поля _voices в файлах VoiceMaskNameChangeWindow
fd -e cs -e xaml 'VoiceMaskNameChangeWindow' --exec grep -l "_voices" {} \;
rg -n "_voices" --type cs -C 3

Repository: ss14-art/open-space

Length of output: 3171


🏁 Script executed:

wc -l Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs

Repository: ss14-art/open-space

Length of output: 124


🏁 Script executed:

cat -n Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs | head -50

Repository: ss14-art/open-space

Length of output: 1879


🏁 Script executed:

rg -n "private.*_voices|_voices\s*=" "Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs"

Repository: ss14-art/open-space

Length of output: 45


🏁 Script executed:

cat -n Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs | tail -50

Repository: ss14-art/open-space

Length of output: 1834


🏁 Script executed:

rg -n "ReloadVoices\|class VoiceMaskNameChangeWindow" "Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs"

Repository: ss14-art/open-space

Length of output: 45


🏁 Script executed:

fd -type f -name "*.cs" "Content.Client/VoiceMask/"

Repository: ss14-art/open-space

Length of output: 233


🏁 Script executed:

rg -l "VoiceMaskNameChangeWindow" "Content.Client/VoiceMask/"

Repository: ss14-art/open-space

Length of output: 177


🏁 Script executed:

cat Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs | head -100

Repository: ss14-art/open-space

Length of output: 1831


🏁 Script executed:

rg -n "_voices|ReloadVoices" "Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs"

Repository: ss14-art/open-space

Length of output: 45


🏁 Script executed:

rg -n "private.*ReloadVoices\|public.*ReloadVoices" "Content.Client/"

Repository: ss14-art/open-space

Length of output: 45


🏁 Script executed:

grep -n "ReloadVoices" "Content.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.cs"

Repository: ss14-art/open-space

Length of output: 97


Поле _voices и метод ReloadVoices() не определены в этом классе.

Класс VoiceMaskNameChangeWindow в пространстве имён Content.Client.VoiceMask использует поле _voices (строка 90) и вызывает метод ReloadVoices() (строка 31), которые не объявлены в этом файле. Эти члены существуют в другом классе VoiceMaskNameChangeWindow в пространстве имён Content.Client._Art.TTS, но это отдельные классы, а не части одного partial класса. Необходимо добавить объявление поля _voices: List<TTSVoicePrototype> и реализацию метода ReloadVoices() в класс Content.Client.VoiceMask.VoiceMaskNameChangeWindow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs` around lines 89 -
93, Добавьте в класс Content.Client.VoiceMask.VoiceMaskNameChangeWindow поле
List<TTSVoicePrototype> _voices и реализуйте метод ReloadVoices() — метод должен
заполнять _voices (например, из того же источника, что и в
Content.Client._Art.TTS версия класса), обновлять элементы селектора
VoiceSelector (Items/ItemsSource) и корректно выставлять текущий выбранный
элемент; убедитесь, что добавлены нужные using (System.Collections.Generic и
пространство имён, где объявлен TTSVoicePrototype) и что имена методов/полей
соответствуют существующему коду (включая _voices, ReloadVoices, VoiceSelector,
VoiceMaskNameChangeWindow).

Comment on lines +21 to +30
private void OnChangeVoice(Entity<VoiceMaskComponent> entity, ref VoiceMaskChangeVoiceMessage msg)
{
if (msg.Voice is { } id && !_proto.HasIndex<TTSVoicePrototype>(id))
return;

entity.Comp.VoiceId = msg.Voice;

_popupSystem.PopupEntity(Loc.GetString("voice-mask-voice-popup-success"), entity);

UpdateUI(entity);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Поиск использования VoiceId и проверка обработки null значений
rg -n "VoiceId" --type cs -C 3 | head -100

Repository: ss14-art/open-space

Length of output: 4828


🏁 Script executed:

# Check OnSpeakerVoiceTransform usage and how null VoiceId is handled
rg -n "OnSpeakerVoiceTransform|TransformSpeakerVoiceEvent" --type cs -B 2 -A 8 | head -80

Repository: ss14-art/open-space

Length of output: 4621


🏁 Script executed:

# Check how TTSSystem handles voiceId, particularly in TTSSpeak and related methods
rg -n "voiceId" Content.Server/_Art/TTS/TTSSystem.cs -B 2 -A 2

Repository: ss14-art/open-space

Length of output: 651


🏁 Script executed:

# Check if there are null checks for VoiceId before using it
rg -n "VoiceId\s*\?" --type cs

Repository: ss14-art/open-space

Length of output: 45


🏁 Script executed:

# Find TTSConfig.DefaultVoice definition and usage
rg -n "DefaultVoice|TTSConfig" --type cs | head -40

Repository: ss14-art/open-space

Length of output: 959


🏁 Script executed:

# Check GetCurrentVoiceName implementation
rg -n "GetCurrentVoiceName" --type cs -A 10

Repository: ss14-art/open-space

Length of output: 2737


Логика валидации позволяет установить null в VoiceId, что приводит к нежелательному поведению.

Текущая проверка на строке 23 отклоняет только невалидные голоса, но пропускает null:

if (msg.Voice is { } id && !_proto.HasIndex<TTSVoicePrototype>(id))
    return;

Когда msg.Voice == null, условие ложно и выполнение продолжается, устанавливая entity.Comp.VoiceId = null. При этом выводится сообщение об успехе (строка 28), но:

  1. VoiceId инициализируется константой TTSConfig.DefaultVoice (никогда не null по умолчанию)
  2. TTSSystem на строке 103 вызывает TryIndex<TTSVoicePrototype>(voiceId) с null, что заканчивается молчаливым отказом
  3. Пользователь видит успешное изменение, но голос не меняется

Либо добавьте явную проверку для отклонения null, либо явно задокументируйте и обработайте случай, когда null означает возврат к голосу по умолчанию.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/_Art/TTS/VoiceMaskSystem.TTS.cs` around lines 21 - 30,
OnChangeVoice currently allows msg.Voice == null to proceed and sets
entity.Comp.VoiceId to null, causing silent failures downstream; fix by handling
null explicitly: in OnChangeVoice check for msg.Voice == null and either reject
it (return early and show a failure popup via _popupSystem.PopupEntity) or treat
it as a reset and set entity.Comp.VoiceId = TTSConfig.DefaultVoice before
calling UpdateUI; keep the existing prototype validation using
_proto.HasIndex<TTSVoicePrototype>(id) for non-null ids and ensure
TTSSystem.TryIndex<TTSVoicePrototype>(voiceId) will never receive null.

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Content.Server._Art.TTS; // Art-TTS
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Проверяем, есть ли метод Shutdown в TTSManager
ast-grep --pattern $'class TTSManager {
  $$$
  Shutdown($$$) {
    $$$
  }
  $$$
}'

# Также ищем IDisposable или другие методы очистки
rg -n "class TTSManager" -A 30 --type cs

Repository: ss14-art/open-space

Length of output: 2470


🏁 Script executed:

#!/bin/bash
# Получаем полный файл TTSManager для анализа методов
wc -l Content.Server/_Art/TTS/TTSManager.cs

# Проверяем наличие методов Shutdown, Dispose в TTSManager
rg -n "public|private|protected.*(?:Shutdown|Dispose)" Content.Server/_Art/TTS/TTSManager.cs

# Проверяем наличие интерфейса IDisposable
rg -n "IDisposable|: IDisposable" Content.Server/_Art/TTS/TTSManager.cs

# Смотрим структуру методов в классе
rg -n "^\s*(public|private|protected)\s+(async\s+)?(void|Task|bool|string)" Content.Server/_Art/TTS/TTSManager.cs

Repository: ss14-art/open-space

Length of output: 2932


🏁 Script executed:

#!/bin/bash
# Читаем полный файл TTSManager для подтверждения отсутствия Shutdown/Dispose
cat -n Content.Server/_Art/TTS/TTSManager.cs | tail -100

# Проверяем метод Dispose в EntryPoint
rg -n "Dispose|Shutdown" Content.Server/Entry/EntryPoint.cs -A 3 -B 1

Repository: ss14-art/open-space

Length of output: 4991


Требуется реализовать методы очистки для TTSManager.

TTSManager не имеет методов Shutdown() или Dispose(), в отличие от других менеджеров (_dbManager, _playTimeTracking, _serverApi, _discordLink), которые вызываются в методе Dispose() EntryPoint. Поскольку TTSManager содержит поле HttpClient, необходимо:

  1. Реализовать метод Shutdown() в TTSManager для корректного завершения работы (или реализовать IDisposable)
  2. Добавить вызов _ttsManager.Shutdown() в метод Dispose() EntryPoint, сохраняя консистентность с другими менеджерами
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/Entry/EntryPoint.cs` at line 2, Добавить в класс TTSManager
реализацию корректного освобождения ресурсов: либо реализовать метод Shutdown()
который закрывает/отменяет и диспоузит внутренний HttpClient (и прочие ресурсы),
либо реализовать IDisposable.Dispose() и перенести туда логику очистки; затем в
EntryPoint.Dispose() вызвать _ttsManager.Shutdown() (или _ttsManager.Dispose()
если выбран IDisposable) рядом с вызовами для _dbManager, _playTimeTracking,
_serverApi и _discordLink, чтобы гарантировать корректное завершение TTSManager.

Comment on lines +20 to +21
public static readonly CVarDef<bool> TTSClientEnabled =
CVarDef.Create("tts.enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the ArtCVars.cs file and examine its content
find . -name "ArtCVars.cs" -type f | head -20

Repository: ss14-art/open-space

Length of output: 102


🏁 Script executed:

# Search for TTSClientEnabled usage throughout the codebase
rg "TTSClientEnabled" -n --no-heading

Repository: ss14-art/open-space

Length of output: 1061


🏁 Script executed:

# Search for ClientOptionTTSEvent to understand how TTS settings are handled
rg "ClientOptionTTSEvent" -n --no-heading -A 5 -B 2

Repository: ss14-art/open-space

Length of output: 2455


🏁 Script executed:

# Look for CVar flag definitions and their meanings
rg "CVar\.(SERVER|REPLICATED|CLIENTONLY|ARCHIVE)" -n --no-heading -B 2 -A 2 | head -50

Repository: ss14-art/open-space

Length of output: 3259


🏁 Script executed:

# Find other CVar definitions to see common patterns for client settings
rg "CVarDef\.Create" -n --no-heading -A 1 | grep -E "(CLIENTONLY|CVar\.CLIENT)" | head -20

Repository: ss14-art/open-space

Length of output: 2765


tts.enabled объявлена с флагами CVar.SERVER | CVar.REPLICATED, но должна быть клиентской настройкой.

По коду видно, что это opt-out опция каждого клиента. Клиент отправляет ClientOptionTTSEvent на сервер для синхронизации. С текущими флагами сервер будет реплицировать значение всем клиентам, что переопределит локальную настройку пользователя. Должны быть флаги CVar.CLIENTONLY | CVar.ARCHIVE (как и для похожей настройки TTSVolume).

Фикс
-    public static readonly CVarDef<bool> TTSClientEnabled =
-        CVarDef.Create("tts.enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
+    public static readonly CVarDef<bool> TTSClientEnabled =
+        CVarDef.Create("tts.enabled", false, CVar.CLIENTONLY | CVar.ARCHIVE);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static readonly CVarDef<bool> TTSClientEnabled =
CVarDef.Create("tts.enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
public static readonly CVarDef<bool> TTSClientEnabled =
CVarDef.Create("tts.enabled", false, CVar.CLIENTONLY | CVar.ARCHIVE);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/_Art/CVars/ArtCVars.cs` around lines 20 - 21, The
TTSClientEnabled CVar (public static readonly CVarDef<bool> TTSClientEnabled) is
declared with server/replicated flags but should be a per-client opt-out
setting; change its flags from CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE to
CVar.CLIENTONLY | CVar.ARCHIVE (matching TTSVolume) so the client-local setting
isn't overridden by server replication.

Comment on lines +30 to +31
[DataField("voice")]
public ProtoId<TTSVoicePrototype> Voice = TTSConfig.DefaultVoice; // HumanoidCharacterProfile
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Проверка, что поле Voice в HumanoidProfileComponent помечено как сетевое.
set -euo pipefail

FILE="$(fd -i 'HumanoidProfileComponent.cs' | head -n1)"
echo "Inspecting: $FILE"
sed -n '1,120p' "$FILE"

echo
echo "Relevant annotations/fields:"
rg -n -C2 'AutoGenerateComponentState\(true\)|AutoNetworkedField|DataField\("voice"\)|\bVoice\s*=' "$FILE"

Repository: ss14-art/open-space

Length of output: 1945


Поле Voice отсутствует атрибут AutoNetworkedField для синхронизации в сетевом компоненте.

На строке 30-31 новое поле Voice не имеет AutoNetworkedField, в то время как все остальные поля в компоненте (Gender, Sex, Age, Species), которые должны синхронизироваться, помечены этим атрибутом. Компонент использует AutoGenerateComponentState(true), поэтому без AutoNetworkedField поле Voice не будет включено в автоматически генерируемый сетевой state и не будет синхронизироваться на клиентов.

Предлагаемое исправление
-    [DataField("voice")]
+    [DataField("voice"), AutoNetworkedField]
     public ProtoId<TTSVoicePrototype> Voice = TTSConfig.DefaultVoice; // HumanoidCharacterProfile
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[DataField("voice")]
public ProtoId<TTSVoicePrototype> Voice = TTSConfig.DefaultVoice; // HumanoidCharacterProfile
[DataField("voice"), AutoNetworkedField]
public ProtoId<TTSVoicePrototype> Voice = TTSConfig.DefaultVoice; // HumanoidCharacterProfile
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/Humanoid/HumanoidProfileComponent.cs` around lines 30 - 31,
The new public field Voice on HumanoidProfileComponent is missing the
AutoNetworkedField attribute so it won't be included in the
AutoGenerateComponentState(true) network state; add the [AutoNetworkedField]
attribute to the Voice field (matching how Gender, Sex, Age, Species are
annotated) so it is included in the auto-generated network state and will sync
to clients.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Content.Shared/Humanoid/HumanoidProfileSystem.cs`:
- Around line 33-37: В методе ApplyProfileTo/в блоке где присваивается
ent.Comp.Voice = profile.Voice удалите проверку на
_TTSComponent.VoicePrototypeId == null и всегда присваивайте
_TTSComponent.VoicePrototypeId = profile.Voice при наличии компонента (используя
TryComp<TTSComponent>(ent, out var _TTSComponent)), чтобы голос из профиля
всегда синхронизировался с TTSComponent; оставьте саму проверку наличия
компонента (TryComp) без условия по значению VoicePrototypeId.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1810ab21-e76b-42d6-9664-e4d5481c0e88

📥 Commits

Reviewing files that changed from the base of the PR and between eaf589c and ebda1ce.

📒 Files selected for processing (8)
  • Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
  • Content.Server/Preferences/Managers/ServerPreferencesManager.cs
  • Content.Shared/Humanoid/HumanoidProfileComponent.cs
  • Content.Shared/Humanoid/HumanoidProfileExportV1.cs
  • Content.Shared/Humanoid/HumanoidProfileSystem.cs
  • Content.Shared/Preferences/HumanoidCharacterProfile.cs
  • Content.Shared/_Art/TTS/TTSComponent.cs
  • Content.Shared/_Art/TTS/TTSConfig.cs
✅ Files skipped from review due to trivial changes (3)
  • Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
  • Content.Shared/_Art/TTS/TTSConfig.cs
  • Content.Shared/_Art/TTS/TTSComponent.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • Content.Shared/Humanoid/HumanoidProfileComponent.cs
  • Content.Shared/Humanoid/HumanoidProfileExportV1.cs

Comment on lines +33 to +37
ent.Comp.Voice = profile.Voice;
if (TryComp<TTSComponent>(ent, out var _TTSComponent) && _TTSComponent.VoicePrototypeId == null)
{
_TTSComponent.VoicePrototypeId = profile.Voice;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'TTSComponent definition:\n'
rg -n -C3 '\bclass\s+TTSComponent\b|\bVoicePrototypeId\b' --type=cs

printf '\nAssignments to VoicePrototypeId in code:\n'
rg -n -C2 '\bVoicePrototypeId\s*=' --type=cs

printf '\nPrototype defaults for VoicePrototypeId:\n'
rg -n -C2 'VoicePrototypeId|voicePrototypeId' --glob '*.yml' --glob '*.yaml'

printf '\nApplyProfileTo call sites:\n'
rg -n -C2 '\bApplyProfileTo\s*\(' --type=cs

Repository: ss14-art/open-space

Length of output: 3999


Удалите условие == null, чтобы синхронизировать голос при любом применении профиля.

TTSComponent.VoicePrototypeId имеет дефолт "papich" (а не null), поэтому условие _TTSComponent.VoicePrototypeId == null никогда не будет истинным после инициализации сущности. Это значит, что при вызове ApplyProfileTo() голос из профиля никогда не будет скопирован в TTSComponent, и сущность будет говорить дефолтным голосом, а не выбранным в профиле.

Предлагаемое исправление
         ent.Comp.Voice = profile.Voice;
-        if (TryComp<TTSComponent>(ent, out var _TTSComponent) && _TTSComponent.VoicePrototypeId == null)
+        if (TryComp<TTSComponent>(ent, out var ttsComponent))
         {
-            _TTSComponent.VoicePrototypeId = profile.Voice;
+            ttsComponent.VoicePrototypeId = profile.Voice;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ent.Comp.Voice = profile.Voice;
if (TryComp<TTSComponent>(ent, out var _TTSComponent) && _TTSComponent.VoicePrototypeId == null)
{
_TTSComponent.VoicePrototypeId = profile.Voice;
}
ent.Comp.Voice = profile.Voice;
if (TryComp<TTSComponent>(ent, out var ttsComponent))
{
ttsComponent.VoicePrototypeId = profile.Voice;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Shared/Humanoid/HumanoidProfileSystem.cs` around lines 33 - 37, В
методе ApplyProfileTo/в блоке где присваивается ent.Comp.Voice = profile.Voice
удалите проверку на _TTSComponent.VoicePrototypeId == null и всегда присваивайте
_TTSComponent.VoicePrototypeId = profile.Voice при наличии компонента (используя
TryComp<TTSComponent>(ent, out var _TTSComponent)), чтобы голос из профиля
всегда синхронизировался с TTSComponent; оставьте саму проверку наличия
компонента (TryComp) без условия по значению VoicePrototypeId.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant