Skip to content

Commit cfc99f0

Browse files
authored
Merge pull request #2004 from delexagon/transcript-download
Transcript download
2 parents 773dadd + c615eef commit cfc99f0

File tree

5 files changed

+131
-76
lines changed

5 files changed

+131
-76
lines changed

components/hearing/HearingDetails.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Col, Container, Image, Row } from "../bootstrap"
88
import { firestore } from "../firebase"
99
import * as links from "../links"
1010
import { Back } from "../shared/CommonComponents"
11+
import { Paragraph, fetchTranscriptionData } from "./transcription"
1112

1213
const ButtonContainer = styled.div`
1314
width: fit-content;
@@ -44,11 +45,7 @@ export const HearingDetails = ({
4445
hearingId: string | string[] | undefined
4546
}) => {
4647
const { t } = useTranslation(["common", "hearing"])
47-
const [transcriptData, setTranscriptData] = useState(null)
48-
49-
const handleTranscriptData = (data: any) => {
50-
setTranscriptData(data)
51-
}
48+
const [transcriptData, setTranscriptData] = useState<Paragraph[]>([])
5249

5350
const [videoLoaded, setVideoLoaded] = useState(false)
5451
const handleVideoLoad = () => {
@@ -85,6 +82,14 @@ export const HearingDetails = ({
8582
setVideoURL(docData?.videoURL)
8683
}, [eventId])
8784

85+
useEffect(() => {
86+
;(async function () {
87+
if (!videoTranscriptionId || transcriptData.length !== 0) return
88+
const docList = await fetchTranscriptionData(videoTranscriptionId)
89+
setTranscriptData(docList)
90+
})()
91+
}, [videoTranscriptionId])
92+
8893
useEffect(() => {
8994
hearingData()
9095
}, [hearingData])
@@ -168,11 +173,10 @@ export const HearingDetails = ({
168173
)}
169174

170175
<Transcriptions
171-
handleTranscriptData={handleTranscriptData}
176+
transcriptData={transcriptData}
172177
setCurTimeVideo={setCurTimeVideo}
173178
videoLoaded={videoLoaded}
174179
videoRef={videoRef}
175-
videoTranscriptionId={videoTranscriptionId}
176180
/>
177181
</Col>
178182

@@ -182,6 +186,8 @@ export const HearingDetails = ({
182186
committeeCode={committeeCode}
183187
generalCourtNumber={generalCourtNumber}
184188
hearingDate={hearingDate}
189+
hearingId={hearingId}
190+
transcriptData={transcriptData}
185191
/>
186192
</div>
187193
</Row>

components/hearing/HearingSidebar.tsx

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import Link from "next/link"
44
import { useCallback, useEffect, useState } from "react"
55
import type { ModalProps } from "react-bootstrap"
66
import styled from "styled-components"
7+
import Papa from "papaparse"
78
import { Col, Image, Modal, Row } from "../bootstrap"
89
import { firestore } from "../firebase"
910
import * as links from "../links"
1011
import { billSiteURL, Internal } from "../links"
1112
import { LabeledIcon } from "../shared"
13+
import { Paragraph, formatMilliseconds } from "./transcription"
1214

1315
type Bill = {
1416
BillNumber: string
@@ -116,12 +118,16 @@ export const HearingSidebar = ({
116118
billsInAgenda,
117119
committeeCode,
118120
generalCourtNumber,
119-
hearingDate
121+
hearingDate,
122+
hearingId,
123+
transcriptData
120124
}: {
121125
billsInAgenda: never[]
122126
committeeCode: string
123127
generalCourtNumber: string
124128
hearingDate: string
129+
hearingId: undefined | string | string[]
130+
transcriptData: Paragraph[]
125131
}) => {
126132
const { t } = useTranslation(["common", "hearing"])
127133

@@ -136,6 +142,8 @@ export const HearingSidebar = ({
136142
dateCheck = true
137143
}
138144

145+
const [downloadName, setDownloadName] = useState<string>("hearing.csv")
146+
const [downloadURL, setDownloadURL] = useState<string>("")
139147
const [houseChairName, setHouseChairName] = useState<string>("")
140148
const [houseChairperson, setHouseChairperson] = useState<Legislator>()
141149
const [members, setMembers] = useState<Members[]>()
@@ -176,6 +184,30 @@ export const HearingSidebar = ({
176184
setMembers(memberData)
177185
}, [committeeCode, generalCourtNumber])
178186

187+
useEffect(() => {
188+
if (!hearingId) {
189+
setDownloadName("hearing.csv")
190+
} else {
191+
setDownloadName(`hearing-${hearingId}.csv`)
192+
}
193+
}, [hearingId])
194+
195+
useEffect(() => {
196+
if (transcriptData.length === 0) return
197+
const csv_objects = transcriptData.map(doc => ({
198+
start: formatMilliseconds(doc.start),
199+
text: doc.text
200+
}))
201+
const csv = Papa.unparse(csv_objects)
202+
const blob = new Blob([csv], { type: "text/csv" })
203+
const url = URL.createObjectURL(blob)
204+
setDownloadURL(url)
205+
206+
return () => {
207+
URL.revokeObjectURL(url)
208+
}
209+
}, [transcriptData])
210+
179211
useEffect(() => {
180212
committeeCode && generalCourtNumber ? committeeData() : null
181213
}, [committeeCode, committeeData, generalCourtNumber])
@@ -193,12 +225,31 @@ export const HearingSidebar = ({
193225
{t("hearing_details", { ns: "hearing" })}
194226
</SidebarHeader>
195227

196-
{dateCheck ? (
197-
<SidebarBody className={`border-bottom fs-6 fw-bold px-3 py-3`}>
198-
<SidebarSubbody className={`mb-1`}>
199-
{t("recording_date", { ns: "hearing" })}
200-
</SidebarSubbody>
201-
<div className={`fw-normal`}>{formattedDate}</div>
228+
{dateCheck || (downloadURL !== "" && hearingId !== undefined) ? (
229+
<SidebarBody className={`border-bottom fs-6 px-3 py-3`}>
230+
{dateCheck ? (
231+
<>
232+
<SidebarSubbody className={`mb-1 fw-bold`}>
233+
{t("recording_date", { ns: "hearing" })}
234+
</SidebarSubbody>
235+
{formattedDate}
236+
</>
237+
) : (
238+
<></>
239+
)}
240+
{downloadURL !== "" && hearingId !== undefined ? (
241+
<div>
242+
<a
243+
href={downloadURL}
244+
download={downloadName}
245+
className="text-blue-600 underline"
246+
>
247+
{t("download_transcript", { ns: "hearing" })}
248+
</a>
249+
</div>
250+
) : (
251+
<></>
252+
)}
202253
</SidebarBody>
203254
) : (
204255
<></>

components/hearing/Transcriptions.tsx

Lines changed: 12 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
import { faMagnifyingGlass, faTimes } from "@fortawesome/free-solid-svg-icons"
22
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
3-
import { collection, getDocs, orderBy, query } from "firebase/firestore"
43
import { useTranslation } from "next-i18next"
54
import React, { useCallback, useEffect, useState } from "react"
65
import styled from "styled-components"
76
import { Col, Container, Row } from "../bootstrap"
8-
import { firestore } from "../firebase"
9-
10-
type Paragraph = {
11-
confidence: number
12-
end: number
13-
start: number
14-
text: string
15-
}
7+
import { Paragraph, formatMilliseconds } from "./transcription"
168

179
const ClearButton = styled(FontAwesomeIcon)`
1810
position: absolute;
@@ -54,7 +46,7 @@ const TranscriptBottom = styled(Container)`
5446
`
5547

