diff --git a/README.md b/README.md index 7b21286..8169dfc 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,12 @@ It is designed for practical daily work: IDEs, chats, notes, CRM fields, email, ## Latest Changes +### v0.3.5 + +- Fixed local STT mode so managed localhost runtimes are called without a stale API bearer token and no longer show an API-key error for local runtime failures. +- Improved widget notice layout so the status icon stays visually centered next to long messages. +- Prevented call-capture startup errors from resetting the completed permissions onboarding state after restart. + ### v0.3.4 - Fixed repeated permission prompts after restarting macOS: startup checks no longer trigger a microphone request, and Accessibility is not reset before re-requesting access. diff --git a/docs/release/review-v0.3.5.md b/docs/release/review-v0.3.5.md new file mode 100644 index 0000000..be850cb --- /dev/null +++ b/docs/release/review-v0.3.5.md @@ -0,0 +1,54 @@ +# Release Review v0.3.5 + +## Release + +- Version: 0.3.5 +- Release branch: `release/v0.3.5` +- Target tag: `v0.3.5` +- Reviewer: Codex +- Date: 2026-07-02 + +## Scope + +- Key changes included in this release: + - Local STT requests to localhost runtimes no longer attach the global API key as a bearer token. + - Widget error mapping now treats local STT `401/403` responses as local runtime errors instead of API-key failures. + - Widget notice icon layout uses a stable icon column so long messages stay visually aligned. + - Call-capture permission-like startup errors no longer reset the completed permissions onboarding flag. +- User-facing changes: + - Local model users should no longer see "check API key" when local runtime auth-like errors occur. + - Error notices render with a centered icon. + - Restarting after a call-capture startup failure should not reopen the permissions onboarding only because the app reset its own flag. +- Risky areas: + - Local STT authorization handling. + - Permission onboarding persistence after call-capture errors. + - Widget notice compact/expanded sizing. + +## Checks run + +- `bun run check:release`: passed +- `bun run build:release:macos`: local build reached `Talkis.app` and updater `.app.tar.gz`, then stopped because `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` is not present in the local environment; GitHub Actions is expected to run this step with repository secrets. +- Native/GitHub Windows build: GitHub Actions after tag +- Native/GitHub Linux build: GitHub Actions after tag +- Additional manual checks: + - `bunx tsc --noEmit`: passed before release prep + - `cargo check`: passed before release prep + +## Manual review + +- Hotkey flow: no hotkey reducer changes in this release. +- Onboarding permissions: reviewed `SettingsApp`, `PermissionScreen`, and widget call-capture error handling; removed app-side reset of completed onboarding from call-capture startup errors. +- Widget position and notice behavior: notice bubble positioning unchanged; notice content layout reviewed for compact and expanded message states. +- Transcription quality and short-utterance handling: no transcript filtering changes in this release. +- README refreshed: yes, `v0.3.5` added to Latest Changes. + +## Findings + +- Blockers: local macOS updater signing needs `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`; release publication proceeds through GitHub Actions secrets. +- Non-blocking issues: Windows/Linux artifacts are expected from GitHub Actions, not local macOS. Vite reports the existing large chunk warning during production builds. +- Follow-ups after release: verify GitHub Actions artifacts and updater metadata after tag publish. + +## Decision + +- Ready for `main` merge: yes +- Ready for tag publish: yes, via GitHub Actions release build diff --git a/package.json b/package.json index 4a9ae8c..bc6a672 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "talkis", "private": true, - "version": "0.3.4", + "version": "0.3.5", "license": "AGPL-3.0-or-later", "type": "module", "packageManager": "bun@1.2.13", diff --git a/scripts/build-release.mjs b/scripts/build-release.mjs index 711d0b9..894e3ea 100644 --- a/scripts/build-release.mjs +++ b/scripts/build-release.mjs @@ -14,10 +14,15 @@ function detectPlatform() { const platform = process.argv[2] || process.env.TALKIS_RELEASE_PLATFORM || detectPlatform(); const bundleTargets = { - macos: ["app", "dmg"], + macos: ["app"], windows: ["nsis"], linux: ["appimage", "deb"], }; +const releaseOutputs = { + macos: ["app", "dmg"], + windows: bundleTargets.windows, + linux: bundleTargets.linux, +}; if (!Object.hasOwn(bundleTargets, platform)) { throw new Error(`Unsupported release platform: ${platform}`); @@ -57,4 +62,4 @@ if (platform === "macos" && process.env.TALKIS_POSTPROCESS_MACOS_RELEASE !== "0" run("bun", ["run", "postprocess:macos-release"]); } -console.log(`Built ${platform} release bundles: ${bundleTargets[platform].join(", ")}`); +console.log(`Built ${platform} release bundles: ${releaseOutputs[platform].join(", ")}`); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7196cd6..d30cb48 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4969,7 +4969,7 @@ dependencies = [ [[package]] name = "talkis" -version = "0.3.4" +version = "0.3.5" dependencies = [ "base64 0.22.1", "block2 0.6.2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5096e26..a63cb4f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "talkis" -version = "0.3.4" +version = "0.3.5" description = "Talkis - Voice to Text Desktop Widget" authors = ["trixter"] license = "AGPL-3.0-or-later" diff --git a/src-tauri/binaries/talkis-llm-aarch64-apple-darwin b/src-tauri/binaries/talkis-llm-aarch64-apple-darwin index 034578c..7b50980 100644 Binary files a/src-tauri/binaries/talkis-llm-aarch64-apple-darwin and b/src-tauri/binaries/talkis-llm-aarch64-apple-darwin differ diff --git a/src-tauri/src/ai.rs b/src-tauri/src/ai.rs index 64f2ad4..7cca279 100644 --- a/src-tauri/src/ai.rs +++ b/src-tauri/src/ai.rs @@ -586,8 +586,13 @@ async fn transcribe_audio_bytes_internal( let whisper_key = req .whisper_api_key .as_ref() + .map(|s| s.trim()) .filter(|s| !s.is_empty()) - .unwrap_or(&req.api_key); + .or_else(|| { + (!is_local_endpoint) + .then_some(req.api_key.trim()) + .filter(|s| !s.is_empty()) + }); let stt_client = if is_local_endpoint { reqwest::Client::builder() @@ -600,10 +605,12 @@ async fn transcribe_audio_bytes_internal( (*client).clone() }; - let whisper_res = stt_client - .post(&whisper_url) - .bearer_auth(whisper_key) - .multipart(form) + let mut whisper_request = stt_client.post(&whisper_url).multipart(form); + if let Some(whisper_key) = whisper_key { + whisper_request = whisper_request.bearer_auth(whisper_key); + } + + let whisper_res = whisper_request .send() .await .map_err(|e| { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index dd72816..e668a1d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "Talkis", "mainBinaryName": "Talkis", - "version": "0.3.4", + "version": "0.3.5", "identifier": "com.trixter.talkis", "build": { "beforeDevCommand": "bun run prepare:sidecars && bun run dev", diff --git a/src/lib/i18n/dict/widget.ts b/src/lib/i18n/dict/widget.ts index 5149dc6..79596c4 100644 --- a/src/lib/i18n/dict/widget.ts +++ b/src/lib/i18n/dict/widget.ts @@ -128,6 +128,10 @@ export const widget = { ru: "Не удалось запустить локальный runtime распознавания. Откройте Настройки -> Модели -> Локально и нажмите «Скачать» для нужной Whisper-модели.", en: "Couldn't start the local recognition runtime. Open Settings -> Models -> Local and click “Download” for the Whisper model you need.", }, + "widget.error.localRuntimeRejected": { + ru: "Локальный runtime распознавания отклонил запрос. Перезапустите локальную модель или выберите её заново в Настройки -> Модели -> Локально.", + en: "The local recognition runtime rejected the request. Restart the local model or select it again in Settings -> Models -> Local.", + }, "widget.error.regionUnsupported": { ru: "Сервис распознавания сейчас недоступен в вашем регионе. Попробуйте другой endpoint или VPN.", en: "The recognition service is currently unavailable in your region. Try a different endpoint or a VPN.", diff --git a/src/windows/widget/Widget.tsx b/src/windows/widget/Widget.tsx index 7afae35..ce6cb8c 100644 --- a/src/windows/widget/Widget.tsx +++ b/src/windows/widget/Widget.tsx @@ -21,8 +21,6 @@ import { getHistory, getSettings, reconcileInterruptedProcessing, - setPermissionsPassed, - setSystemAudioPermissionPassed, type HistoryEntry, } from "../../lib/store"; import { @@ -653,26 +651,6 @@ export function Widget() { callMicPausedForVoiceRef.current = false; if (isCallCapturePermissionError(error)) { callSystemAudioPermissionReadyRef.current = false; - void setSystemAudioPermissionPassed(false).catch((storeError) => { - logError( - "CALL_CAPTURE", - `Failed to reset system audio permissions flag: ${ - storeError instanceof Error - ? storeError.message - : String(storeError) - }`, - ); - }); - void setPermissionsPassed(false).catch((storeError) => { - logError( - "CALL_CAPTURE", - `Failed to reset permissions flag: ${ - storeError instanceof Error - ? storeError.message - : String(storeError) - }`, - ); - }); } setCallError(message); setCallSession(null); diff --git a/src/windows/widget/WidgetNoticeOverlay.tsx b/src/windows/widget/WidgetNoticeOverlay.tsx index 8d2e4d2..2834c7d 100644 --- a/src/windows/widget/WidgetNoticeOverlay.tsx +++ b/src/windows/widget/WidgetNoticeOverlay.tsx @@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react"; import type { ReactElement } from "react"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; +import { AlertCircle, Info } from "lucide-react"; import { NOTICE_AREA_HEIGHT, NOTICE_WIDGET_WIDTH, WIDGET_NOTICE_EVENT, type WidgetNoticeState } from "./widgetConstants"; @@ -49,6 +50,8 @@ export function WidgetNoticeOverlay(): ReactElement | null { const toggleExpanded = () => { setExpanded((value) => !value); }; + const Icon = notice.tone === "error" ? AlertCircle : Info; + const iconColor = notice.tone === "error" ? "rgba(184,52,52,0.9)" : "rgba(0,0,0,0.58)"; return (
- {notice.message} +
diff --git a/src/windows/widget/services/transcriptionPipeline.ts b/src/windows/widget/services/transcriptionPipeline.ts index b32bd32..9c49383 100644 --- a/src/windows/widget/services/transcriptionPipeline.ts +++ b/src/windows/widget/services/transcriptionPipeline.ts @@ -37,9 +37,18 @@ function arrayBufferToBase64(buffer: ArrayBuffer): string { return btoa(binary); } -function toUserFacingErrorMessage(error: unknown): string { +function isLocalSttSettings(settings: AppSettings): boolean { + return ( + settings.useOwnKey && + settings.provider === "custom" && + /127\.0\.0\.1|localhost/i.test(settings.whisperEndpoint || "") + ); +} + +function toUserFacingErrorMessage(error: unknown, settings: AppSettings): string { const raw = formatErrorMessage(error); const normalized = raw.toLowerCase(); + const isLocalStt = isLocalSttSettings(settings); const missingModelMatch = raw.match(/Model ['"]([^'"]+)['"] is not installed locally/i); if (missingModelMatch) { @@ -63,6 +72,10 @@ function toUserFacingErrorMessage(error: unknown): string { } if (normalized.includes("403") || normalized.includes("forbidden")) { + if (isLocalStt) { + return tn("widget.error.localRuntimeRejected"); + } + if (normalized.includes("subscription inactive") || normalized.includes("активная подписка") || normalized.includes("cloud mode")) { return tn("widget.error.subscriptionRequired"); } @@ -87,6 +100,10 @@ function toUserFacingErrorMessage(error: unknown): string { } if (normalized.includes("401") || normalized.includes("unauthorized") || normalized.includes("invalid api key")) { + if (isLocalStt) { + return tn("widget.error.localRuntimeRejected"); + } + return tn("widget.error.authFailed"); } @@ -358,7 +375,7 @@ export async function processRecordingBlob({ : String(error); logError("API", `Pipeline raw error: ${rawErrorMessage}`); - const userFacingErrorMessage = toUserFacingErrorMessage(error); + const userFacingErrorMessage = toUserFacingErrorMessage(error, settings); await finishProcessing({ ...baseEntry, status: "failed", @@ -449,7 +466,7 @@ export async function retryHistoryEntry( return { hasTranscription: false, updatedEntry: interrupted }; } - const userFacingErrorMessage = toUserFacingErrorMessage(error); + const userFacingErrorMessage = toUserFacingErrorMessage(error, retrySettings); const failedEntry: HistoryEntry = { ...entry, status: "failed",