-
-
Notifications
You must be signed in to change notification settings - Fork 140
Working end-to-end transcription integrations. #1774
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
17fdca1
ca0f2bb
f16f46e
e054c49
5c4a21b
f1f947c
92fccb3
24474c6
c9d5a90
64475c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,57 +1,112 @@ | ||
import * as functions from "firebase-functions" | ||
import { AssemblyAI } from "assemblyai" | ||
import { db } from "../firebase" | ||
import { db, Timestamp } from "../firebase" | ||
import { sha256 } from "js-sha256" | ||
|
||
const assembly = new AssemblyAI({ | ||
apiKey: process.env.ASSEMBLY_API_KEY ? process.env.ASSEMBLY_API_KEY : "" | ||
}) | ||
|
||
export const transcription = functions.https.onRequest(async (req, res) => { | ||
if ( | ||
req.headers["X-Maple-Webhook"] && | ||
req.headers["webhook_auth_header_value"] | ||
) { | ||
if (req.headers["x-maple-webhook"]) { | ||
if (req.body.status === "completed") { | ||
// If we get a request with the right header and status, get the | ||
// transcription from the assembly API. | ||
const transcript = await assembly.transcripts.get(req.body.transcript_id) | ||
if (transcript && transcript.webhook_auth) { | ||
const maybeEventInDb = await db | ||
// If there is a transcript and the transcript has an auth property, | ||
// look for an event (aka Hearing) in the DB with a matching ID. | ||
const maybeEventsInDb = await db | ||
.collection("events") | ||
.where("videoAssemblyId", "==", transcript.id) | ||
.where("videoTranscriptionId", "==", transcript.id) | ||
.get() | ||
if (maybeEventInDb.docs.length) { | ||
const authenticatedEventsInDb = maybeEventInDb.docs.filter( | ||
async e => { | ||
const hashedToken = sha256( | ||
String(req.headers["webhook_auth_header_value"]) | ||
) | ||
|
||
const tokenInDb = await db | ||
.collection("events") | ||
.doc(e.id) | ||
.collection("private") | ||
.doc("webhookAuth") | ||
.get() | ||
const tokenInDbData = tokenInDb.data() | ||
if (tokenInDbData) { | ||
return hashedToken === tokenInDbData.videoAssemblyWebhookToken | ||
} | ||
return false | ||
if (maybeEventsInDb.docs.length) { | ||
// If we have a match look for one that matches a hash of the token | ||
// we gave Assembly. There should only be one of these but firestore | ||
// gives us an array. If there is more than one member, something is | ||
// wrong | ||
const authenticatedEventIds = [] as string[] | ||
const hashedToken = sha256(String(req.headers["x-maple-webhook"])) | ||
|
||
for (const index in maybeEventsInDb.docs) { | ||
const doc = maybeEventsInDb.docs[index] | ||
|
||
const tokenDocInDb = await db | ||
.collection("events") | ||
.doc(doc.id) | ||
.collection("private") | ||
.doc("webhookAuth") | ||
.get() | ||
|
||
const tokenDataInDb = tokenDocInDb.data()?.videoAssemblyWebhookToken | ||
|
||
if (hashedToken === tokenDataInDb) { | ||
authenticatedEventIds.push(doc.id) | ||
} | ||
) | ||
if (authenticatedEventsInDb) { | ||
} | ||
|
||
// Log edge cases | ||
if (maybeEventsInDb.docs.length === 0) { | ||
console.log("No matching event in db.") | ||
} | ||
if (authenticatedEventIds.length === 0) { | ||
console.log("No authenticated events in db.") | ||
} | ||
if (authenticatedEventIds.length > 1) { | ||
console.log("More than one matching event in db.") | ||
} | ||
|
||
if (authenticatedEventIds.length === 1) { | ||
// If there is one authenticated event, pull out the parts we want to | ||
// save and try to save them in the db. | ||
const { id, text, audio_url, utterances } = transcript | ||
try { | ||
await db | ||
const transcriptionInDb = await db | ||
.collection("transcriptions") | ||
.doc(transcript.id) | ||
.set({ _timestamp: new Date(), ...transcript }) | ||
.doc(id) | ||
|
||
authenticatedEventsInDb.forEach(async d => { | ||
await d.ref.update({ | ||
["webhook_auth_header_value"]: null | ||
}) | ||
await transcriptionInDb.set({ | ||
id, | ||
text, | ||
createdAt: Timestamp.now(), | ||
audio_url | ||
}) | ||
console.log("transcript saved in db") | ||
|
||
// Put each `utterance` in a separate doc in an utterances | ||
// collection. Previously had done the same for `words` but | ||
// got worried about collection size and write times since | ||
// `words` can be tens of thousands of members. | ||
if (utterances) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the expected scale of utterances (i.e. several dozen per document), I believe this is fine. Firestore does generally caution against using sequential ids because it can lead to hotspotting - but given that we'll be disabling indexes for both If this does end up causing an issue for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Come to think of it, this would have a potential problem if we ever re-transcribe the same document (e.g. with different settings) - the utterance divisions and We don't have any plans for that right now - I just want to note a caveat that we need to delete any existing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm just going to go ahead and preemptively change this to a start. If I just remove There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can test in the firebase-admin repl to be sure, but I think you might also need the more explicit method for a subcollection autogenerated id: e.g. db.collection("transcriptions").doc(`${transcript.id}`).collection("utterances").doc() There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense. Reading this line of my last commit back and realizing it wouldn't work. |
||
const writer = db.bulkWriter() | ||
for (let utterance of utterances) { | ||
const { speaker, confidence, start, end, text } = utterance | ||
|
||
writer.set( | ||
db | ||
.collection("transcriptions") | ||
.doc(`${transcript.id}`) | ||
.collection("utterances") | ||
.doc(), | ||
{ speaker, confidence, start, end, text } | ||
) | ||
} | ||
|
||
await writer.close() | ||
} | ||
|
||
// Delete the hashed webhook auth token from our db now that | ||
// we're done. | ||
for (const index in authenticatedEventIds) { | ||
await db | ||
.collection("events") | ||
.doc(authenticatedEventIds[index]) | ||
.collection("private") | ||
.doc("webhookAuth") | ||
.set({ | ||
videoAssemblyWebhookToken: null | ||
}) | ||
} | ||
} catch (error) { | ||
console.log(error) | ||
} | ||
|
Uh oh!
There was an error while loading. Please reload this page.