From a4a409c1d6c143e9e5a467a134eea59d26e362bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:28:44 +0000 Subject: [PATCH 01/12] Initial plan From 2646c392e4865ffd2b8ce2a4bea2980c0cd93958 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:32:00 +0000 Subject: [PATCH 02/12] Add GitHub workflow to build a signed APK and upload as artifact Co-authored-by: LeMyst <1592048+LeMyst@users.noreply.github.com> --- .github/workflows/build-apk.yml | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/build-apk.yml diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml new file mode 100644 index 000000000..b52fe1fc1 --- /dev/null +++ b/.github/workflows/build-apk.yml @@ -0,0 +1,51 @@ +name: Build Signed APK + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-apk: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + + - name: Install dependencies + run: flutter pub get + + - name: Decode keystore + env: + UPLOAD_KEYSTORE: ${{ secrets.UPLOAD_KEYSTORE }} + run: echo "$UPLOAD_KEYSTORE" | base64 --decode > "$RUNNER_TEMP/upload-keystore.jks" + + - name: Build signed APK + env: + KEY_ALIAS: upload + KEY_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} + STORE_FILE: ${{ runner.temp }}/upload-keystore.jks + STORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} + run: flutter build apk --release + + - name: Upload signed APK + uses: actions/upload-artifact@v4 + with: + name: signed-apk + path: build/app/outputs/flutter-apk/app-release.apk From 4a46e826f2693ea87a2b2dfbbfc1f4b0fead422e Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:17:26 +0100 Subject: [PATCH 03/12] Remove rtchat GitHub Actions workflow Delete .github/workflows/rtchat.yml which defined the RealtimeChat App CI workflow. The removed file contained iOS and Android jobs that referenced muxable reusable workflows, concurrency groups, environment/publish settings, environment URLs, package name, and required secrets for App Store and Google Play publishing. --- .github/workflows/rtchat.yml | 38 ------------------------------------ 1 file changed, 38 deletions(-) delete mode 100644 .github/workflows/rtchat.yml diff --git a/.github/workflows/rtchat.yml b/.github/workflows/rtchat.yml deleted file mode 100644 index e95c97cd0..000000000 --- a/.github/workflows/rtchat.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: RealtimeChat App - -on: - push: - branches: - - main - pull_request: - branches: - - main - release: - types: [released] - workflow_dispatch: - -jobs: - ios: - uses: muxable/.github/.github/workflows/flutter-ios.yml@main - concurrency: - group: ios-${{ github.ref }} - with: - environment-name: ios-${{ github.event_name == 'release' && 'app-store' || 'testflight' }} - environment-url: https://appstoreconnect.apple.com/apps/1567720948/${{ github.event_name == 'release' && 'appstore' || 'testflight' }} - publish: ${{ github.event_name != 'pull_request' }} - secrets: - APPSTORE_API_PRIVATE_KEY: ${{ secrets.APPSTORE_API_PRIVATE_KEY }} - APPLE_CERTIFICATE_PRIVATE_KEY: ${{ secrets.APPLE_CERTIFICATE_PRIVATE_KEY }} - android: - uses: muxable/.github/.github/workflows/flutter-android.yml@main - concurrency: - group: android-${{ github.ref }} - with: - environment-name: android-${{ github.event_name == 'release' && 'production' || 'internal' }} - environment-url: https://play.google.com/console/developers/8168733962061318282/app/4973471021781937122/tracks/${{ github.event_name == 'release' && 'production' || 'internal-testing' }} - package-name: com.rtirl.chat - publish: ${{ github.event_name != 'pull_request' }} - secrets: - UPLOAD_KEYSTORE: ${{ secrets.UPLOAD_KEYSTORE }} - UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} - GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} From 7aee431dd6e6ade5bdd551f62d0b8b61d41b187c Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:04:58 +0100 Subject: [PATCH 04/12] ci: update flutter analyze workflow to allow non-fatal warnings --- .github/workflows/flutter-analyze.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/flutter-analyze.yml b/.github/workflows/flutter-analyze.yml index b2430258c..fd706bd82 100644 --- a/.github/workflows/flutter-analyze.yml +++ b/.github/workflows/flutter-analyze.yml @@ -20,3 +20,5 @@ jobs: - run: flutter pub get - name: Run flutter analyze uses: invertase/github-action-dart-analyzer@v3 + with: + fatal-warnings: false From e320864bb21b86af8e76636f35e33b3b2c082a52 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:55:06 +0100 Subject: [PATCH 05/12] Update .github/workflows/build-apk.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/build-apk.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml index b52fe1fc1..678ffe5ce 100644 --- a/.github/workflows/build-apk.yml +++ b/.github/workflows/build-apk.yml @@ -34,7 +34,7 @@ jobs: - name: Decode keystore env: UPLOAD_KEYSTORE: ${{ secrets.UPLOAD_KEYSTORE }} - run: echo "$UPLOAD_KEYSTORE" | base64 --decode > "$RUNNER_TEMP/upload-keystore.jks" + run: printf '%s' "$UPLOAD_KEYSTORE" | base64 --decode > "$RUNNER_TEMP/upload-keystore.jks" - name: Build signed APK env: From 814f53cab4a6014810bcca26e295509689e33b54 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:59:34 +0100 Subject: [PATCH 06/12] ci: add GitHub workflow steps to build and upload debug and signed APKs --- .github/workflows/build-apk.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml index 678ffe5ce..f3b44c50a 100644 --- a/.github/workflows/build-apk.yml +++ b/.github/workflows/build-apk.yml @@ -31,12 +31,21 @@ jobs: - name: Install dependencies run: flutter pub get + # Build debug APK for external PRs (forks) + - name: Build Debug APK + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository + run: flutter build apk --debug + + # Decode keystore: Runs on push, dispatch, OR internal PRs - name: Decode keystore + if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) env: UPLOAD_KEYSTORE: ${{ secrets.UPLOAD_KEYSTORE }} run: printf '%s' "$UPLOAD_KEYSTORE" | base64 --decode > "$RUNNER_TEMP/upload-keystore.jks" + # Build signed APK: Runs on push, dispatch, OR internal PRs - name: Build signed APK + if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) env: KEY_ALIAS: upload KEY_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} @@ -44,7 +53,15 @@ jobs: STORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} run: flutter build apk --release + - name: Upload Debug APK + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository + uses: actions/upload-artifact@v4 + with: + name: debug-apk + path: build/app/outputs/flutter-apk/app-debug.apk + - name: Upload signed APK + if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) uses: actions/upload-artifact@v4 with: name: signed-apk From 386a034ce993d0418872f1fffd35d29e76fa4742 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Sun, 22 Feb 2026 09:46:57 +0100 Subject: [PATCH 07/12] Add isSubscriber helper; use in TTS Introduce TwitchMessageModel.isSubscriber which inspects the badges-raw tag to determine subscription status (handles null/empty safely). Update TtsModel to use this helper for the subscribers-only check instead of directly indexing tags, making the gating logic more robust and null-safe. --- lib/models/messages/twitch/message.dart | 8 ++++++++ lib/models/tts.dart | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/models/messages/twitch/message.dart b/lib/models/messages/twitch/message.dart index 6a566bd6e..641427855 100644 --- a/lib/models/messages/twitch/message.dart +++ b/lib/models/messages/twitch/message.dart @@ -265,6 +265,14 @@ class TwitchMessageModel extends MessageModel { return tags['badges']['moderator'] != null; } + bool get isSubscriber { + final badgesRaw = tags['badges-raw'] as String?; + if (badgesRaw == null || badgesRaw.isEmpty) { + return false; + } + return badgesRaw.contains('subscriber/'); + } + List get tokenized => _tokenized ??= tokenize(); List? _tokenized; List tokenize() { diff --git a/lib/models/tts.dart b/lib/models/tts.dart index b92706b77..0b53ef06e 100644 --- a/lib/models/tts.dart +++ b/lib/models/tts.dart @@ -384,7 +384,7 @@ class TtsModel extends ChangeNotifier { } if (model is TwitchMessageModel) { - if (_isSubscribersOnly && (model.tags['badges']?['subscriber'] == null)) { + if (_isSubscribersOnly && !model.isSubscriber) { return; } From fdd1f7083043ec0796f9720cb3b40dda007fca35 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Sun, 22 Feb 2026 09:50:17 +0100 Subject: [PATCH 08/12] Add TTS command-only mode and settings toggle Introduce a new isTtsCommandEncouraged flag to TtsModel and expose it in the TTS settings UI as a Switch. When enabled, messages must start with the "!v" prefix to be spoken; the prefix is stripped from the vocalized text. Adjusted message gating logic to separately handle bot muting and command checks, added getters/setters, included the new flag in JSON (de)serialization, and added debug logging for prefix handling. --- lib/models/tts.dart | 34 ++++++++++++++++++++++++++++++++-- lib/screens/settings/tts.dart | 7 +++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/lib/models/tts.dart b/lib/models/tts.dart index b92706b77..f59130747 100644 --- a/lib/models/tts.dart +++ b/lib/models/tts.dart @@ -54,6 +54,7 @@ class TtsModel extends ChangeNotifier { var _isBotMuted = false; var _isEmoteMuted = false; var _isPreludeMuted = false; + var _isTtsCommandEncouraged = false; var _speed = Platform.isAndroid ? 0.8 : 0.395; var _pitch = 1.0; var _mode = TtsMode.disabled; @@ -124,7 +125,7 @@ class TtsModel extends ChangeNotifier { String getVocalization(AppLocalizations l10n, MessageModel model, {bool includeAuthorPrelude = false}) { if (model is TwitchMessageModel) { - final text = model.tokenized + var text = model.tokenized .where((token) => token is TextToken || (!_isEmoteMuted && token is EmoteToken) || @@ -141,6 +142,12 @@ class TtsModel extends ChangeNotifier { return token.url.host; } }).join(""); + + if (text.toLowerCase().startsWith("!v ")) { + debugPrint("Message starts with TTS command prefix: $text"); + text = text.substring("!v".length).trim(); + } + if (text.trim().isEmpty) { return ""; } @@ -328,6 +335,15 @@ class TtsModel extends ChangeNotifier { notifyListeners(); } + bool get isTtsCommandEncouraged { + return _isTtsCommandEncouraged; + } + + set isTtsCommandEncouraged(bool value) { + _isTtsCommandEncouraged = value; + notifyListeners(); + } + bool get isCloudTtsEnabled { return _isCloudTtsEnabled; } @@ -388,13 +404,23 @@ class TtsModel extends ChangeNotifier { return; } + if (_isTtsCommandEncouraged && + !model.message.toLowerCase().startsWith("!v ")) { + debugPrint("Message does not start with TTS command prefix: ${model.message}"); + return; + } + if (_mutedUsers.any((user) => user.displayName?.toLowerCase() == model.author.displayName?.toLowerCase())) { return; } - if ((_isBotMuted && model.author.isBot) || model.isCommand) { + if (_isBotMuted && model.author.isBot) { + return; + } + + if(model.isCommand && !model.message.toLowerCase().startsWith("!v")) { return; } } @@ -497,6 +523,9 @@ class TtsModel extends ChangeNotifier { if (json['isBotMuted'] != null) { _isBotMuted = json['isBotMuted']; } + if (json['isTtsCommandEncouraged'] != null) { + _isTtsCommandEncouraged = json['isTtsCommandEncouraged']; + } if (json['pitch'] != null) { _pitch = json['pitch']; } @@ -534,6 +563,7 @@ class TtsModel extends ChangeNotifier { "isBotMuted": isBotMuted, "isEmoteMuted": isEmoteMuted, "isPreludeMuted": isPreludeMuted, + "isTtsCommandEncouraged": isTtsCommandEncouraged, "isRandomVoiceEnabled": isRandomVoiceEnabled, "language": language.languageCode, "pitch": pitch, diff --git a/lib/screens/settings/tts.dart b/lib/screens/settings/tts.dart index 0f7e75381..292db7718 100644 --- a/lib/screens/settings/tts.dart +++ b/lib/screens/settings/tts.dart @@ -271,6 +271,13 @@ class TextToSpeechScreen extends StatelessWidget { model.isSubscribersOnly = value; }, ), + SwitchListTile.adaptive( + title: const Text("Only say messages starting with !v"), + value: model.isTtsCommandEncouraged, + onChanged: (value) { + model.isTtsCommandEncouraged = value; + }, + ), ], ); }), From 111eae74f20cb505def08e3e8948c1fa42b53779 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:21:37 +0100 Subject: [PATCH 09/12] Add text simplification option for TTS Introduce a text simplification feature for the TtsModel and expose it in the TTS settings. Adds a private flag, getter/setter (with notifyListeners), and persistence (toJson/fromJson). When enabled the message text is normalized by removing/replacing punctuation, diacritics, box-drawing/braille chars, collapsing repeated characters and whitespace; if the result is empty, vocalization is skipped. Also add a SwitchListTile in the settings UI to toggle the feature and log debug messages for simplified/skipped text. # Conflicts: # lib/models/tts.dart --- lib/models/tts.dart | 51 ++++++++++++++++++++++++++++++++--- lib/screens/settings/tts.dart | 8 ++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/lib/models/tts.dart b/lib/models/tts.dart index beb595f12..027e1b17b 100644 --- a/lib/models/tts.dart +++ b/lib/models/tts.dart @@ -35,6 +35,17 @@ import 'package:rtchat/models/user.dart'; enum TtsMode { disabled, alertsOnly, enabled } class TtsModel extends ChangeNotifier { + static final _punctuation1 = RegExp(r'[\u0020-\u0026\u0028-\u002f]'); + static final _punctuation2 = RegExp(r'[\u003a-\u0040]'); + static final _punctuation3 = RegExp(r'[\u005b-\u0060]'); + static final _punctuation4 = RegExp(r'[\u007b-\u007e]'); + static final _punctuation5 = RegExp(r'[\u00a0-\u00bf]'); + static final _diacritics = RegExp(r'[\u0300-\u036f]'); + static final _repeatedChars = RegExp(r'(.)\1{2,}'); + static final _boxDrawing = RegExp(r'[\u2500-\u257f]'); + static final _braille = RegExp(r'[\u2800-\u28ff]'); + static final _whitespace = RegExp(r'\s+'); + var _isCloudTtsEnabled = false; final _tts = FlutterTts() ..setSharedInstance(true) @@ -64,6 +75,7 @@ class TtsModel extends ChangeNotifier { var _lastMessageTime = DateTime.now(); MessageModel? _activeMessage; var _isSubscribersOnly = false; + var _isTextSimplificationEnabled = false; @override void dispose() { @@ -148,13 +160,33 @@ class TtsModel extends ChangeNotifier { text = text.substring("!v".length).trim(); } - if (text.trim().isEmpty) { - return ""; + if (_isTextSimplificationEnabled) { + text = text.replaceAll(_punctuation1, ' '); // replace punctuation with space except for apostrophes + text = text.replaceAll(_punctuation2, ' '); // replace punctuation with space + text = text.replaceAll(_punctuation3, ' '); // replace punctuation with space + text = text.replaceAll(_punctuation4, ' '); // replace punctuation with space + text = text.replaceAll(_punctuation5, ' '); // replace punctuation with space + text = text.replaceAll(_diacritics, ''); // remove diacritics + text = text.replaceAllMapped(_repeatedChars, (match) { + // replace repeated characters with a single instance + return match.group(1)!; + }); + text = text.replaceAll(_boxDrawing, ''); // remove box drawing characters + text = text.replaceAll(_braille, ''); // remove braille patterns + + // Remove doubles spaces that may have been introduced and trim the text + text = text.replaceAll(_whitespace, ' ').trim(); } - final author = model.author.displayName ?? model.author.login; + if (!includeAuthorPrelude || isPreludeMuted) { return text; } + + if (text.trim().isEmpty) { + return ""; + } + + final author = model.author.displayName ?? model.author.login; return model.isAction ? l10n.actionMessage(author, text) : l10n.saidMessage(author, text); @@ -213,6 +245,15 @@ class TtsModel extends ChangeNotifier { }); } + bool get isTextSimplificationEnabled { + return _isTextSimplificationEnabled; + } + + set isTextSimplificationEnabled(bool value) { + _isTextSimplificationEnabled = value; + notifyListeners(); + } + set newTtsEnabled(bool value) { if (value == _isNewTTsEnabled) { return; @@ -526,6 +567,9 @@ class TtsModel extends ChangeNotifier { if (json['isTtsCommandEncouraged'] != null) { _isTtsCommandEncouraged = json['isTtsCommandEncouraged']; } + if (json['isTextSimplificationEnabled'] != null) { + _isTextSimplificationEnabled = json['isTextSimplificationEnabled']; + } if (json['pitch'] != null) { _pitch = json['pitch']; } @@ -562,6 +606,7 @@ class TtsModel extends ChangeNotifier { Map toJson() => { "isBotMuted": isBotMuted, "isEmoteMuted": isEmoteMuted, + "isTextSimplificationEnabled": isTextSimplificationEnabled, "isPreludeMuted": isPreludeMuted, "isTtsCommandEncouraged": isTtsCommandEncouraged, "isRandomVoiceEnabled": isRandomVoiceEnabled, diff --git a/lib/screens/settings/tts.dart b/lib/screens/settings/tts.dart index 292db7718..201e91f9c 100644 --- a/lib/screens/settings/tts.dart +++ b/lib/screens/settings/tts.dart @@ -264,6 +264,14 @@ class TextToSpeechScreen extends StatelessWidget { model.isPreludeMuted = value; }, ), + SwitchListTile.adaptive( + title: const Text("Simplify messages"), + subtitle: const Text("Simplify punctuation, symbols, and whitespace (including muting repetitive punctuation and unsupported characters)"), + value: model.isTextSimplificationEnabled, + onChanged: (value) { + model.isTextSimplificationEnabled = value; + }, + ), SwitchListTile.adaptive( title: const Text("Subscribers only"), value: model.isSubscribersOnly, From 61ba1d7c067a63ba192cb4574e23ce75ef182d71 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:16:52 +0100 Subject: [PATCH 10/12] Disable deploy workflows and add release APK Commented out the agent, auxiliary, and periodic-restart GitHub Actions workflows (effectively disabling build/deploy and periodic restarts). Added a new release.yml workflow that triggers on tag pushes (vX.Y.Z), validates tag format, sets up Java and Flutter, decodes the upload keystore, builds a signed APK, renames the artifact, generates release notes from the previous tag, and creates a draft GitHub release with the APK attached (marks prereleases when the tag contains a hyphen). --- .github/workflows/agent.yml | 174 ++++++++++++------------- .github/workflows/auxiliary.yml | 160 +++++++++++------------ .github/workflows/periodic-restart.yml | 90 ++++++------- .github/workflows/release.yml | 90 +++++++++++++ 4 files changed, 302 insertions(+), 212 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/agent.yml b/.github/workflows/agent.yml index e1a5dc9e8..c10525450 100644 --- a/.github/workflows/agent.yml +++ b/.github/workflows/agent.yml @@ -1,87 +1,87 @@ -name: Build and Deploy Agent - -on: - push: - paths: - - "agent/**" - branches: - - main - pull_request: - paths: - - "agent/**" - branches: - - 'main' - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Log in to the Container registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v3 - with: - images: ghcr.io/muxable/rtchat-agent - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - - - name: Build and push Docker image - uses: docker/build-push-action@v2 - with: - context: ./agent - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - deploy: - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - needs: build - concurrency: - group: deploy - cancel-in-progress: true - environment: - name: agent - url: https://console.cloud.google.com/compute/instanceGroups/details/us-central1-c/agent-small-us-central1?project=rtchat-47692 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: google-github-actions/auth@v1 - with: - workload_identity_provider: 'projects/832669896677/locations/global/workloadIdentityPools/github-actions/providers/github-actions' - service_account: 'github-action-356868763@rtchat-47692.iam.gserviceaccount.com' - - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v0 - - - name: Deploy to GCE - id: deploy - run: | - gcloud compute instance-groups managed rolling-action start-update agent-small-us-central1 --project=rtchat-47692 --type='proactive' --max-surge=1 --max-unavailable=1 --minimal-action='replace' --replacement-method='substitute' --version=template=https://www.googleapis.com/compute/beta/projects/rtchat-47692/global/instanceTemplates/agent-e2-small --zone=us-central1-c +#name: Build and Deploy Agent +# +#on: +# push: +# paths: +# - "agent/**" +# branches: +# - main +# pull_request: +# paths: +# - "agent/**" +# branches: +# - 'main' +# workflow_dispatch: +# +#jobs: +# build: +# runs-on: ubuntu-latest +# permissions: +# contents: read +# packages: write +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# +# - name: Set up Docker Buildx +# uses: docker/setup-buildx-action@v1 +# +# - name: Log in to the Container registry +# uses: docker/login-action@v1 +# with: +# registry: ghcr.io +# username: ${{ github.actor }} +# password: ${{ secrets.GITHUB_TOKEN }} +# +# - name: Extract metadata (tags, labels) for Docker +# id: meta +# uses: docker/metadata-action@v3 +# with: +# images: ghcr.io/muxable/rtchat-agent +# tags: | +# type=ref,event=branch +# type=ref,event=pr +# type=semver,pattern={{version}} +# type=semver,pattern={{major}}.{{minor}} +# +# - name: Build and push Docker image +# uses: docker/build-push-action@v2 +# with: +# context: ./agent +# push: true +# tags: ${{ steps.meta.outputs.tags }} +# labels: ${{ steps.meta.outputs.labels }} +# cache-from: type=gha +# cache-to: type=gha,mode=max +# +# deploy: +# if: github.ref == 'refs/heads/main' +# runs-on: ubuntu-latest +# permissions: +# id-token: write +# contents: read +# needs: build +# concurrency: +# group: deploy +# cancel-in-progress: true +# environment: +# name: agent +# url: https://console.cloud.google.com/compute/instanceGroups/details/us-central1-c/agent-small-us-central1?project=rtchat-47692 +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# +# - uses: google-github-actions/auth@v1 +# with: +# workload_identity_provider: 'projects/832669896677/locations/global/workloadIdentityPools/github-actions/providers/github-actions' +# service_account: 'github-action-356868763@rtchat-47692.iam.gserviceaccount.com' +# +# - name: Set up Cloud SDK +# uses: google-github-actions/setup-gcloud@v0 +# +# - name: Deploy to GCE +# id: deploy +# run: | +# gcloud compute instance-groups managed rolling-action start-update agent-small-us-central1 --project=rtchat-47692 --type='proactive' --max-surge=1 --max-unavailable=1 --minimal-action='replace' --replacement-method='substitute' --version=template=https://www.googleapis.com/compute/beta/projects/rtchat-47692/global/instanceTemplates/agent-e2-small --zone=us-central1-c diff --git a/.github/workflows/auxiliary.yml b/.github/workflows/auxiliary.yml index 0368b650b..4e5460ffc 100644 --- a/.github/workflows/auxiliary.yml +++ b/.github/workflows/auxiliary.yml @@ -1,80 +1,80 @@ -name: Build and Deploy Auxiliary - -on: - push: - paths: - - "auxiliary/**" - branches: - - main - pull_request: - paths: - - "auxiliary/**" - branches: - - 'main' - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Log in to the Container registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v3 - with: - images: ghcr.io/muxable/rtchat-auxiliary - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - - - name: Build and push Docker image - uses: docker/build-push-action@v2 - with: - context: ./auxiliary - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - deploy: - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - needs: build - concurrency: - group: deploy - cancel-in-progress: true - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: google-github-actions/auth@v0 - with: - credentials_json: ${{ secrets.SERVICE_ACCOUNT_JSON }} - - - name: Set up Cloud SDK - uses: google-github-actions/setup-gcloud@v0 - - - name: Deploy to GCE - id: deploy - run: | - gcloud compute instance-groups managed rolling-action start-update auxiliary-us-central1 --version=template=auxiliary-e2-micro --zone=us-central1-a +#name: Build and Deploy Auxiliary +# +#on: +# push: +# paths: +# - "auxiliary/**" +# branches: +# - main +# pull_request: +# paths: +# - "auxiliary/**" +# branches: +# - 'main' +# workflow_dispatch: +# +#jobs: +# build: +# runs-on: ubuntu-latest +# permissions: +# contents: read +# packages: write +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# +# - name: Set up Docker Buildx +# uses: docker/setup-buildx-action@v1 +# +# - name: Log in to the Container registry +# uses: docker/login-action@v1 +# with: +# registry: ghcr.io +# username: ${{ github.actor }} +# password: ${{ secrets.GITHUB_TOKEN }} +# +# - name: Extract metadata (tags, labels) for Docker +# id: meta +# uses: docker/metadata-action@v3 +# with: +# images: ghcr.io/muxable/rtchat-auxiliary +# tags: | +# type=ref,event=branch +# type=ref,event=pr +# type=semver,pattern={{version}} +# type=semver,pattern={{major}}.{{minor}} +# +# - name: Build and push Docker image +# uses: docker/build-push-action@v2 +# with: +# context: ./auxiliary +# push: true +# tags: ${{ steps.meta.outputs.tags }} +# labels: ${{ steps.meta.outputs.labels }} +# cache-from: type=gha +# cache-to: type=gha,mode=max +# +# deploy: +# if: github.ref == 'refs/heads/main' +# runs-on: ubuntu-latest +# needs: build +# concurrency: +# group: deploy +# cancel-in-progress: true +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# +# - uses: google-github-actions/auth@v0 +# with: +# credentials_json: ${{ secrets.SERVICE_ACCOUNT_JSON }} +# +# - name: Set up Cloud SDK +# uses: google-github-actions/setup-gcloud@v0 +# +# - name: Deploy to GCE +# id: deploy +# run: | +# gcloud compute instance-groups managed rolling-action start-update auxiliary-us-central1 --version=template=auxiliary-e2-micro --zone=us-central1-a diff --git a/.github/workflows/periodic-restart.yml b/.github/workflows/periodic-restart.yml index 505bd12e8..68bb57a6e 100644 --- a/.github/workflows/periodic-restart.yml +++ b/.github/workflows/periodic-restart.yml @@ -1,45 +1,45 @@ -name: Periodically Restart Agent - -# This action periodically restarts the agent. -# Twitch likes to expire tokens if the agent runs for too long, so every couple of days we restart -# a random instance to ensure there's always a fresh agent available. - -on: - schedule: - - cron: "0 12 * * 1,3,5" # every Monday, Wednesday, and Friday at noon UTC - workflow_dispatch: - -jobs: - restart: - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - uses: google-github-actions/auth@v1 - with: - workload_identity_provider: "projects/832669896677/locations/global/workloadIdentityPools/github-actions/providers/github-actions" - service_account: "github-action-356868763@rtchat-47692.iam.gserviceaccount.com" - - - uses: "google-github-actions/setup-gcloud@v1" - - - run: | - INSTANCES=$(gcloud compute instance-groups managed list-instances agent-small-us-west1 --region=us-west1) - NUM_HEALTHY_INSTANCES=$(echo "$INSTANCES" | grep "HEALTHY" | wc -l) - - echo "$INSTANCES" - - echo "Number of healthy instances: $NUM_HEALTHY_INSTANCES" - - # if there are fewer than three healthy instances, don't do anything - if [ "$NUM_HEALTHY_INSTANCES" -lt 3 ]; then - echo "Not enough healthy instances, not restarting" - exit 0 - fi - - # otherwise, pick an instance at random - INSTANCE=$(echo "$INSTANCES" | tail -n +2 | shuf -n 1 | cut -d ' ' -f 1) - echo "Restarting instance: $INSTANCE" - - # restart the instance - gcloud compute instance-groups managed recreate-instances agent-small-us-west1 --region=us-west1 --instances=$INSTANCE +#name: Periodically Restart Agent +# +## This action periodically restarts the agent. +## Twitch likes to expire tokens if the agent runs for too long, so every couple of days we restart +## a random instance to ensure there's always a fresh agent available. +# +#on: +# schedule: +# - cron: "0 12 * * 1,3,5" # every Monday, Wednesday, and Friday at noon UTC +# workflow_dispatch: +# +#jobs: +# restart: +# runs-on: ubuntu-latest +# permissions: +# contents: read +# id-token: write +# steps: +# - uses: google-github-actions/auth@v1 +# with: +# workload_identity_provider: "projects/832669896677/locations/global/workloadIdentityPools/github-actions/providers/github-actions" +# service_account: "github-action-356868763@rtchat-47692.iam.gserviceaccount.com" +# +# - uses: "google-github-actions/setup-gcloud@v1" +# +# - run: | +# INSTANCES=$(gcloud compute instance-groups managed list-instances agent-small-us-west1 --region=us-west1) +# NUM_HEALTHY_INSTANCES=$(echo "$INSTANCES" | grep "HEALTHY" | wc -l) +# +# echo "$INSTANCES" +# +# echo "Number of healthy instances: $NUM_HEALTHY_INSTANCES" +# +# # if there are fewer than three healthy instances, don't do anything +# if [ "$NUM_HEALTHY_INSTANCES" -lt 3 ]; then +# echo "Not enough healthy instances, not restarting" +# exit 0 +# fi +# +# # otherwise, pick an instance at random +# INSTANCE=$(echo "$INSTANCES" | tail -n +2 | shuf -n 1 | cut -d ' ' -f 1) +# echo "Restarting instance: $INSTANCE" +# +# # restart the instance +# gcloud compute instance-groups managed recreate-instances agent-small-us-west1 --region=us-west1 --instances=$INSTANCE diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..db27cd3ed --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Release APK + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate tag format + run: | + TAG="${GITHUB_REF#refs/tags/}" + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "::error::Tag '$TAG' does not match expected format vX.Y.Z (e.g. v1.0.0, v1.2.3-beta.1)" + exit 1 + fi + echo "TAG=$TAG" >> "$GITHUB_ENV" + echo "VERSION=${TAG#v}" >> "$GITHUB_ENV" + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version-file: pubspec.yaml + + - name: Install dependencies + run: flutter pub get + + - name: Decode keystore + env: + UPLOAD_KEYSTORE: ${{ secrets.UPLOAD_KEYSTORE }} + run: printf '%s' "$UPLOAD_KEYSTORE" | base64 --decode > "$RUNNER_TEMP/upload-keystore.jks" + + - name: Build signed APK + env: + KEY_ALIAS: upload + KEY_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} + STORE_FILE: ${{ runner.temp }}/upload-keystore.jks + STORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }} + run: flutter build apk --release --build-name="${{ env.VERSION }}" + + - name: Rename APK + run: mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/rtchat-${{ env.TAG }}.apk + + - name: Generate release notes + id: release_notes + run: | + # Find the previous tag + PREVIOUS_TAG=$(git tag --sort=-creatordate | grep -E '^v[0-9]' | sed -n '2p') + # Escape for GitHub Actions multiline output + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + if [ -z "$PREVIOUS_TAG" ]; then + echo "notes<<$EOF" >> "$GITHUB_OUTPUT" + echo "Initial release 🎉" >> "$GITHUB_OUTPUT" + echo "$EOF" >> "$GITHUB_OUTPUT" + else + NOTES=$(git log --pretty=format:"- %s (%h)" "$PREVIOUS_TAG"..HEAD -- ':!.github') + echo "notes<<$EOF" >> "$GITHUB_OUTPUT" + echo "## What's Changed" >> "$GITHUB_OUTPUT" + echo "" >> "$GITHUB_OUTPUT" + echo "$NOTES" >> "$GITHUB_OUTPUT" + echo "" >> "$GITHUB_OUTPUT" + echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${{ env.TAG }}" >> "$GITHUB_OUTPUT" + echo "$EOF" >> "$GITHUB_OUTPUT" + fi + + - name: Create draft release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.TAG }} + name: ${{ env.TAG }} + body: ${{ steps.release_notes.outputs.notes }} + draft: true + prerelease: ${{ contains(env.TAG, '-') }} + files: build/app/outputs/flutter-apk/rtchat-${{ env.TAG }}.apk From b5a24857fbe9e4575cde8b252faa3d449ab4dac0 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:14:55 +0100 Subject: [PATCH 11/12] Update release.yml --- .github/workflows/release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db27cd3ed..ed7d7b7b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,8 +35,6 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 - with: - flutter-version-file: pubspec.yaml - name: Install dependencies run: flutter pub get From 258eddba2a2bde9e3b83f0386c035dcd4b5a1ec1 Mon Sep 17 00:00:00 2001 From: Myst <1592048+LeMyst@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:13:00 +0100 Subject: [PATCH 12/12] Enable underscore replacement by default Change the default for _isUnderscoreReplacementEnabled in TtsModel from false to true so underscore replacement is enabled by default in TTS behavior. Add option to replace underscores in author names for TTS Introduces a new setting to replace underscores with spaces in author names when generating TTS prelude. Adds a toggle in the TTS settings screen and persists the setting in the TtsModel. # Conflicts: # lib/models/tts.dart # lib/screens/settings/tts.dart --- lib/models/tts.dart | 26 ++++++++++++++++++++++++-- lib/screens/settings/tts.dart | 7 +++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/models/tts.dart b/lib/models/tts.dart index 027e1b17b..fd98be579 100644 --- a/lib/models/tts.dart +++ b/lib/models/tts.dart @@ -65,6 +65,7 @@ class TtsModel extends ChangeNotifier { var _isBotMuted = false; var _isEmoteMuted = false; var _isPreludeMuted = false; + var _isUnderscoreReplacementEnabled = true; var _isTtsCommandEncouraged = false; var _speed = Platform.isAndroid ? 0.8 : 0.395; var _pitch = 1.0; @@ -185,8 +186,16 @@ class TtsModel extends ChangeNotifier { if (text.trim().isEmpty) { return ""; } - - final author = model.author.displayName ?? model.author.login; + var author = model.author.displayName ?? model.author.login; + if (_isUnderscoreReplacementEnabled) { + author = author + .replaceAll("_", " ") + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + } + if (!includeAuthorPrelude || isPreludeMuted) { + return text; + } return model.isAction ? l10n.actionMessage(author, text) : l10n.saidMessage(author, text); @@ -385,6 +394,15 @@ class TtsModel extends ChangeNotifier { notifyListeners(); } + bool get isUnderscoreReplacementEnabled { + return _isUnderscoreReplacementEnabled; + } + + set isUnderscoreReplacementEnabled(bool value) { + _isUnderscoreReplacementEnabled = value; + notifyListeners(); + } + bool get isCloudTtsEnabled { return _isCloudTtsEnabled; } @@ -582,6 +600,9 @@ class TtsModel extends ChangeNotifier { if (json['isPreludeMuted'] != null) { _isPreludeMuted = json['isPreludeMuted']; } + if (json['isUnderscoreReplacementEnabled'] != null) { + _isUnderscoreReplacementEnabled = json['isUnderscoreReplacementEnabled']; + } if (json['isRandomVoiceEnabled'] != null) { _isRandomVoiceEnabled = json['isRandomVoiceEnabled']; } @@ -608,6 +629,7 @@ class TtsModel extends ChangeNotifier { "isEmoteMuted": isEmoteMuted, "isTextSimplificationEnabled": isTextSimplificationEnabled, "isPreludeMuted": isPreludeMuted, + "isUnderscoreReplacementEnabled": isUnderscoreReplacementEnabled, "isTtsCommandEncouraged": isTtsCommandEncouraged, "isRandomVoiceEnabled": isRandomVoiceEnabled, "language": language.languageCode, diff --git a/lib/screens/settings/tts.dart b/lib/screens/settings/tts.dart index 201e91f9c..2766797d2 100644 --- a/lib/screens/settings/tts.dart +++ b/lib/screens/settings/tts.dart @@ -264,6 +264,13 @@ class TextToSpeechScreen extends StatelessWidget { model.isPreludeMuted = value; }, ), + SwitchListTile.adaptive( + title: const Text("Replace underscores with spaces in viewer names"), + value: model.isUnderscoreReplacementEnabled, + onChanged: (value) { + model.isUnderscoreReplacementEnabled = value; + }, + ), SwitchListTile.adaptive( title: const Text("Simplify messages"), subtitle: const Text("Simplify punctuation, symbols, and whitespace (including muting repetitive punctuation and unsupported characters)"),