From 5f143fe3640875dcb56c26694254a93189281d17 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Mon, 15 Sep 2025 10:49:20 -0600 Subject: [PATCH 1/6] fix: zulip and consent handler on the file pipeline (#645) --- .../reflector/pipelines/main_file_pipeline.py | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/server/reflector/pipelines/main_file_pipeline.py b/server/reflector/pipelines/main_file_pipeline.py index 5c57dddb..ce9d000e 100644 --- a/server/reflector/pipelines/main_file_pipeline.py +++ b/server/reflector/pipelines/main_file_pipeline.py @@ -12,7 +12,7 @@ from pathlib import Path import av import structlog -from celery import shared_task +from celery import chain, shared_task from reflector.asynctask import asynctask from reflector.db.rooms import rooms_controller @@ -26,6 +26,8 @@ from reflector.logger import logger from reflector.pipelines.main_live_pipeline import ( PipelineMainBase, broadcast_to_sockets, + task_cleanup_consent, + task_pipeline_post_to_zulip, ) from reflector.processors import ( AudioFileWriterProcessor, @@ -379,6 +381,28 @@ class PipelineMainFile(PipelineMainBase): await processor.flush() +@shared_task +@asynctask +async def task_send_webhook_if_needed(*, transcript_id: str): + """Send webhook if this is a room recording with webhook configured""" + transcript = await transcripts_controller.get_by_id(transcript_id) + if not transcript: + return + + if transcript.source_kind == SourceKind.ROOM and transcript.room_id: + room = await rooms_controller.get_by_id(transcript.room_id) + if room and room.webhook_url: + logger.info( + "Dispatching webhook", + transcript_id=transcript_id, + room_id=room.id, + webhook_url=room.webhook_url, + ) + send_transcript_webhook.delay( + transcript_id, room.id, event_id=uuid.uuid4().hex + ) + + @shared_task @asynctask async def task_pipeline_file_process(*, transcript_id: str): @@ -406,16 +430,10 @@ async def task_pipeline_file_process(*, transcript_id: str): await pipeline.set_status(transcript_id, "error") raise - # Trigger webhook if this is a room recording with webhook configured - if transcript.source_kind == SourceKind.ROOM and transcript.room_id: - room = await rooms_controller.get_by_id(transcript.room_id) - if room and room.webhook_url: - logger.info( - "Dispatching webhook task", - transcript_id=transcript_id, - room_id=room.id, - webhook_url=room.webhook_url, - ) - send_transcript_webhook.delay( - transcript_id, room.id, event_id=uuid.uuid4().hex - ) + # Run post-processing chain: consent cleanup -> zulip -> webhook + post_chain = chain( + task_cleanup_consent.si(transcript_id=transcript_id), + task_pipeline_post_to_zulip.si(transcript_id=transcript_id), + task_send_webhook_if_needed.si(transcript_id=transcript_id), + ) + post_chain.delay() From 3f1fe8c9bf0a63f26555b6bdf22f73846d3f88b1 Mon Sep 17 00:00:00 2001 From: Igor Monadical Date: Mon, 15 Sep 2025 14:19:10 -0400 Subject: [PATCH 2/6] chore: remove timeout-based auth session logic (#649) * remove timeout-based auth session logic * remove timeout-based auth session logic --------- Co-authored-by: Igor Loskutov --- www/app/lib/apiClient.tsx | 40 ++++++++++++++++++++------------------- www/app/lib/types.ts | 10 ++++++---- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/www/app/lib/apiClient.tsx b/www/app/lib/apiClient.tsx index 86f8f161..a5cec06b 100644 --- a/www/app/lib/apiClient.tsx +++ b/www/app/lib/apiClient.tsx @@ -3,8 +3,10 @@ import createClient from "openapi-fetch"; import type { paths } from "../reflector-api"; import createFetchClient from "openapi-react-query"; -import { assertExistsAndNonEmptyString } from "./utils"; +import { assertExistsAndNonEmptyString, parseNonEmptyString } from "./utils"; import { isBuildPhase } from "./next"; +import { getSession } from "next-auth/react"; +import { assertExtendedToken } from "./types"; export const API_URL = !isBuildPhase ? assertExistsAndNonEmptyString( @@ -21,29 +23,29 @@ export const client = createClient({ baseUrl: API_URL, }); -const waitForAuthTokenDefinitivePresenceOrAbscence = async () => { - let tries = 0; - let time = 0; - const STEP = 100; - while (currentAuthToken === undefined) { - await new Promise((resolve) => setTimeout(resolve, STEP)); - time += STEP; - tries++; - // most likely first try is more than enough, if it's more there's already something weird happens - if (tries > 10) { - // even when there's no auth assumed at all, we probably should explicitly call configureApiAuth(null) - throw new Error( - `Could not get auth token definitive presence/absence in ${time}ms. not calling configureApiAuth?`, - ); - } +// will assert presence/absence of login initially +const initialSessionPromise = getSession(); + +const waitForAuthTokenDefinitivePresenceOrAbsence = async () => { + const initialSession = await initialSessionPromise; + if (currentAuthToken === undefined) { + currentAuthToken = + initialSession === null + ? null + : assertExtendedToken(initialSession).accessToken; } + // otherwise already overwritten by external forces + return currentAuthToken; }; client.use({ async onRequest({ request }) { - await waitForAuthTokenDefinitivePresenceOrAbscence(); - if (currentAuthToken) { - request.headers.set("Authorization", `Bearer ${currentAuthToken}`); + const token = await waitForAuthTokenDefinitivePresenceOrAbsence(); + if (token !== null) { + request.headers.set( + "Authorization", + `Bearer ${parseNonEmptyString(token)}`, + ); } // XXX Only set Content-Type if not already set (FormData will set its own boundary) // This is a work around for uploading file, we're passing a formdata diff --git a/www/app/lib/types.ts b/www/app/lib/types.ts index af5625ec..7bcb522b 100644 --- a/www/app/lib/types.ts +++ b/www/app/lib/types.ts @@ -21,7 +21,7 @@ export interface CustomSession extends Session { // assumption that JWT is JWTWithAccessToken - we set it in jwt callback of auth; typing isn't strong around there // but the assumption is crucial to auth working export const assertExtendedToken = ( - t: T, + t: Exclude, ): T & { accessTokenExpires: number; accessToken: string; @@ -45,7 +45,7 @@ export const assertExtendedToken = ( }; export const assertExtendedTokenAndUserId = ( - t: T, + t: Exclude, ): T & { accessTokenExpires: number; accessToken: string; @@ -55,7 +55,7 @@ export const assertExtendedTokenAndUserId = ( } => { const extendedToken = assertExtendedToken(t); if (typeof (extendedToken.user as any)?.id === "string") { - return t as T & { + return t as Exclude & { accessTokenExpires: number; accessToken: string; user: U & { @@ -67,7 +67,9 @@ export const assertExtendedTokenAndUserId = ( }; // best attempt to check the session is valid -export const assertCustomSession = (s: S): CustomSession => { +export const assertCustomSession = ( + s: Exclude, +): CustomSession => { const r = assertExtendedTokenAndUserId(s); // no other checks for now return r as CustomSession; From c546e69739e68bb74fbc877eb62609928e5b8de6 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Mon, 15 Sep 2025 12:34:51 -0600 Subject: [PATCH 3/6] fix: zulip stream and topic selection in share dialog (#644) * fix: zulip stream and topic selection in share dialog Replace useListCollection with createListCollection to match the working room edit implementation. This ensures collections update when data loads, fixing the issue where streams and topics wouldn't appear until navigation. * fix: wrap createListCollection in useMemo to prevent recreation on every render Both streamCollection and topicCollection are now memoized to improve performance and prevent unnecessary re-renders of Combobox components --- www/app/(app)/transcripts/shareZulip.tsx | 43 +++++++++++------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/www/app/(app)/transcripts/shareZulip.tsx b/www/app/(app)/transcripts/shareZulip.tsx index 5cee16c1..bee14822 100644 --- a/www/app/(app)/transcripts/shareZulip.tsx +++ b/www/app/(app)/transcripts/shareZulip.tsx @@ -14,8 +14,7 @@ import { Checkbox, Combobox, Spinner, - useFilter, - useListCollection, + createListCollection, } from "@chakra-ui/react"; import { TbBrandZulip } from "react-icons/tb"; import { @@ -48,8 +47,6 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) { const { data: topics = [] } = useZulipTopics(selectedStreamId); const postToZulipMutation = useTranscriptPostToZulip(); - const { contains } = useFilter({ sensitivity: "base" }); - const streamItems = useMemo(() => { return streams.map((stream: Stream) => ({ label: stream.name, @@ -64,17 +61,21 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) { })); }, [topics]); - const { collection: streamItemsCollection, filter: streamItemsFilter } = - useListCollection({ - initialItems: streamItems, - filter: contains, - }); + const streamCollection = useMemo( + () => + createListCollection({ + items: streamItems, + }), + [streamItems], + ); - const { collection: topicItemsCollection, filter: topicItemsFilter } = - useListCollection({ - initialItems: topicItems, - filter: contains, - }); + const topicCollection = useMemo( + () => + createListCollection({ + items: topicItems, + }), + [topicItems], + ); // Update selected stream ID when stream changes useEffect(() => { @@ -156,15 +157,12 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) { # { setTopic(undefined); setStream(e.value[0]); }} - onInputValueChange={(e) => - streamItemsFilter(e.inputValue) - } openOnClick={true} positioning={{ strategy: "fixed", @@ -181,7 +179,7 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) { No streams found - {streamItemsCollection.items.map((item) => ( + {streamItems.map((item) => ( {item.label} @@ -197,12 +195,9 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) { # setTopic(e.value[0])} - onInputValueChange={(e) => - topicItemsFilter(e.inputValue) - } openOnClick selectionBehavior="replace" skipAnimationOnMount={true} @@ -222,7 +217,7 @@ export default function ShareZulip(props: ShareZulipProps & BoxProps) { No topics found - {topicItemsCollection.items.map((item) => ( + {topicItems.map((item) => ( {item.label} From b42f7cfc606783afcee792590efcc78b507468ab Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Mon, 15 Sep 2025 18:19:19 -0600 Subject: [PATCH 4/6] feat: remove profanity filter that was there for conference (#652) --- server/pyproject.toml | 1 - server/reflector/processors/types.py | 26 ++------------- server/uv.lock | 47 +++++++--------------------- 3 files changed, 14 insertions(+), 60 deletions(-) diff --git a/server/pyproject.toml b/server/pyproject.toml index 47d314d9..d055f461 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -27,7 +27,6 @@ dependencies = [ "prometheus-fastapi-instrumentator>=6.1.0", "sentencepiece>=0.1.99", "protobuf>=4.24.3", - "profanityfilter>=2.0.6", "celery>=5.3.4", "redis>=5.0.1", "python-jose[cryptography]>=3.3.0", diff --git a/server/reflector/processors/types.py b/server/reflector/processors/types.py index 480086af..7096e81c 100644 --- a/server/reflector/processors/types.py +++ b/server/reflector/processors/types.py @@ -4,11 +4,8 @@ import tempfile from pathlib import Path from typing import Annotated, TypedDict -from profanityfilter import ProfanityFilter from pydantic import BaseModel, Field, PrivateAttr -from reflector.redis_cache import redis_cache - class DiarizationSegment(TypedDict): """Type definition for diarization segment containing speaker information""" @@ -20,9 +17,6 @@ class DiarizationSegment(TypedDict): PUNC_RE = re.compile(r"[.;:?!…]") -profanity_filter = ProfanityFilter() -profanity_filter.set_censor("*") - class AudioFile(BaseModel): name: str @@ -124,21 +118,11 @@ def words_to_segments(words: list[Word]) -> list[TranscriptSegment]: class Transcript(BaseModel): translation: str | None = None - words: list[Word] = None - - @property - def raw_text(self): - # Uncensored text - return "".join([word.text for word in self.words]) - - @redis_cache(prefix="profanity", duration=3600 * 24 * 7) - def _get_censored_text(self, text: str): - return profanity_filter.censor(text).strip() + words: list[Word] = [] @property def text(self): - # Censored text - return self._get_censored_text(self.raw_text) + return "".join([word.text for word in self.words]) @property def human_timestamp(self): @@ -170,12 +154,6 @@ class Transcript(BaseModel): word.start += offset word.end += offset - def clone(self): - words = [ - Word(text=word.text, start=word.start, end=word.end) for word in self.words - ] - return Transcript(text=self.text, translation=self.translation, words=words) - def as_segments(self) -> list[TranscriptSegment]: return words_to_segments(self.words) diff --git a/server/uv.lock b/server/uv.lock index 5604f922..b93d0ac3 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -1325,15 +1325,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, ] -[[package]] -name = "inflection" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" }, -] - [[package]] name = "iniconfig" version = "2.1.0" @@ -2311,18 +2302,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/c1/bb7e334135859c3a92ec399bc89293ea73f28e815e35b43929c8db6af030/primePy-1.3-py3-none-any.whl", hash = "sha256:5ed443718765be9bf7e2ff4c56cdff71b42140a15b39d054f9d99f0009e2317a", size = 4040, upload-time = "2018-05-29T17:18:17.53Z" }, ] -[[package]] -name = "profanityfilter" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "inflection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/03/08740b5e0800f9eb9f675c149a497a3f3735e7b04e414bcce64136e7e487/profanityfilter-2.1.0.tar.gz", hash = "sha256:0ede04e92a9d7255faa52b53776518edc6586dda828aca677c74b5994dfdd9d8", size = 7910, upload-time = "2024-11-25T22:31:51.194Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/03/eb18f72dc6e6398e75e3762677f18ab3a773a384b18efd3ed9119844e892/profanityfilter-2.1.0-py2.py3-none-any.whl", hash = "sha256:e1bc07012760fd74512a335abb93a36877831ed26abab78bfe31bebb68f8c844", size = 7483, upload-time = "2024-11-25T22:31:50.129Z" }, -] - [[package]] name = "prometheus-client" version = "0.22.1" @@ -3131,7 +3110,6 @@ dependencies = [ { name = "loguru" }, { name = "nltk" }, { name = "openai" }, - { name = "profanityfilter" }, { name = "prometheus-fastapi-instrumentator" }, { name = "protobuf" }, { name = "psycopg2-binary" }, @@ -3208,7 +3186,6 @@ requires-dist = [ { name = "loguru", specifier = ">=0.7.0" }, { name = "nltk", specifier = ">=3.8.1" }, { name = "openai", specifier = ">=1.59.7" }, - { name = "profanityfilter", specifier = ">=2.0.6" }, { name = "prometheus-fastapi-instrumentator", specifier = ">=6.1.0" }, { name = "protobuf", specifier = ">=4.24.3" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, @@ -3954,8 +3931,8 @@ dependencies = [ { name = "typing-extensions", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:3d05017d19bc99741288e458888283a44b0ee881d53f05f72f8b1cfea8998122" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp311-none-macosx_11_0_arm64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl" }, ] [[package]] @@ -3980,16 +3957,16 @@ dependencies = [ { name = "typing-extensions", marker = "platform_python_implementation == 'PyPy' or sys_platform != 'darwin'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-linux_s390x.whl", hash = "sha256:2bfc013dd6efdc8f8223a0241d3529af9f315dffefb53ffa3bf14d3f10127da6" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:680129efdeeec3db5da3f88ee5d28c1b1e103b774aef40f9d638e2cce8f8d8d8" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cb06175284673a581dd91fb1965662ae4ecaba6e5c357aa0ea7bb8b84b6b7eeb" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:7631ef49fbd38d382909525b83696dc12a55d68492ade4ace3883c62b9fc140f" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_arm64.whl", hash = "sha256:41e6fc5ec0914fcdce44ccf338b1d19a441b55cafdd741fd0bf1af3f9e4cfd14" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d" }, - { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-linux_s390x.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_amd64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp311-cp311-win_arm64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl" }, + { url = "https://download.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl" }, ] [[package]] From 2ce7479967630a1fae9073804df3e9697fa06a4b Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Mon, 15 Sep 2025 22:42:53 -0600 Subject: [PATCH 5/6] chore(main): release 0.11.0 (#648) --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40174fc4..e59f1ab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [0.11.0](https://github.com/Monadical-SAS/reflector/compare/v0.10.0...v0.11.0) (2025-09-16) + + +### Features + +* remove profanity filter that was there for conference ([#652](https://github.com/Monadical-SAS/reflector/issues/652)) ([b42f7cf](https://github.com/Monadical-SAS/reflector/commit/b42f7cfc606783afcee792590efcc78b507468ab)) + + +### Bug Fixes + +* zulip and consent handler on the file pipeline ([#645](https://github.com/Monadical-SAS/reflector/issues/645)) ([5f143fe](https://github.com/Monadical-SAS/reflector/commit/5f143fe3640875dcb56c26694254a93189281d17)) +* zulip stream and topic selection in share dialog ([#644](https://github.com/Monadical-SAS/reflector/issues/644)) ([c546e69](https://github.com/Monadical-SAS/reflector/commit/c546e69739e68bb74fbc877eb62609928e5b8de6)) + ## [0.10.0](https://github.com/Monadical-SAS/reflector/compare/v0.9.0...v0.10.0) (2025-09-11) From fa049e8d068190ce7ea015fd9fcccb8543f54a3f Mon Sep 17 00:00:00 2001 From: Sergey Mankovsky Date: Tue, 16 Sep 2025 10:57:35 +0200 Subject: [PATCH 6/6] fix: ignore player hotkeys for text inputs (#646) * Ignore player hotkeys for text inputs * Fix event listener effect --- www/app/(app)/transcripts/player.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/www/app/(app)/transcripts/player.tsx b/www/app/(app)/transcripts/player.tsx index 10b02aa1..2cefe8c1 100644 --- a/www/app/(app)/transcripts/player.tsx +++ b/www/app/(app)/transcripts/player.tsx @@ -33,17 +33,27 @@ export default function Player(props: PlayerProps) { const topicsRef = useRef(props.topics); const [firstRender, setFirstRender] = useState(true); - const keyHandler = (e) => { - if (e.key == " ") { + const shouldIgnoreHotkeys = (target: EventTarget | null) => { + return ( + target instanceof HTMLInputElement || + target instanceof HTMLTextAreaElement + ); + }; + + const keyHandler = (e: KeyboardEvent) => { + if (e.key === " ") { + if (e.repeat) return; + if (shouldIgnoreHotkeys(e.target)) return; + e.preventDefault(); wavesurfer?.playPause(); } }; useEffect(() => { - document.addEventListener("keyup", keyHandler); + document.addEventListener("keydown", keyHandler); return () => { - document.removeEventListener("keyup", keyHandler); + document.removeEventListener("keydown", keyHandler); }; - }); + }, [wavesurfer]); // Waveform setup useEffect(() => {