From be30f736dc08fd36a85b153ff789e3f6f7aff8c6 Mon Sep 17 00:00:00 2001 From: Mephistic Date: Thu, 13 Mar 2025 03:40:56 -0400 Subject: [PATCH 1/5] Don't merge - just playing with assembly ai --- pages/video-transcription.jsx | 120 +++++++++++++++++++++++++++ styles/VideoTranscription.module.css | 71 ++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 pages/video-transcription.jsx create mode 100644 styles/VideoTranscription.module.css diff --git a/pages/video-transcription.jsx b/pages/video-transcription.jsx new file mode 100644 index 000000000..b81e41d37 --- /dev/null +++ b/pages/video-transcription.jsx @@ -0,0 +1,120 @@ +import { useEffect, useRef, useState } from "react" +import Head from "next/head" +import styles from "../styles/VideoTranscription.module.css" +import { firestore } from "../components/firebase" +import { doc, getDoc } from "firebase/firestore" + +export default function VideoTranscription({ videoUrl, utterances }) { + const [currentTime, setCurrentTime] = useState(0) + const videoRef = useRef(null) + const transcriptionRef = useRef(null) + const utteranceRefs = useRef({}) + + // Update current time when video plays + const handleTimeUpdate = () => { + if (videoRef.current) { + setCurrentTime(videoRef.current.currentTime * 1000) // Convert to ms + } + } + + // Scroll to the current utterance + useEffect(() => { + const currentUtterance = utterances.find( + utterance => + currentTime >= utterance.start && currentTime <= utterance.end + ) + + if (currentUtterance && utteranceRefs.current[currentUtterance.start]) { + const element = utteranceRefs.current[currentUtterance.start] + const container = transcriptionRef.current + + if (container) { + container.scrollTop = element.offsetTop - container.offsetTop - 100 // Offset for better visibility + } + } + }, [currentTime, utterances]) + + // Click on transcription to seek video + const seekToTime = startTime => { + if (videoRef.current) { + videoRef.current.currentTime = startTime / 1000 // Convert ms to seconds + } + } + + return ( +
+ + Video Transcription + + + +
+
+
+ +
+

Transcription