5648
const TranscriptContainer = styled(Container)`
57-
max-height: 460px;
49+
max-height: 483px;
5850
overflow-y: auto;
5951
background-color: #ffffff;
6052
`
@@ -115,57 +107,32 @@ const SearchWrapper = styled.div`
115107
`
116108

117109
export const Transcriptions = ({
118-
handleTranscriptData,
110+
transcriptData,
119111
setCurTimeVideo,
120112
videoLoaded,
121-
videoRef,
122-
videoTranscriptionId
113+
videoRef
123114
}: {
124-
handleTranscriptData: (data: any) => void
115+
transcriptData: Paragraph[]
125116
setCurTimeVideo: any
126117
videoLoaded: boolean
127118
videoRef: any
128-
videoTranscriptionId: string
129119
}) => {
130120
const { t } = useTranslation(["common", "hearing"])
131121
const [highlightedId, setHighlightedId] = useState(-1)
132-
const [transcriptData, setTranscriptData] = useState<Paragraph[]>([])
133122
const [searchTerm, setSearchTerm] = useState("")
134-
const vid = videoTranscriptionId || "prevent FirebaseError"
135-
136-
const subscriptionRef = collection(
137-
firestore,
138-
`transcriptions/${vid}/paragraphs`
139-
)
140-
141-
const fetchTranscriptionData = useCallback(async () => {
142-
let docList: any[] = []
143-
144-
const q = query(subscriptionRef, orderBy("start"))
145-
const querySnapshot = await getDocs(q)
146-
147-
querySnapshot.forEach(doc => {
148-
// doc.data() is never undefined for query doc snapshots
149-
docList.push(doc.data())
150-
})
151-
152-
if (transcriptData.length === 0 && docList.length != 0) {
153-
setTranscriptData(docList)
154-
handleTranscriptData(docList)
155-
}
156-
}, [subscriptionRef, transcriptData])
123+
const [filteredData, setFilteredData] = useState<Paragraph[]>([])
157124

158125
const handleClearInput = () => {
159126
setSearchTerm("")
160127
}
161128

162129
useEffect(() => {
163-
fetchTranscriptionData()
164-
}, [fetchTranscriptionData])
165-
166-
const filteredData = transcriptData.filter(el =>
167-
el.text.toLowerCase().includes(searchTerm.toLowerCase())
168-
)
130+
setFilteredData(
131+
transcriptData.filter(el =>
132+
el.text.toLowerCase().includes(searchTerm.toLowerCase())
133+
)
134+
)
135+
}, [transcriptData, searchTerm])
169136

