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: {