+
+ {utterances.map(utterance => { + const isActive = + currentTime >= utterance.start && currentTime <= utterance.end + return ( +
(utteranceRefs.current[utterance.start] = el)} + className={`${styles.utterance} ${ + isActive ? styles.active : "" + }`} + onClick={() => seekToTime(utterance.start)} + > + + {formatTime(utterance.start)} - {formatTime(utterance.end)} + +

{utterance.text}

+
+ ) + })} +
+
+
+
+ ) +} + +// Helper function to format milliseconds to MM:SS format +function formatTime(ms) { + const totalSeconds = Math.floor(ms / 1000) + const minutes = Math.floor(totalSeconds / 60) + const seconds = totalSeconds % 60 + return `${minutes.toString().padStart(2, "0")}:${seconds + .toString() + .padStart(2, "0")}` +} + +export async function getServerSideProps() { + const exampleTranscriptionId = "17c91397-c023-4f28-a621-4cef45c70749" + const transcription = await getDoc( + doc(firestore, `transcriptions/${exampleTranscriptionId}`) + ) + console.log(transcription.data()) + + const videoUrl = transcription.data().audio_url + const utterances = transcription.data().utterances + + return { + props: { + videoUrl, + utterances + } + } +} diff --git a/styles/VideoTranscription.module.css b/styles/VideoTranscription.module.css new file mode 100644 index 000000000..386f2a3fd --- /dev/null +++ b/styles/VideoTranscription.module.css @@ -0,0 +1,71 @@ +.container { + padding: 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.main { + display: flex; + flex-direction: column; + gap: 2rem; +} + +@media (min-width: 768px) { + .main { + flex-direction: row; + } +} + +.videoContainer { + flex: 1; +} + +.video { + width: 100%; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.transcriptionContainer { + flex: 1; + max-height: 600px; + overflow-y: auto; + padding: 1rem; + border: 1px solid #e5e5e5; + border-radius: 8px; + background-color: #f9f9f9; +} + +.transcription { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.utterance { + padding: 0.75rem; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.utterance:hover { + background-color: #f0f0f0; +} + +.active { + background-color: #e6f7ff; + border-left: 3px solid #1890ff; +} + +.timestamp { + font-size: 0.8rem; + color: #666; + display: block; + margin-bottom: 0.25rem; +} + +.utterance p { + margin: 0; + line-height: 1.5; +} From 18025f3af9d431ff77a1fe5c067e01719b13bab3 Mon Sep 17 00:00:00 2001 From: Mephistic Date: Sat, 15 Mar 2025 22:39:59 -0400 Subject: [PATCH 2/5] Adding hearing-specific page - will not work until we actually start transcribing hearings --- pages/hearing/[...hearingId].tsx | 150 +++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 pages/hearing/[...hearingId].tsx diff --git a/pages/hearing/[...hearingId].tsx b/pages/hearing/[...hearingId].tsx new file mode 100644 index 000000000..7bec608f1 --- /dev/null +++ b/pages/hearing/[...hearingId].tsx @@ -0,0 +1,150 @@ +import { useEffect, useRef, useState } from "react" +import Head from "next/head" +import styles from "../../styles/VideoTranscription.module.css" // Adjust the path as necessary +import { firestore } from "../../components/firebase" +import { doc, getDoc } from "firebase/firestore" +import { z } from "zod" +import { GetServerSideProps } from "next" +import { serverSideTranslations } from "next-i18next/serverSideTranslations" + +const Query = z.object({ hearingId: z.string({}) }) + +export default function VideoTranscription({ + videoUrl, + utterances +}: { + videoUrl: any + utterances: Array +}) { + const [currentTime, setCurrentTime] = useState(0) + const videoRef = useRef(null) + const transcriptionRef = useRef(null) + const utteranceRefs = useRef({}) + + // Update current time when video plays + const handleTimeUpdate = () => { + if (videoRef.current) { + setCurrentTime(videoRef.current.currentTime * 1000) // Convert to ms + } + } + + // Scroll to the current utterance + useEffect(() => { + const currentUtterance = utterances.find( + utterance => + currentTime >= utterance.start && currentTime <= utterance.end + ) + + if (currentUtterance && utteranceRefs.current[currentUtterance.start]) { + const element = utteranceRefs.current[currentUtterance.start] + const container = transcriptionRef.current + + if (container) { + container.scrollTop = element.offsetTop - container.offsetTop - 100 // Offset for better visibility + } + } + }, [currentTime, utterances]) + + // Click on transcription to seek video + const seekToTime = (startTime: number) => { + if (videoRef.current) { + videoRef.current.currentTime = startTime / 1000 // Convert ms to seconds + } + } + + return ( +
+ + Video Transcription + + + +
+
+
+ +
+

Transcription

