From 83b1a3cbe2d4d557e13bc6b19e8647a7a71926ea Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 07:20:43 +0000 Subject: [PATCH] fix(app): keep empty dictation feedback factual and readable An empty transcript does not identify whether silence filtering or speech recognition produced it. Preserve the recording and offer Retry without mirroring the daemon's independently configured silence threshold. Show the finished result in place of the recording meter so its explanation fits in the composer. Verify exact PCM replay across repeated retries, discard on a narrow screen, and partial-text fallback through browser flows. Co-authored-by: NightMachinery --- .../browser/dictation-empty-result.spec.ts | 80 ++++++---- packages/app/e2e/support/helpers/dictation.ts | 62 ++++++- .../app/src/components/dictation-controls.tsx | 27 ++-- .../dictation/empty-transcript-reason.test.ts | 46 +----- .../src/dictation/empty-transcript-reason.ts | 26 +-- packages/app/src/dictation/pcm-level.ts | 24 --- .../app/src/hooks/use-dictation.shared.ts | 2 +- packages/app/src/hooks/use-dictation.test.tsx | 151 ------------------ packages/app/src/hooks/use-dictation.ts | 8 +- packages/app/src/i18n/resources/ar.ts | 3 +- packages/app/src/i18n/resources/en.ts | 3 +- packages/app/src/i18n/resources/es.ts | 3 +- packages/app/src/i18n/resources/fr.ts | 3 +- packages/app/src/i18n/resources/ja.ts | 3 +- packages/app/src/i18n/resources/ko.ts | 3 +- packages/app/src/i18n/resources/pt-BR.ts | 3 +- packages/app/src/i18n/resources/ru.ts | 3 +- packages/app/src/i18n/resources/zh-CN.ts | 3 +- 18 files changed, 141 insertions(+), 312 deletions(-) delete mode 100644 packages/app/src/dictation/pcm-level.ts delete mode 100644 packages/app/src/hooks/use-dictation.test.tsx diff --git a/packages/app/e2e/browser/dictation-empty-result.spec.ts b/packages/app/e2e/browser/dictation-empty-result.spec.ts index 5c868feb3b..a08f9f5e09 100644 --- a/packages/app/e2e/browser/dictation-empty-result.spec.ts +++ b/packages/app/e2e/browser/dictation-empty-result.spec.ts @@ -1,62 +1,84 @@ import { expect, test } from "../support/fixtures"; import { composerLocator, expectComposerVisible } from "../support/helpers/composer"; import { + discardDictation, + expectEmptyDictation, installDictationTranscriptHarness, installSyntheticMicrophone, + recordDictation, + retryDictation, } from "../support/helpers/dictation"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; -const TRANSCRIPT = "Recovered on the second attempt"; +const TRANSCRIPT = "Recovered on the second retry"; test.describe("Dictation empty result", () => { - test.describe.configure({ timeout: 240_000 }); - - test("explains an empty transcript and retries the same recording", async ({ page }) => { + test("explains an empty result and replays the same audio through repeated retries", async ({ + page, + }) => { const agent = await seedMockAgentWorkspace({ repoPrefix: "dictation-empty-", title: "Empty dictation", }); await installSyntheticMicrophone(page); - const harness = await installDictationTranscriptHarness(page, ["", TRANSCRIPT]); + const harness = await installDictationTranscriptHarness(page, ["", "", TRANSCRIPT]); try { await openAgentRoute(page, agent); await expectComposerVisible(page); - - await page.getByRole("button", { name: "Start dictation" }).click(); - await harness.waitForAudio(); - await page.getByRole("button", { name: "Insert transcription", exact: true }).click(); - - await expect( - page.getByText("No speech was recognized. Tap retry to try again.", { exact: true }), - ).toBeVisible(); - await page.getByRole("button", { name: "Retry dictation", exact: true }).click(); + await recordDictation(page, harness); + await expectEmptyDictation(page); + await retryDictation(page); + await expectEmptyDictation(page); + await retryDictation(page); await expect(composerLocator(page)).toHaveValue(TRANSCRIPT); + expect(harness.recordings).toHaveLength(3); + expect(harness.recordings[0].length).toBeGreaterThan(0); + expect(harness.recordings[1]).toEqual(harness.recordings[0]); + expect(harness.recordings[2]).toEqual(harness.recordings[0]); } finally { await agent.cleanup(); } }); - test("says when the microphone only delivered silence", async ({ page }) => { + test.describe("narrow composer", () => { + test.use({ viewport: { width: 390, height: 844 } }); + + test("explains and discards an empty result from a silent microphone", async ({ page }) => { + const agent = await seedMockAgentWorkspace({ + repoPrefix: "dictation-silent-", + title: "Silent dictation", + }); + await installSyntheticMicrophone(page, { silent: true }); + const harness = await installDictationTranscriptHarness(page, [""]); + try { + await openAgentRoute(page, agent); + await expectComposerVisible(page); + await recordDictation(page, harness); + await expectEmptyDictation(page); + await discardDictation(page); + await expect(composerLocator(page)).toBeEditable(); + await expect(composerLocator(page)).toHaveValue(""); + } finally { + await agent.cleanup(); + } + }); + }); + + test("keeps the latest partial text when the final response is empty", async ({ page }) => { const agent = await seedMockAgentWorkspace({ - repoPrefix: "dictation-silent-", - title: "Silent dictation", + repoPrefix: "dictation-partial-", + title: "Partial dictation", + }); + await installSyntheticMicrophone(page); + const harness = await installDictationTranscriptHarness(page, [""], { + partialTranscript: "Words already recognized", }); - await installSyntheticMicrophone(page, { silent: true }); - const harness = await installDictationTranscriptHarness(page, [""]); try { await openAgentRoute(page, agent); await expectComposerVisible(page); - - await page.getByRole("button", { name: "Start dictation" }).click(); - await harness.waitForAudio(); - await page.getByRole("button", { name: "Insert transcription", exact: true }).click(); - - await expect( - page.getByText("Only silence was recorded. Check your microphone.", { exact: true }), - ).toBeVisible(); - await page.getByRole("button", { name: "Cancel dictation", exact: true }).click(); - await expect(composerLocator(page)).toBeEditable(); + await recordDictation(page, harness); + await expect(composerLocator(page)).toHaveValue("Words already recognized"); } finally { await agent.cleanup(); } diff --git a/packages/app/e2e/support/helpers/dictation.ts b/packages/app/e2e/support/helpers/dictation.ts index 68395e574c..d9554e292b 100644 --- a/packages/app/e2e/support/helpers/dictation.ts +++ b/packages/app/e2e/support/helpers/dictation.ts @@ -1,4 +1,4 @@ -import type { Page } from "../fixtures"; +import { expect, type Page } from "../fixtures"; import { daemonWsRoutePattern } from "./daemon-port"; type WebSocketMessage = string | Buffer; @@ -78,8 +78,14 @@ export async function installSyntheticMicrophone( * Answers dictation stream requests in place of the daemon. Each finish is * answered with the next queued transcript; everything else reaches the daemon. */ -export async function installDictationTranscriptHarness(page: Page, transcripts: string[]) { +export async function installDictationTranscriptHarness( + page: Page, + transcripts: string[], + options: { partialTranscript?: string } = {}, +) { const queue = [...transcripts]; + const streams = new Map>(); + const recordings: string[][] = []; let resolveAudioChunk!: () => void; const audioChunk = new Promise((resolve) => { resolveAudioChunk = resolve; @@ -90,10 +96,15 @@ export async function installDictationTranscriptHarness(page: Page, transcripts: ws.onMessage((message) => { const request = sessionMessage(message); - const type = request?.type; - const dictationId = typeof request?.dictationId === "string" ? request.dictationId : null; + if (!request) { + server.send(message); + return; + } + const type = request.type; + const dictationId = typeof request.dictationId === "string" ? request.dictationId : null; if (type === "dictation_stream_start" && dictationId) { + streams.set(dictationId, new Map()); sendSessionMessage(ws, { type: "dictation_stream_ack", payload: { dictationId, ackSeq: -1 }, @@ -106,10 +117,22 @@ export async function installDictationTranscriptHarness(page: Page, transcripts: type: "dictation_stream_ack", payload: { dictationId, ackSeq: seq }, }); + streams.get(dictationId)?.set(seq, String(request.audio)); + if (options.partialTranscript) { + sendSessionMessage(ws, { + type: "dictation_stream_partial", + payload: { dictationId, text: options.partialTranscript }, + }); + } resolveAudioChunk(); return; } if (type === "dictation_stream_finish" && dictationId) { + recordings.push( + [...(streams.get(dictationId)?.entries() ?? [])] + .sort(([a], [b]) => a - b) + .map(([, audio]) => audio), + ); sendSessionMessage(ws, { type: "dictation_stream_finish_accepted", payload: { dictationId, timeoutMs: 5_000 }, @@ -127,5 +150,34 @@ export async function installDictationTranscriptHarness(page: Page, transcripts: server.onMessage((message) => ws.send(enableDictationCapability(message))); }); - return { waitForAudio: () => audioChunk }; + return { waitForAudio: () => audioChunk, recordings }; +} + +export async function recordDictation(page: Page, harness: { waitForAudio(): Promise }) { + await page.getByRole("button", { name: "Start dictation", exact: true }).click(); + await harness.waitForAudio(); + await page.getByRole("button", { name: "Insert transcription", exact: true }).click(); +} + +export async function expectEmptyDictation(page: Page) { + const message = page.getByText("No transcription was returned. Tap retry to try again.", { + exact: true, + }); + await expect(message).toBeVisible(); + const bounds = await message.boundingBox(); + const cancel = await page + .getByRole("button", { name: "Cancel dictation", exact: true }) + .boundingBox(); + expect(bounds).not.toBeNull(); + expect(cancel).not.toBeNull(); + expect(bounds!.y).toBeGreaterThanOrEqual(cancel!.y); + expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(cancel!.y + cancel!.height); +} + +export async function retryDictation(page: Page) { + await page.getByRole("button", { name: "Retry dictation", exact: true }).click(); +} + +export async function discardDictation(page: Page) { + await page.getByRole("button", { name: "Cancel dictation", exact: true }).click(); } diff --git a/packages/app/src/components/dictation-controls.tsx b/packages/app/src/components/dictation-controls.tsx index 95e75497d4..f95607b93f 100644 --- a/packages/app/src/components/dictation-controls.tsx +++ b/packages/app/src/components/dictation-controls.tsx @@ -28,8 +28,7 @@ interface DictationControlsProps { const EMPTY_REASON_MESSAGE_KEYS = { "no-audio": "message.dictation.empty.noAudio", - "silent-audio": "message.dictation.empty.silentAudio", - "no-speech": "message.dictation.empty.noSpeech", + "no-transcript": "message.dictation.empty.noTranscript", } as const satisfies Record; // The line under the meter once an attempt has finished without text. @@ -41,7 +40,7 @@ function resolveResultText(input: { }): string | null { const { t, status, errorText, emptyReason } = input; if (status === "empty") { - return t(EMPTY_REASON_MESSAGE_KEYS[emptyReason ?? "no-speech"]); + return t(EMPTY_REASON_MESSAGE_KEYS[emptyReason ?? "no-transcript"]); } if (status !== "failed") { return null; @@ -237,16 +236,18 @@ export function DictationOverlay({ - - - {formatDuration(duration)} - + {!resultText ? ( + + + {formatDuration(duration)} + + ) : null} {status === "paused" ? ( {t("message.dictation.paused")} diff --git a/packages/app/src/dictation/empty-transcript-reason.test.ts b/packages/app/src/dictation/empty-transcript-reason.test.ts index f316d27daf..2a73e813dd 100644 --- a/packages/app/src/dictation/empty-transcript-reason.test.ts +++ b/packages/app/src/dictation/empty-transcript-reason.test.ts @@ -1,50 +1,12 @@ -import { Buffer } from "buffer"; import { describe, expect, it } from "vitest"; import { resolveDictationEmptyReason } from "@/dictation/empty-transcript-reason"; -import { - DICTATION_SILENCE_PEAK_THRESHOLD, - isAudiblePeak, - pcm16PeakFromBase64, -} from "@/dictation/pcm-level"; - -function pcm16Base64(samples: number[]): string { - const bytes = Buffer.alloc(samples.length * 2); - samples.forEach((sample, index) => bytes.writeInt16LE(sample, index * 2)); - return bytes.toString("base64"); -} - -describe("pcm16PeakFromBase64", () => { - it("returns the largest absolute sample, including negative ones", () => { - expect(pcm16PeakFromBase64(pcm16Base64([0, 120, -4000, 2500]))).toBe(4000); - }); - - it("reads digital silence as zero", () => { - expect(pcm16PeakFromBase64(pcm16Base64([0, 0, 0, 0]))).toBe(0); - }); - - it("ignores a trailing odd byte", () => { - const bytes = Buffer.concat([Buffer.from(pcm16Base64([-700]), "base64"), Buffer.from([0x7f])]); - expect(pcm16PeakFromBase64(bytes.toString("base64"))).toBe(700); - }); -}); - -describe("isAudiblePeak", () => { - it("uses the daemon's default silence threshold", () => { - expect(isAudiblePeak(DICTATION_SILENCE_PEAK_THRESHOLD - 1)).toBe(false); - expect(isAudiblePeak(DICTATION_SILENCE_PEAK_THRESHOLD)).toBe(true); - }); -}); describe("resolveDictationEmptyReason", () => { - it("reports missing audio when the microphone delivered nothing", () => { - expect(resolveDictationEmptyReason({ segmentCount: 0, peak: 0 })).toBe("no-audio"); + it("reports when the microphone delivered no audio", () => { + expect(resolveDictationEmptyReason({ segmentCount: 0 })).toBe("no-audio"); }); - it("reports silent audio when nothing crossed the silence threshold", () => { - expect(resolveDictationEmptyReason({ segmentCount: 40, peak: 120 })).toBe("silent-audio"); - }); - - it("reports no recognized speech when audible audio produced no text", () => { - expect(resolveDictationEmptyReason({ segmentCount: 40, peak: 9000 })).toBe("no-speech"); + it("does not infer the server's reason from recorded audio", () => { + expect(resolveDictationEmptyReason({ segmentCount: 40 })).toBe("no-transcript"); }); }); diff --git a/packages/app/src/dictation/empty-transcript-reason.ts b/packages/app/src/dictation/empty-transcript-reason.ts index 98e7633292..1c3e49d866 100644 --- a/packages/app/src/dictation/empty-transcript-reason.ts +++ b/packages/app/src/dictation/empty-transcript-reason.ts @@ -1,24 +1,6 @@ -import { isAudiblePeak } from "./pcm-level"; +/** What the client knows after a dictation finishes without text. */ +export type DictationEmptyReason = "no-audio" | "no-transcript"; -/** - * Why a finished dictation produced no text. - * - * - `no-audio`: the microphone delivered no audio at all. - * - `silent-audio`: audio arrived, but it never rose above the daemon's silence - * threshold, so the daemon dropped it before speech-to-text. - * - `no-speech`: audible audio reached speech-to-text, which returned no words. - */ -export type DictationEmptyReason = "no-audio" | "silent-audio" | "no-speech"; - -export function resolveDictationEmptyReason(input: { - segmentCount: number; - peak: number; -}): DictationEmptyReason { - if (input.segmentCount === 0) { - return "no-audio"; - } - if (!isAudiblePeak(input.peak)) { - return "silent-audio"; - } - return "no-speech"; +export function resolveDictationEmptyReason(input: { segmentCount: number }): DictationEmptyReason { + return input.segmentCount === 0 ? "no-audio" : "no-transcript"; } diff --git a/packages/app/src/dictation/pcm-level.ts b/packages/app/src/dictation/pcm-level.ts deleted file mode 100644 index 245585155f..0000000000 --- a/packages/app/src/dictation/pcm-level.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Buffer } from "buffer"; - -// Mirrors the daemon's default `PASEO_DICTATION_SILENCE_PEAK_THRESHOLD` -// (packages/server/src/server/dictation/dictation-stream-manager.ts). Audio whose -// peak stays below it is cleared by the daemon and never reaches speech-to-text. -// The `volume` level from the audio sources can't be used for this: native reports -// a dB curve and web a linear RMS, so the same input reads differently per platform. -export const DICTATION_SILENCE_PEAK_THRESHOLD = 300; - -export function pcm16PeakFromBase64(base64Pcm: string): number { - const bytes = Buffer.from(base64Pcm, "base64"); - let peak = 0; - for (let offset = 0; offset + 1 < bytes.length; offset += 2) { - const sample = Math.abs(bytes.readInt16LE(offset)); - if (sample > peak) { - peak = sample; - } - } - return peak; -} - -export function isAudiblePeak(peak: number): boolean { - return peak >= DICTATION_SILENCE_PEAK_THRESHOLD; -} diff --git a/packages/app/src/hooks/use-dictation.shared.ts b/packages/app/src/hooks/use-dictation.shared.ts index d2b6d3748b..e2e885428f 100644 --- a/packages/app/src/hooks/use-dictation.shared.ts +++ b/packages/app/src/hooks/use-dictation.shared.ts @@ -7,7 +7,7 @@ export type DictationStatus = | "paused" | "uploading" | "failed" - // Finished, but produced no text. `emptyReason` says why; the audio is kept for retry. + // Finished without text; captured audio remains available for retry. | "empty"; export interface UseDictationOptions { diff --git a/packages/app/src/hooks/use-dictation.test.tsx b/packages/app/src/hooks/use-dictation.test.tsx deleted file mode 100644 index 0b0ce158ad..0000000000 --- a/packages/app/src/hooks/use-dictation.test.tsx +++ /dev/null @@ -1,151 +0,0 @@ -/** @vitest-environment jsdom */ -import { Buffer } from "buffer"; -import { act, renderHook } from "@testing-library/react"; -import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { DictationAudioSourceConfig } from "./use-dictation-audio-source.types"; -import { useDictation, type UseDictationOptions } from "./use-dictation"; - -const audioSource = vi.hoisted(() => ({ - config: null as DictationAudioSourceConfig | null, -})); - -vi.mock("@/hooks/use-dictation-audio-source", () => ({ - useDictationAudioSource: (config: DictationAudioSourceConfig) => { - audioSource.config = config; - return { - start: async () => undefined, - stop: async () => undefined, - volume: 0, - isPaused: false, - }; - }, -})); - -function pcm16Base64(samples: number[]): string { - const bytes = Buffer.alloc(samples.length * 2); - samples.forEach((sample, index) => bytes.writeInt16LE(sample, index * 2)); - return bytes.toString("base64"); -} - -class FakeDictationClient { - isConnected = true; - transcript = ""; - finishes: Array<{ dictationId: string; finalSeq: number }> = []; - private readonly rawListeners = new Set<(message: SessionOutboundMessage) => void>(); - - async startDictationStream(): Promise {} - - sendDictationStreamChunk(dictationId: string, seq: number): void { - const ack = { type: "dictation_stream_ack", payload: { dictationId, ackSeq: seq } }; - for (const listener of this.rawListeners) { - listener(ack as SessionOutboundMessage); - } - } - - async finishDictationStream(dictationId: string, finalSeq: number) { - this.finishes.push({ dictationId, finalSeq }); - return { dictationId, text: this.transcript }; - } - - cancelDictationStream(): void {} - - subscribeRawMessages(handler: (message: SessionOutboundMessage) => void): () => void { - this.rawListeners.add(handler); - return () => this.rawListeners.delete(handler); - } - - subscribeConnectionStatus(): () => void { - return () => undefined; - } - - on(): () => void { - return () => undefined; - } -} - -function renderDictation(client: FakeDictationClient) { - const onTranscript = vi.fn(); - const view = renderHook(() => - useDictation({ - client: client as unknown as UseDictationOptions["client"], - onTranscript, - }), - ); - return { view, onTranscript }; -} - -async function record(view: ReturnType["view"], segments: string[]) { - await act(async () => { - await view.result.current.startDictation(); - }); - await act(async () => { - for (const segment of segments) { - audioSource.config?.onPcmSegment(segment); - } - }); - await act(async () => { - await view.result.current.confirmDictation(); - }); -} - -describe("useDictation empty results", () => { - beforeEach(() => { - audioSource.config = null; - }); - - it("explains a silent recording and keeps its audio for retry", async () => { - const client = new FakeDictationClient(); - const { view, onTranscript } = renderDictation(client); - - await record(view, [pcm16Base64([0, 40, -60]), pcm16Base64([10, -20, 0])]); - - expect(view.result.current.status).toBe("empty"); - expect(view.result.current.emptyReason).toBe("silent-audio"); - expect(onTranscript).not.toHaveBeenCalled(); - - client.transcript = "hello there"; - await act(async () => { - await view.result.current.retryFailedDictation(); - }); - - expect(client.finishes.map((finish) => finish.finalSeq)).toEqual([1, 1]); - expect(onTranscript).toHaveBeenCalledWith("hello there", expect.anything()); - expect(view.result.current.status).toBe("idle"); - expect(view.result.current.emptyReason).toBeNull(); - }); - - it("says when audible speech produced no text", async () => { - const client = new FakeDictationClient(); - const { view } = renderDictation(client); - - await record(view, [pcm16Base64([0, 9000, -12000])]); - - expect(view.result.current.status).toBe("empty"); - expect(view.result.current.emptyReason).toBe("no-speech"); - }); - - it("says when the microphone delivered no audio", async () => { - const client = new FakeDictationClient(); - const { view } = renderDictation(client); - - await record(view, []); - - expect(view.result.current.status).toBe("empty"); - expect(view.result.current.emptyReason).toBe("no-audio"); - expect(client.finishes).toEqual([]); - }); - - it("clears the empty result when dismissed", async () => { - const client = new FakeDictationClient(); - const { view } = renderDictation(client); - - await record(view, [pcm16Base64([5, 5])]); - act(() => { - view.result.current.discardFailedDictation(); - }); - - expect(view.result.current.status).toBe("idle"); - expect(view.result.current.emptyReason).toBeNull(); - }); -}); diff --git a/packages/app/src/hooks/use-dictation.ts b/packages/app/src/hooks/use-dictation.ts index c1c27e575d..f2c8f050f9 100644 --- a/packages/app/src/hooks/use-dictation.ts +++ b/packages/app/src/hooks/use-dictation.ts @@ -6,7 +6,6 @@ import { resolveDictationEmptyReason, type DictationEmptyReason, } from "@/dictation/empty-transcript-reason"; -import { pcm16PeakFromBase64 } from "@/dictation/pcm-level"; import { useDictationAudioSource } from "@/hooks/use-dictation-audio-source"; import { generateMessageId } from "@/types/stream"; import { AttemptGuard } from "@/utils/attempt-guard"; @@ -40,8 +39,6 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { const [status, setStatus] = useState("idle"); const [emptyReason, setEmptyReason] = useState(null); const latestPartialTranscriptRef = useRef(""); - // Loudest sample captured in the current recording; explains an empty transcript. - const recordingPeakRef = useRef(0); const onTranscriptRef = useRef(onTranscript); useEffect(() => { @@ -141,7 +138,6 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { const clearStreamingState = useCallback(() => { senderRef.current?.clearAll(); - recordingPeakRef.current = 0; latestPartialTranscriptRef.current = ""; setPartialTranscript(""); setEmptyReason(null); @@ -206,12 +202,11 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { text.trim().length > 0 ? text.trim() : latestPartialTranscriptRef.current.trim(); if (!transcriptText) { - // Keep the audio so the user can retry, and say why nothing came back. + // Keep the audio until the user retries or discards this empty result. setStatus("empty"); setEmptyReason( resolveDictationEmptyReason({ segmentCount: senderRef.current?.getSegmentCount() ?? 0, - peak: recordingPeakRef.current, }), ); return; @@ -246,7 +241,6 @@ export function useDictation(options: UseDictationOptions): UseDictationResult { const audio = useDictationAudioSource({ onPcmSegment: (audioData) => { - recordingPeakRef.current = Math.max(recordingPeakRef.current, pcm16PeakFromBase64(audioData)); senderRef.current?.enqueueSegment(audioData); }, onError: (err) => { diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 91d09ee263..c236b3739a 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -373,8 +373,7 @@ export const ar: TranslationResources = { failedRetry: "فشل الإملاء. اضغط على إعادة المحاولة.", empty: { noAudio: "لم يُسجَّل أي صوت. هل يستخدم تطبيق آخر الميكروفون؟", - silentAudio: "سُجِّل صمت فقط. تحقّق من الميكروفون.", - noSpeech: "لم يُتعرَّف على أي كلام. اضغط على إعادة المحاولة للمحاولة مجددًا.", + noTranscript: "لم يتم إرجاع نص. اضغط على إعادة المحاولة.", }, }, question: { diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 2b8e97a914..ebe9a13c4f 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -370,8 +370,7 @@ export const en = { failedRetry: "Dictation failed. Tap retry.", empty: { noAudio: "No audio was recorded. Is another app using the microphone?", - silentAudio: "Only silence was recorded. Check your microphone.", - noSpeech: "No speech was recognized. Tap retry to try again.", + noTranscript: "No transcription was returned. Tap retry to try again.", }, }, question: { diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index 433c7d125a..5592443ad3 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -378,8 +378,7 @@ export const es: TranslationResources = { failedRetry: "El dictado falló. Toca reintentar.", empty: { noAudio: "No se grabó audio. ¿Otra app está usando el micrófono?", - silentAudio: "Solo se grabó silencio. Revisa tu micrófono.", - noSpeech: "No se reconoció ninguna voz. Toca reintentar para volver a intentarlo.", + noTranscript: "No se devolvió ninguna transcripción. Toca reintentar.", }, }, question: { diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index e411f97cfe..074333eec6 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -378,8 +378,7 @@ export const fr: TranslationResources = { failedRetry: "La dictée a échoué. Appuyez sur Réessayer.", empty: { noAudio: "Aucun son n'a été enregistré. Une autre app utilise-t-elle le micro ?", - silentAudio: "Seul du silence a été enregistré. Vérifiez votre micro.", - noSpeech: "Aucune parole reconnue. Appuyez sur Réessayer pour recommencer.", + noTranscript: "Aucune transcription reçue. Appuyez sur Réessayer.", }, }, question: { diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index dbcd34d77d..da2fbe39f6 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -378,8 +378,7 @@ export const ja: TranslationResources = { failedRetry: "音声入力に失敗しました。再試行をタップしてください。", empty: { noAudio: "音声が録音されませんでした。別のアプリがマイクを使用していませんか?", - silentAudio: "無音しか録音されませんでした。マイクを確認してください。", - noSpeech: "音声を認識できませんでした。再試行をタップしてください。", + noTranscript: "文字起こしが返されませんでした。再試行をタップしてください。", }, }, question: { diff --git a/packages/app/src/i18n/resources/ko.ts b/packages/app/src/i18n/resources/ko.ts index d5e22aea9d..6a726a0d16 100644 --- a/packages/app/src/i18n/resources/ko.ts +++ b/packages/app/src/i18n/resources/ko.ts @@ -374,8 +374,7 @@ export const ko: TranslationResources = { failedRetry: "받아쓰기에 실패했습니다. 다시 시도하려면 누르세요.", empty: { noAudio: "녹음된 오디오가 없습니다. 다른 앱이 마이크를 사용 중인가요?", - silentAudio: "무음만 녹음되었습니다. 마이크를 확인하세요.", - noSpeech: "음성이 인식되지 않았습니다. 다시 시도하려면 누르세요.", + noTranscript: "전사 결과가 없습니다. 다시 시도하려면 누르세요.", }, }, question: { diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 4dab47d71e..759141ab6e 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -377,8 +377,7 @@ export const ptBR: TranslationResources = { failedRetry: "Falha no ditado. Toque para tentar novamente.", empty: { noAudio: "Nenhum áudio foi gravado. Outro app está usando o microfone?", - silentAudio: "Só foi gravado silêncio. Verifique seu microfone.", - noSpeech: "Nenhuma fala foi reconhecida. Toque para tentar novamente.", + noTranscript: "Nenhuma transcrição foi retornada. Toque para tentar novamente.", }, }, question: { diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index da7fee5150..08fa07513e 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -377,8 +377,7 @@ export const ru: TranslationResources = { failedRetry: "Диктовка не удалась. Нажмите «Повторить».", empty: { noAudio: "Звук не записан. Возможно, микрофон занят другим приложением?", - silentAudio: "Записана только тишина. Проверьте микрофон.", - noSpeech: "Речь не распознана. Нажмите «Повторить», чтобы попробовать снова.", + noTranscript: "Текст не получен. Нажмите «Повторить».", }, }, question: { diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 65e8416d95..6e939bd1dd 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -373,8 +373,7 @@ export const zhCN: TranslationResources = { failedRetry: "听写失败。点按重试。", empty: { noAudio: "未录到音频。是否有其他应用正在使用麦克风?", - silentAudio: "只录到了静音。请检查麦克风。", - noSpeech: "未识别到语音。点按重试再试一次。", + noTranscript: "未返回转写文本。点按重试。", }, }, question: {