170137
useEffect(() => {
171138
const handleTimeUpdate = () => {
@@ -261,23 +228,6 @@ function TranscriptItem({
261228
setCurTimeVideo(valSeconds)
262229
}
263230

264-
const formatMilliseconds = (ms: number) => {
265-
const totalSeconds = Math.floor(ms / 1000)
266-
const hours = Math.floor(totalSeconds / 3600)
267-
const minutes = Math.floor((totalSeconds % 3600) / 60)
268-
const seconds = totalSeconds % 60
269-
270-
const formattedHours = String(hours).padStart(2, "0")
271-
const formattedMinutes = String(minutes).padStart(2, "0")
272-
const formattedSeconds = String(seconds).padStart(2, "0")
273-
274-
if (hours >= 1) {
275-
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`
276-
} else {
277-
return `${formattedMinutes}:${formattedSeconds}`
278-
}
279-
}
280-
281231
const isHighlighted = (index: number): boolean => {
282232
return index === highlightedId
283233
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { firestore } from "../firebase"
2+
import { collection, getDocs, orderBy, query } from "firebase/firestore"
3+
4+
export type Paragraph = {
5+
confidence: number
6+
end: number
7+
start: number
8+
text: string
9+
}
10+
11+
export async function fetchTranscriptionData(
12+
videoTranscriptionId: string
13+
): Promise<Paragraph[]> {
14+
const subscriptionRef = collection(
15+
firestore,
16+
`transcriptions/${videoTranscriptionId}/paragraphs`
17+
)
18+
19+
let docList: any[] = []
20+
21+
const q = query(subscriptionRef, orderBy("start"))
22+
const querySnapshot = await getDocs(q)
23+
24+
querySnapshot.forEach(doc => {
25+
// doc.data() is never undefined for query doc snapshots
26+
docList.push(doc.data())
27+
})
28+
29+
return docList
30+
}
31+
32+
export function formatMilliseconds(ms: number): string {
33+
const totalSeconds = Math.floor(ms / 1000)
34+
const hours = Math.floor(totalSeconds / 3600)
35+
const minutes = Math.floor((totalSeconds % 3600) / 60)
36+
const seconds = totalSeconds % 60
37+
38+
const formattedHours = String(hours).padStart(2, "0")
39+
const formattedMinutes = String(minutes).padStart(2, "0")
40+
const formattedSeconds = String(seconds).padStart(2, "0")
41+
42+
if (hours >= 1) {
43+
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`
44+
} else {
45+
return `${formattedMinutes}:${formattedSeconds}`
46+
}
47+
}

public/locales/en/hearing.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"bills_consideration": "Bills under consideration",
44
"chairs": "Chairs",
55
"committee_members": "Committee members",
6+
"download_transcript": "Download transcript",
67
"hearing_details": "Hearing details",
78
"house_chair": "House Chair",
89
"member": "Member",

0 commit comments

Comments
 (0)