+
+ {utterances.map(utterance => { + const isActive = + currentTime >= utterance.start && currentTime <= utterance.end + return ( +
(utteranceRefs.current[utterance.start] = el)} + className={`${styles.utterance} ${ + isActive ? styles.active : "" + }`} + onClick={() => seekToTime(utterance.start)} + > + + {formatTime(utterance.start)} - {formatTime(utterance.end)} + +

{utterance.text}

+
+ ) + })} +
+
+
+
+ ) +} + +// Helper function to format milliseconds to MM:SS format +function formatTime(ms: number) { + const totalSeconds = Math.floor(ms / 1000) + const minutes = Math.floor(totalSeconds / 60) + const seconds = totalSeconds % 60 + return `${minutes.toString().padStart(2, "0")}:${seconds + .toString() + .padStart(2, "0")}` +} + +export const getServerSideProps: GetServerSideProps = async ctx => { + const locale = ctx.locale ?? ctx.defaultLocale ?? "en" + const query = Query.safeParse(ctx.query) + if (!query.success) return { notFound: true } + const { hearingId } = query.data + + const rawHearing = await getDoc(doc(firestore, `events/hearing-${hearingId}`)) + if (!rawHearing.exists) return { notFound: true } + + const hearing = rawHearing.data() as any + const transcriptionId = hearing.videoAssemblyId + if (!transcriptionId) return { notFound: true } + + const rawTranscription = await getDoc( + doc(firestore, `transcriptions/${transcriptionId}`) + ) + if (!rawTranscription.exists()) return { notFound: true } + const transcription = rawTranscription.data() as any + + const videoUrl = transcription.data().audio_url + const utterances = transcription.data().utterances + + return { + props: { + videoUrl, + utterances, + ...(await serverSideTranslations(locale, [ + "auth", + "common", + "footer", + "testimony", + "profile" + ])) + } + } +} From ad229e70269b7aa60c1c3192afd814a9b06b133f Mon Sep 17 00:00:00 2001 From: Mephistic Date: Thu, 17 Apr 2025 09:38:22 -0400 Subject: [PATCH 3/5] Updating hearing-specific page to use new utterances subcollection --- pages/hearing/[...hearingId].tsx | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/pages/hearing/[...hearingId].tsx b/pages/hearing/[...hearingId].tsx index 7bec608f1..1a1aab93e 100644 --- a/pages/hearing/[...hearingId].tsx +++ b/pages/hearing/[...hearingId].tsx @@ -2,7 +2,15 @@ import { useEffect, useRef, useState } from "react" import Head from "next/head" import styles from "../../styles/VideoTranscription.module.css" // Adjust the path as necessary import { firestore } from "../../components/firebase" -import { doc, getDoc } from "firebase/firestore" +import { + collection, + doc, + getDoc, + getDocs, + where, + orderBy, + query as fbQuery +} from "firebase/firestore" import { z } from "zod" import { GetServerSideProps } from "next" import { serverSideTranslations } from "next-i18next/serverSideTranslations" @@ -132,7 +140,23 @@ export const getServerSideProps: GetServerSideProps = async ctx => { const transcription = rawTranscription.data() as any const videoUrl = transcription.data().audio_url - const utterances = transcription.data().utterances + + const docRef = collection( + firestore, + `transcriptions/${transcriptionId}/utterances` + ) + const q = fbQuery(docRef, orderBy("start", "asc")) + + const rawUtterances = await getDocs(q) + if (rawUtterances.empty) { + console.log("No utterances found") + return { notFound: true } + } + + const utterances = rawUtterances.docs.map(doc => ({ + ...doc.data(), + id: doc.id + })) return { props: { From f4f97eca14e67326f670f4b6abb42b31752241a0 Mon Sep 17 00:00:00 2001 From: Mephistic Date: Wed, 4 Jun 2025 05:03:32 -0400 Subject: [PATCH 4/5] Use newer transcription format + switch example transcription to a better example --- pages/video-transcription.jsx | 37 ++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/pages/video-transcription.jsx b/pages/video-transcription.jsx index b81e41d37..28a07b83c 100644 --- a/pages/video-transcription.jsx +++ b/pages/video-transcription.jsx @@ -2,7 +2,16 @@ import { useEffect, useRef, useState } from "react" import Head from "next/head" import styles from "../styles/VideoTranscription.module.css" import { firestore } from "../components/firebase" -import { doc, getDoc } from "firebase/firestore" +import { + collection, + doc, + getDoc, + getDocs, + orderBy, + query +} from "firebase/firestore" +import { useQuery } from "react-query" +import { useRouter } from "next/router" export default function VideoTranscription({ videoUrl, utterances }) { const [currentTime, setCurrentTime] = useState(0) @@ -102,19 +111,29 @@ function formatTime(ms) { } export async function getServerSideProps() { - const exampleTranscriptionId = "17c91397-c023-4f28-a621-4cef45c70749" + const hearingId = "hearing-5180" + const hearing = await getDoc(doc(firestore, `events/${hearingId}`)) + const { videoTranscriptionId, videoURL } = hearing.data() + + // should be + // const exampleTranscriptionId = "639e73ff-bd01-4902-bba7-88faaf39afa9" const transcription = await getDoc( - doc(firestore, `transcriptions/${exampleTranscriptionId}`) + doc(firestore, `transcriptions/${videoTranscriptionId}`) + ) + const utterances = await getDocs( + query( + collection( + firestore, + `transcriptions/${videoTranscriptionId}/utterances` + ), + orderBy("start", "asc") + ) ) - console.log(transcription.data()) - - const videoUrl = transcription.data().audio_url - const utterances = transcription.data().utterances return { props: { - videoUrl, - utterances + videoUrl: videoURL, + utterances: utterances.docs.map(doc => doc.data()) } } } From 77e961ba5ade6ee367e5e0cddf63b6484eb8a855 Mon Sep 17 00:00:00 2001 From: Mephistic Date: Wed, 4 Jun 2025 05:19:12 -0400 Subject: [PATCH 5/5] Fixing parameterized hearing page to use new transcription format, providing example transcription --- .../{[...hearingId].tsx => [hearingId].tsx} | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) rename pages/hearing/{[...hearingId].tsx => [hearingId].tsx} (85%) diff --git a/pages/hearing/[...hearingId].tsx b/pages/hearing/[hearingId].tsx similarity index 85% rename from pages/hearing/[...hearingId].tsx rename to pages/hearing/[hearingId].tsx index 1a1aab93e..9dc0e24db 100644 --- a/pages/hearing/[...hearingId].tsx +++ b/pages/hearing/[hearingId].tsx @@ -122,37 +122,42 @@ function formatTime(ms: number) { export const getServerSideProps: GetServerSideProps = async ctx => { const locale = ctx.locale ?? ctx.defaultLocale ?? "en" - const query = Query.safeParse(ctx.query) + const query = Query.safeParse(ctx.params) if (!query.success) return { notFound: true } const { hearingId } = query.data + // Example: const hearingId = "hearing-5180" const rawHearing = await getDoc(doc(firestore, `events/hearing-${hearingId}`)) - if (!rawHearing.exists) return { notFound: true } - + if (!rawHearing.exists()) return { notFound: true } const hearing = rawHearing.data() as any - const transcriptionId = hearing.videoAssemblyId - if (!transcriptionId) return { notFound: true } + const { videoTranscriptionId, videoURL } = hearing + if (!videoTranscriptionId || !videoURL) { + return { notFound: true } + } + // Example: constt videoTranscriptionId = "639e73ff-bd01-4902-bba7-88faaf39afa9" const rawTranscription = await getDoc( - doc(firestore, `transcriptions/${transcriptionId}`) + doc(firestore, `transcriptions/${videoTranscriptionId}`) ) if (!rawTranscription.exists()) return { notFound: true } const transcription = rawTranscription.data() as any - - const videoUrl = transcription.data().audio_url - - const docRef = collection( - firestore, - `transcriptions/${transcriptionId}/utterances` + console.log( + `Hearing ${hearingId} was transcribed at: ${transcription.createdAt?.toDate()}` ) - const q = fbQuery(docRef, orderBy("start", "asc")) - const rawUtterances = await getDocs(q) + const rawUtterances = await getDocs( + fbQuery( + collection( + firestore, + `transcriptions/${videoTranscriptionId}/utterances` + ), + orderBy("start", "asc") + ) + ) if (rawUtterances.empty) { console.log("No utterances found") return { notFound: true } } - const utterances = rawUtterances.docs.map(doc => ({ ...doc.data(), id: doc.id @@ -160,7 +165,7 @@ export const getServerSideProps: GetServerSideProps = async ctx => { return { props: { - videoUrl, + videoUrl: videoURL, utterances, ...(await serverSideTranslations(locale, [ "auth",