From fd91dc1b976d7cdb97d7111c23b000b9cea6e9a8 Mon Sep 17 00:00:00 2001 From: "R. Teodoro" Date: Mon, 5 Oct 2026 12:34:39 -0300 Subject: [PATCH 1/2] fix(recording): drain audio after the helper exits --- source/meeting_audio.py | 15 ++++++-- source/tests/test_meeting_audio.py | 54 ++++++++++++++++++++++++++-- source/tests/test_meeting_support.py | 42 ++++++++++++++++++++++ 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/source/meeting_audio.py b/source/meeting_audio.py index 14ecb3c..6d5f787 100644 --- a/source/meeting_audio.py +++ b/source/meeting_audio.py @@ -178,12 +178,21 @@ def read_event(self, timeout=0.1): return None def command(self, command): - if self._process is None or self._process.poll() is not None: + process = self._process + if process is None: + raise MeetingAudioError(tr("O capturador de áudio não está ativo.")) + if process.poll() is not None: + if command == "stop": + # The controller must drain queued PCM and inspect the terminal + # status even when the helper finished before the stop request. + return raise MeetingAudioError(tr("O capturador de áudio não está ativo.")) try: - self._process.stdin.write((json.dumps({"command": command}) + "\n").encode()) - self._process.stdin.flush() + process.stdin.write((json.dumps({"command": command}) + "\n").encode()) + process.stdin.flush() except (OSError, ValueError) as exc: + if command == "stop" and process.poll() is not None: + return raise MeetingAudioError(tr("Não foi possível controlar a captura.")) from exc def pause(self): diff --git a/source/tests/test_meeting_audio.py b/source/tests/test_meeting_audio.py index 152b95e..8fbd3e4 100644 --- a/source/tests/test_meeting_audio.py +++ b/source/tests/test_meeting_audio.py @@ -55,11 +55,61 @@ def test_truncated_payload_is_not_a_successful_take(self): def fake_helper(self, body): script = ("import json,struct,sys,time\n" - "def emit(event):\n" - " data=json.dumps(event).encode(); sys.stdout.buffer.write(struct.pack(' Date: Mon, 5 Oct 2026 12:36:03 -0300 Subject: [PATCH 2/2] fix(release): validate metadata and prepare version 1.2.1 --- .github/workflows/bundles.yml | 4 +-- CHANGELOG.md | 4 +++ RELEASE-WINDOWS.md | 13 +++---- installer/snipvoice.iss | 2 +- prepare-release-windows.ps1 | 16 +++++++++ source/docs/development.md | 2 +- source/snipvoice.pyw | 4 +-- source/tests/test_voice_packaging.py | 10 +++--- test-prepare-release-windows.ps1 | 54 +++++++++++++++++++++++++++- 9 files changed, 91 insertions(+), 18 deletions(-) diff --git a/.github/workflows/bundles.yml b/.github/workflows/bundles.yml index b2e74b8..46f2d4c 100644 --- a/.github/workflows/bundles.yml +++ b/.github/workflows/bundles.yml @@ -39,9 +39,9 @@ concurrency: cancel-in-progress: true env: - SNIPVOICE_VERSION: "1.2.0" + SNIPVOICE_VERSION: "1.2.1" SNIPVOICE_CHANNEL: "stable" - SNIPVOICE_RELEASE_LABEL: "1.2.0" + SNIPVOICE_RELEASE_LABEL: "1.2.1" jobs: windows: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2116a4b..14779a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Batch contiguous native audio packets to reduce disk synchronization pressure while preserving gaps and pause/stop tails. +- Show recording time, pause state, and partial/interrupted status in the floating indicator while the manager is minimized or hidden. +- Preserve queued audio when stopping races the native helper's exit; continue reporting overflow and unexpected exits. +- Reject inconsistent runtime, installer, or bundle version/channel metadata before Windows release preparation. - Name transcription and summary models by what they offer, such as "Equilibrado · Parakeet" and "Máxima qualidade · Gemma 4 E4B", instead of bare model codes. - List models from lowest to highest quality in Ditado, Configurações > Gravação, and Configurações > Modelos. diff --git a/RELEASE-WINDOWS.md b/RELEASE-WINDOWS.md index 87850ac..1ccf2f6 100644 --- a/RELEASE-WINDOWS.md +++ b/RELEASE-WINDOWS.md @@ -4,11 +4,12 @@ Set the clean FFmpeg evidence directory first: ```powershell $env:SNIPVOICE_FFMPEG_COMPLIANCE_DIR = 'C:\path\to\compliance' -.\prepare-release-windows.ps1 -Tag v1.2.0 -DryRun -.\prepare-release-windows.ps1 -Tag v1.2.0 +.\prepare-release-windows.ps1 -Tag v1.2.1 -DryRun +.\prepare-release-windows.ps1 -Tag v1.2.1 ``` -The helper requires clean `main` at `origin/main`, runs unittest, preserves the -compliance manifest/native capture/runtime probes in the existing builders, and -hashes the Windows installer. macOS capture, signing, notarization, and release -publication remain external gates. +The helper requires matching source/runtime, installer, and bundle-workflow +version/channel metadata, then clean `main` at `origin/main`. It runs unittest, +preserves the compliance manifest/native capture/runtime probes in the existing +builders, and hashes the Windows installer. macOS capture, signing, notarization, +and release publication remain external gates. diff --git a/installer/snipvoice.iss b/installer/snipvoice.iss index 9f8ce6e..da0a883 100644 --- a/installer/snipvoice.iss +++ b/installer/snipvoice.iss @@ -8,7 +8,7 @@ ; Snipvoice never migrates another application's user data. #define MyAppName "SnipVoice" -#define MyAppVersion "1.2.0" +#define MyAppVersion "1.2.1" #define MyAppChannel "stable" #if MyAppChannel == "beta" #define MyAppDisplayVersion MyAppVersion + " beta 3" diff --git a/prepare-release-windows.ps1 b/prepare-release-windows.ps1 index 796c88d..f201376 100644 --- a/prepare-release-windows.ps1 +++ b/prepare-release-windows.ps1 @@ -58,6 +58,22 @@ function Get-DeclaredVersion { } $declared = $Matches[1] if ($content -notmatch '(?m)^Channel: stable\r?$') { throw 'A stable release is required.' } + $installer = Get-Content -Raw -LiteralPath 'installer/snipvoice.iss' + $workflow = Get-Content -Raw -LiteralPath '.github/workflows/bundles.yml' + $checks = @( + @{ Text = $content; Pattern = '(?m)^APP_VERSION = "([^"]+)"'; Expected = $declared; Label = 'runtime version' }, + @{ Text = $content; Pattern = '(?m)^RELEASE_CHANNEL = "([^"]+)"'; Expected = 'stable'; Label = 'runtime channel' }, + @{ Text = $installer; Pattern = '(?m)^#define MyAppVersion "([^"]+)"'; Expected = $declared; Label = 'installer version' }, + @{ Text = $installer; Pattern = '(?m)^#define MyAppChannel "([^"]+)"'; Expected = 'stable'; Label = 'installer channel' }, + @{ Text = $workflow; Pattern = '(?m)^ SNIPVOICE_VERSION: "([^"]+)"'; Expected = $declared; Label = 'workflow version' }, + @{ Text = $workflow; Pattern = '(?m)^ SNIPVOICE_CHANNEL: "([^"]+)"'; Expected = 'stable'; Label = 'workflow channel' }, + @{ Text = $workflow; Pattern = '(?m)^ SNIPVOICE_RELEASE_LABEL: "([^"]+)"'; Expected = $declared; Label = 'release label' } + ) + foreach ($check in $checks) { + if ($check.Text -notmatch $check.Pattern -or $Matches[1] -ne $check.Expected) { + throw "Release $($check.Label) does not match the declared stable version." + } + } return $declared } diff --git a/source/docs/development.md b/source/docs/development.md index f0855bd..ba6902e 100644 --- a/source/docs/development.md +++ b/source/docs/development.md @@ -1,6 +1,6 @@ # Development -Snipvoice source is `1.2.0` on the `stable` channel (`v1.2.0`). +Snipvoice source is `1.2.1` on the `stable` channel (the upcoming `v1.2.1`). Runtime dependencies and native voice dependencies remain in separate manifests. Use an existing interpreter; do not implicitly update host tooling or packages. diff --git a/source/snipvoice.pyw b/source/snipvoice.pyw index c9916bd..3a75be7 100644 --- a/source/snipvoice.pyw +++ b/source/snipvoice.pyw @@ -1,5 +1,5 @@ """Snipvoice - local push-to-talk dictation for Windows and macOS. -Version: 1.2.0 +Version: 1.2.1 Channel: stable Recording is explicit and local. Audio and transcripts are kept in recoverable @@ -91,7 +91,7 @@ from meeting_library import MeetingLibrary from meeting_support import MeetingController from meeting_settings import resolve_meeting_settings, validate_hotkey_conflicts -APP_VERSION = "1.2.0" +APP_VERSION = "1.2.1" RELEASE_CHANNEL = "stable" BETA_NUMBER = 0 APP_DISPLAY_NAME = f"SnipVoice v{APP_VERSION}" diff --git a/source/tests/test_voice_packaging.py b/source/tests/test_voice_packaging.py index a0f2e30..e619201 100644 --- a/source/tests/test_voice_packaging.py +++ b/source/tests/test_voice_packaging.py @@ -385,14 +385,14 @@ def test_release_metadata_is_synchronized(self): source_version = re.search(r'^APP_VERSION = "([^"]+)"$', texts["source"], re.M) installer_version = re.search(r'^#define MyAppVersion "([^"]+)"$', texts["installer"], re.M) workflow_version = re.search(r'^ SNIPVOICE_VERSION: "([^"]+)"$', texts["workflow"], re.M) - self.assertEqual(source_version.group(1), "1.2.0") - self.assertEqual(installer_version.group(1), "1.2.0") - self.assertEqual(workflow_version.group(1), "1.2.0") - self.assertIn('Version: 1.2.0\nChannel: stable', texts["source"]) + self.assertEqual(source_version.group(1), "1.2.1") + self.assertEqual(installer_version.group(1), "1.2.1") + self.assertEqual(workflow_version.group(1), "1.2.1") + self.assertIn('Version: 1.2.1\nChannel: stable', texts["source"]) self.assertIn('RELEASE_CHANNEL = "stable"', texts["source"]) self.assertIn('#define MyAppChannel "stable"', texts["installer"]) self.assertIn('SNIPVOICE_CHANNEL: "stable"', texts["workflow"]) - self.assertIn('SNIPVOICE_RELEASE_LABEL: "1.2.0"', texts["workflow"]) + self.assertIn('SNIPVOICE_RELEASE_LABEL: "1.2.1"', texts["workflow"]) self.assertIn('#define MyAppDisplayVersion MyAppVersion + " beta 3"', texts["installer"]) self.assertIn('#define MyInstallerVersion MyAppVersion + "-beta.3"', diff --git a/test-prepare-release-windows.ps1 b/test-prepare-release-windows.ps1 index 65e4fc7..34ab395 100644 --- a/test-prepare-release-windows.ps1 +++ b/test-prepare-release-windows.ps1 @@ -7,10 +7,22 @@ $global:releasePreparationTestState = @{ Mode = 'ok'; Gates = 0 } try { New-Item -ItemType Directory -Path $sandbox | Out-Null New-Item -ItemType Directory -Path (Join-Path $sandbox 'source') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $sandbox 'installer') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $sandbox '.github/workflows') -Force | Out-Null Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'prepare-release-windows.ps1') -Destination $sandbox 'Version: 1.2.3 Channel: stable +APP_VERSION = "1.2.3" +RELEASE_CHANNEL = "stable" ' | Set-Content -LiteralPath (Join-Path $sandbox 'source/snipvoice.pyw') + '#define MyAppVersion "1.2.3" +#define MyAppChannel "stable" +' | Set-Content -LiteralPath (Join-Path $sandbox 'installer/snipvoice.iss') + 'env: + SNIPVOICE_VERSION: "1.2.3" + SNIPVOICE_CHANNEL: "stable" + SNIPVOICE_RELEASE_LABEL: "1.2.3" +' | Set-Content -LiteralPath (Join-Path $sandbox '.github/workflows/bundles.yml') function git { param([Parameter(ValueFromRemainingArguments)] $ArgumentList) @@ -36,6 +48,9 @@ Channel: stable function python { $global:releasePreparationTestState.Gates += 1 $global:LASTEXITCODE = if ($global:releasePreparationTestState.Mode -eq 'gate-fail') { 8 } else { 0 } + if ($global:releasePreparationTestState.Mode -eq 'metadata-after-tests') { + '#define MyAppVersion "9.9.9"' | Set-Content -LiteralPath (Join-Path $sandbox 'installer/snipvoice.iss') + } } function cmd { throw 'A dry run must never invoke a package builder.' } @@ -66,7 +81,44 @@ Channel: stable throw "A rejected checkout ran gates: $($case.Key)" } } - Write-Host 'snipvoice: 7 release preparation scenarios passed' + $metadataCases = @( + @{ Path = 'source/snipvoice.pyw'; Before = 'APP_VERSION = "1.2.3"'; After = 'APP_VERSION = "9.9.9"'; Error = 'runtime version' }, + @{ Path = 'source/snipvoice.pyw'; Before = 'RELEASE_CHANNEL = "stable"'; After = 'RELEASE_CHANNEL = "beta"'; Error = 'runtime channel' }, + @{ Path = 'installer/snipvoice.iss'; Before = 'MyAppVersion "1.2.3"'; After = 'MyAppVersion "9.9.9"'; Error = 'installer version' }, + @{ Path = 'installer/snipvoice.iss'; Before = 'MyAppChannel "stable"'; After = 'MyAppChannel "beta"'; Error = 'installer channel' }, + @{ Path = '.github/workflows/bundles.yml'; Before = 'SNIPVOICE_VERSION: "1.2.3"'; After = 'SNIPVOICE_VERSION: "9.9.9"'; Error = 'workflow version' }, + @{ Path = '.github/workflows/bundles.yml'; Before = 'SNIPVOICE_CHANNEL: "stable"'; After = 'SNIPVOICE_CHANNEL: "beta"'; Error = 'workflow channel' }, + @{ Path = '.github/workflows/bundles.yml'; Before = 'SNIPVOICE_RELEASE_LABEL: "1.2.3"'; After = 'SNIPVOICE_RELEASE_LABEL: "9.9.9"'; Error = 'release label' } + ) + foreach ($case in $metadataCases) { + $global:releasePreparationTestState.Mode = 'ok' + $global:releasePreparationTestState.Gates = 0 + $path = Join-Path $sandbox $case.Path + $before = Get-Content -Raw -LiteralPath $path + $before.Replace($case.Before, $case.After) | Set-Content -LiteralPath $path + $caught = $false + try { + & (Join-Path $sandbox 'prepare-release-windows.ps1') -Tag v1.2.3 -DryRun + } catch { + if ($_.Exception.Message -notmatch $case.Error) { throw } + $caught = $true + } finally { + $before | Set-Content -LiteralPath $path + } + if (-not $caught) { throw "Mismatched release metadata was accepted: $($case.Path) $($case.Before)" } + if ($global:releasePreparationTestState.Gates -ne 0) { throw 'Mismatched metadata ran test gates.' } + } + $global:releasePreparationTestState.Mode = 'metadata-after-tests' + $global:releasePreparationTestState.Gates = 0 + $caught = $false + try { + & (Join-Path $sandbox 'prepare-release-windows.ps1') -Tag v1.2.3 -DryRun + } catch { + if ($_.Exception.Message -notmatch 'installer version') { throw } + $caught = $true + } + if (-not $caught) { throw 'Metadata drift during tests was accepted.' } + Write-Host 'snipvoice: 15 release preparation scenarios passed' } finally { Set-Location $original