diff --git a/src/AudioNotesSettings.ts b/src/AudioNotesSettings.ts index 570726c..445de0d 100644 --- a/src/AudioNotesSettings.ts +++ b/src/AudioNotesSettings.ts @@ -155,17 +155,17 @@ export class AudioNotesSettingsTab extends PluginSettingTab { }) ); new Setting(containerEl) - .setName("Show Deepgram Logo") - .setDesc( - "Show the Deepgram logo on the bottom of the note. (requires restart)" - ) - .addToggle((toggle: ToggleComponent) => { - toggle.onChange(async (value: boolean) => { - this.plugin.settings.showDeepgramLogo = value; - await this.plugin.saveSettings(); - }); - toggle.setValue(this.plugin.settings.showDeepgramLogo); - }); + .setName("Deepgram Transcript Folder") + .setDesc("The folder your transcripts will be saved in when transcribing audio files.") + .addText((text) => + text + .setPlaceholder("transcripts/") + .setValue(this.plugin.settings.DGTranscriptFolder) + .onChange(async (value) => { + this.plugin.settings.DGTranscriptFolder = value; + await this.plugin.saveSettings(); + }) + ); containerEl.createEl("hr"); containerEl.createDiv( @@ -230,7 +230,7 @@ export interface StringifiedAudioNotesSettings { audioNotesApiKey: string; debugMode: boolean; DGApiKey: string; - showDeepgramLogo: boolean; + DGTranscriptFolder: string; } const DEFAULT_SETTINGS: StringifiedAudioNotesSettings = { @@ -241,7 +241,7 @@ const DEFAULT_SETTINGS: StringifiedAudioNotesSettings = { audioNotesApiKey: "", debugMode: false, DGApiKey: "", - showDeepgramLogo: true, + DGTranscriptFolder: "transcripts/", }; export class AudioNotesSettings { @@ -253,7 +253,7 @@ export class AudioNotesSettings { private _audioNotesApiKey: string, private _debugMode: boolean, private _DGApiKey: string, - private _showDeepgramLogo: boolean + private _DGTranscriptFolder: string, ) {} static fromDefaultSettings(): AudioNotesSettings { @@ -265,7 +265,7 @@ export class AudioNotesSettings { DEFAULT_SETTINGS.audioNotesApiKey, DEFAULT_SETTINGS.debugMode, DEFAULT_SETTINGS.DGApiKey, - DEFAULT_SETTINGS.showDeepgramLogo + DEFAULT_SETTINGS.DGTranscriptFolder, ); } @@ -306,12 +306,6 @@ export class AudioNotesSettings { if (data.DGApiKey !== null && data.DGApiKey !== undefined) { settings.DGApiKey = data.DGApiKey!; } - if ( - data.showDeepgramLogo !== null && - data.showDeepgramLogo !== undefined - ) { - settings.showDeepgramLogo = data.showDeepgramLogo!; - } return settings; } @@ -383,12 +377,12 @@ export class AudioNotesSettings { this._DGApiKey = value; } - get showDeepgramLogo(): boolean { - return this._showDeepgramLogo; + get DGTranscriptFolder(): string { + return this._DGTranscriptFolder; } - set showDeepgramLogo(value: boolean) { - this._showDeepgramLogo = value; + set DGTranscriptFolder(value: string) { + this._DGTranscriptFolder = value; } async getInfoByApiKey(): Promise { diff --git a/src/AudioNotesUtils.ts b/src/AudioNotesUtils.ts new file mode 100644 index 0000000..f6b7a34 --- /dev/null +++ b/src/AudioNotesUtils.ts @@ -0,0 +1,82 @@ +import { MarkdownView, Notice, TFile, type App } from "obsidian"; + +export const createNewAudioNoteFile = async (app: App, audioFilename: string, transcriptFilename: string | undefined, newNoteFilename: string, title: string) => { + if (transcriptFilename === undefined) { + transcriptFilename = audioFilename; + const testTranscriptFilename = transcriptFilename.split(".").slice(0, transcriptFilename.split(".").length - 1).join(".") + ".json"; + if (await app.vault.adapter.exists(testTranscriptFilename)) { + transcriptFilename = testTranscriptFilename; + } + } + const newNoteContents = `\`\`\`audio-note +audio: ${audioFilename} +transcript: ${transcriptFilename} +title: ${title} +\`\`\` +`; + const numberOfLines = 5; + app.vault.create(newNoteFilename, newNoteContents).then((newNote: TFile) => { + // Create the file and open it in the active leaf + const leaf = app.workspace.getLeaf(false); + leaf.openFile(newNote).then(() => { + const view = leaf.view; + if (view && view instanceof MarkdownView) { + view.editor.setCursor(numberOfLines); + } + }); + }).catch((error: any) => { + new Notice(`Could not create new audio note file: ${newNoteFilename}`); + new Notice(`${error}`); + }); +} + +export const createAudioNoteTitleFromUrl = (url: string): string => { + const urlParts = url.split("/"); + const lastPart = urlParts[urlParts.length - 1]; + let title = lastPart.split("?")[0]; + if (title.includes(".mp3")) { + title = title.replace(/.mp3/g, ""); + } else if (title.includes(".m4b")) { + title = title.replace(/.m4b/g, ""); + } else if (title.includes(".m4a")) { + title = title.replace(/.m4a/g, ""); + } + return title; +} + +export const createAudioNoteFilenameFromUrl = (url: string): string => { + const title = createAudioNoteTitleFromUrl(url); + const newNoteFilename = (title.replace(/[|&\/\\#,+()$~%'":*?<>{}]/g, "-")) + ".md"; + return newNoteFilename; +} + +export const createDeepgramQueryParams = (language: string): any => { + const DGoptions = { + language: language, + modelTier: "base", + punctuation: true, + numbers: true, + profanity: true, + keywords: "", + }; + const options = { + language: DGoptions.language, + tier: DGoptions.modelTier, + punctuate: DGoptions.punctuation, + numbers: DGoptions.numbers, + profanity_filter: DGoptions.profanity, + keywords: DGoptions.keywords + .split(",") + .map((keyword: string) => keyword.trim()), + } + let optionsWithValue = Object.keys(options).filter(function (x) { + // @ts-ignore + return options[x] !== false && options[x] !== ""; + }); + let optionsToPass = {}; + optionsWithValue.forEach((key) => { + // @ts-ignore + optionsToPass[key] = options[key]; + }); + return optionsToPass; +} diff --git a/src/CreateNewAudioNoteInNewFileModal.ts b/src/CreateNewAudioNoteInNewFileModal.ts index 34a1d8f..efc2ba0 100644 --- a/src/CreateNewAudioNoteInNewFileModal.ts +++ b/src/CreateNewAudioNoteInNewFileModal.ts @@ -1,292 +1,352 @@ -import { FuzzySuggestModal, App, TFile, Notice, MarkdownView, request } from "obsidian"; -import type { ApiKeyInfo } from "./AudioNotesSettings"; -import { createSelect, Podcast, PodcastEpisode, WHISPER_LANGUAGE_CODES } from "./utils"; - - -export class CreateNewAudioNoteInNewFileModal extends FuzzySuggestModal { - constructor(app: App, private mp3Files: TFile[], private audioNotesApiKey: string, private apiKeyInfo: Promise) { - super(app); - // this.setInstructions([{ "command": "Select mp3 file from vault or enter a URL to an mp3 file online", "purpose": "" }]); - this.setPlaceholder("or select an mp3 file from your vault using the dropdown below:") - } - - getItems(): TFile[] { - return this.mp3Files; - } - - getItemText(file: TFile): string { - return file.path; - } - - async onOpen(): Promise { - super.onOpen(); - const prompt = Array.from(this.containerEl.childNodes)[1]; - const header = createEl("h1", { text: "Create Audio Note in new file", cls: "create-new-audio-note-file-title" }) - const fuzzySelectNodes = [prompt.childNodes[0], prompt.childNodes[1]]; - - let transcriptionOptionsContainer: HTMLDivElement | undefined = undefined; - let submitTranscription: ((url: string) => void) | undefined = undefined; - let transcribeCheckbox: HTMLInputElement | undefined = undefined; - // Check if the user has an API key - const apiKeyInfo = await this.apiKeyInfo; - if (apiKeyInfo) { - transcribeCheckbox = createEl("input", { type: "checkbox" }); - transcribeCheckbox.checked = false; - - const baseOrHigher = ["BASE", "SMALL", "MEDIUM", "LARGE"]; - const smallOrHigher = ["SMALL", "MEDIUM", "LARGE"]; - const mediumOrHigher = ["MEDIUM", "LARGE"]; - const largeOrHigher = ["LARGE"]; - const selectModel = createEl("select", { - cls: "select-model-accuracy" - }); - const tiny = selectModel.createEl("option"); - tiny.value = "Tiny"; - tiny.textContent = "Tiny"; - if (baseOrHigher.includes(apiKeyInfo.tier)) { - const base = selectModel.createEl("option"); - base.value = "Base"; - base.textContent = "Base"; - if (smallOrHigher.includes(apiKeyInfo.tier)) { - const small = selectModel.createEl("option"); - small.value = "Small"; - small.textContent = "Small"; - if (mediumOrHigher.includes(apiKeyInfo.tier)) { - const medium = selectModel.createEl("option"); - medium.value = "Medium"; - medium.textContent = "Medium"; - if (largeOrHigher.includes(apiKeyInfo.tier)) { - const large = selectModel.createEl("option"); - large.value = "Large"; - large.textContent = "Large"; - } - } - } - } - - const selectLanguage = createEl("select", { - cls: "select-model-accuracy" - }); - for (const langs of WHISPER_LANGUAGE_CODES) { - const langCode = langs[0]; - const langName = langs[1]; - const option = selectLanguage.createEl("option"); - option.value = langCode; - option.textContent = langName; - } - - transcribeCheckbox.onclick = () => { - selectModel!.disabled = !(transcribeCheckbox!.checked); - selectLanguage!.disabled = !(transcribeCheckbox!.checked); - } - - submitTranscription = (url: string) => { - if (selectModel && selectModel.value && selectLanguage && selectLanguage.value && url && transcribeCheckbox!.checked) { - const splitUrl = url.split("?"); - const endsWithMp3 = splitUrl[0].endsWith(".mp3") || splitUrl[0].endsWith(".m4b") || splitUrl[0].endsWith(".m4a"); - if (endsWithMp3) { - // Make the request to enqueue the item - request({ - url: 'https://iszrj6j2vk.execute-api.us-east-1.amazonaws.com/prod/queue', - method: 'POST', - headers: { - 'x-api-key': this.audioNotesApiKey, - }, - contentType: 'application/json', - body: JSON.stringify({ - "url": url, - "model": selectModel.value.toUpperCase(), - "language": selectLanguage.value.toLowerCase(), - }) - }).then((r: any) => { - new Notice("Successfully queued .mp3 file for transcription"); - }).finally(() => { - this.close(); - }); - } else { - new Notice("Make sure your URL is an .mp3, .m4b, or .m4a file. It should end in one of those extensions (excluding everything after an optional question mark).", 10000) - } - } else { - new Notice("Please specify a .mp3 URL, an accuracy level, and a language.") - } - } - - transcriptionOptionsContainer = createDiv({ cls: "transcription-options-container-for-new-audio-note" }); - const text = createEl("span"); - text.textContent = "Submit for transcription?"; - transcriptionOptionsContainer.setChildrenInPlace([text, transcribeCheckbox!, selectModel!, selectLanguage!]); - } - - const fromUrl = async () => { - const pasteUrlContainer = createDiv({ cls: "create-new-audio-note-file-url-container" }); - const urlInputContainer = pasteUrlContainer.createDiv({ cls: "prompt-input-container create-new-audio-note-file-prompt-input-container" }); - const urlInput = urlInputContainer.createEl("input", { placeholder: `Paste a URL to an online mp3 file...`, cls: "prompt-input create-new-audio-note-file-input-element" }) - const submitUrlButton = pasteUrlContainer.createEl("button", { cls: "mod-cta create-new-audio-note-file-submit-button", text: "Create new note from URL" }); - - submitUrlButton.addEventListener('click', () => { - const url = urlInput.value; - const urlParts = url.split("/"); - const lastPart = urlParts[urlParts.length - 1]; - let title = lastPart.split("?")[0]; - if (title.includes(".mp3")) { - title = title.replace(/.mp3/g, ""); - } else if (title.includes(".m4b")) { - title = title.replace(/.m4b/g, ""); - } else if (title.includes(".m4a")) { - title = title.replace(/.m4a/g, ""); - } - const newNoteFilename = (title.replace(/[|&\/\\#,+()$~%'":*?<>{}]/g, "-")) + ".md"; - this.createNewAudioNoteFile(url, newNoteFilename, title); - if (transcribeCheckbox && transcribeCheckbox.checked && submitTranscription) { - submitTranscription(url); - } - this.close(); - }); - - // Set the content for the user to see - const nodes: Node[] = [header, tab, pasteUrlContainer]; - if (transcriptionOptionsContainer) { - nodes.push(transcriptionOptionsContainer); - } - prompt.setChildrenInPlace(nodes); - } - - const fromLocalFile = () => { - const nodes: Node[] = [header, tab, ...fuzzySelectNodes]; - prompt.setChildrenInPlace(nodes); - } - - const fromPodcast = () => { - const podcastInputDiv = createDiv({ cls: "podcast-input-div" }); - const podcastInputSpan = podcastInputDiv.createSpan({ text: "Search for a podcast:", cls: "span-podcast-input-text" }); - const podcastSearch = podcastInputDiv.createEl("input", { type: "text", cls: "podcast-search-input" }); - - const podcastSelectDiv = createDiv({ cls: "podcast-select-div" }); - const podcastSelectSpan = podcastSelectDiv.createSpan({ text: "Choose a podcast:", cls: "span-podcast-select-text" }); - let podcastResults = podcastSelectDiv.createEl("select", { cls: "select-podcast-results" }); - podcastResults.disabled = true; - - const episodeSelectDiv = createDiv({ cls: "podcast-episode-select-div" }); - const episodeSelectSpan = episodeSelectDiv.createSpan({ text: "Choose an episode:", cls: "span-podcast-episode-select-text" }); - let episodeResults = episodeSelectDiv.createEl("select", { cls: "select-podcast-episode-results" }); - episodeResults.disabled = true; - - let podcastSearchRetrievalTimer: NodeJS.Timeout | undefined = undefined; - const newGetPodcastsTimer = () => { - return setTimeout(() => { - request({ - url: 'https://iszrj6j2vk.execute-api.us-east-1.amazonaws.com/prod/podcast/search', - method: 'POST', - contentType: 'application/json', - body: JSON.stringify({ - "search": podcastSearch.value, - }) - }).then((result: string) => { - const podcasts = JSON.parse(result) as Podcast[]; - const podcastKeys = podcasts.map((p: Podcast) => `${p.name} - ${p.author}`); - const podcastValues = podcasts.map((p: Podcast) => `{"name": "${p.name}", "author": "${p.author}", "feedUrl": "${p.feedUrl}"}`); - podcastResults = createSelect(podcastKeys, podcastValues, "select-podcast-results", true); - podcastSelectDiv.setChildrenInPlace([podcastSelectSpan, podcastResults]); - - podcastResults.onchange = () => { - request({ - url: 'https://iszrj6j2vk.execute-api.us-east-1.amazonaws.com/prod/podcast/episode/search', - method: 'POST', - contentType: 'application/json', - body: JSON.stringify({ - "podcast": JSON.parse(podcastResults.value), - }) - }).then((result2: string) => { - const episodes = JSON.parse(result2); - const episodeKeys = episodes.map((e: PodcastEpisode) => e.title); - const episodeValues = episodes.map((e: PodcastEpisode) => e.url); - episodeResults = createSelect(episodeKeys, episodeValues, "select-podcast-episode-results", true); - episodeResults.onchange = () => { - submitUrlButton.disabled = false; - } - episodeSelectDiv.setChildrenInPlace([episodeSelectSpan, episodeResults]); - }); - } - }) - }, 1000); - } - podcastSearch.oninput = () => { - if (podcastSearchRetrievalTimer) { - clearTimeout(podcastSearchRetrievalTimer); - } - podcastSearchRetrievalTimer = newGetPodcastsTimer(); - } - - const submitUrlButton = createEl("button", { cls: "mod-cta create-new-audio-note-file-submit-button", text: "Create new note from Podcast" }); - submitUrlButton.disabled = true; - submitUrlButton.addEventListener('click', () => { - const url = episodeResults.value; - const title = episodeResults.options[episodeResults.selectedIndex].text; - const newNoteFilename = (title.replace(/[|&\/\\#,+()$~%'":*?<>{}]/g, "-")) + ".md"; - this.createNewAudioNoteFile(url, newNoteFilename, title); - if (transcribeCheckbox && transcribeCheckbox.checked && submitTranscription) { - submitTranscription(url); - } - this.close(); - }); - - const nodes: Node[] = [header, tab, podcastInputDiv, podcastSelectDiv, episodeSelectDiv, submitUrlButton]; - if (transcriptionOptionsContainer) { - nodes.push(transcriptionOptionsContainer); - } - prompt.setChildrenInPlace(nodes); - } - - const tab = createDiv({ cls: "tab" }); - const tab1 = tab.createEl("button", { cls: "tablinks" }); - tab1.onclick = fromLocalFile; - tab1.textContent = "Local File"; - const tab2 = tab.createEl("button", { cls: "tablinks" }); - tab2.onclick = fromUrl; - tab2.textContent = "URL"; - const tab3 = tab.createEl("button", { cls: "tablinks" }); - tab3.onclick = fromPodcast; - tab3.textContent = "Podcast Search"; - - fromLocalFile(); - } - - onChooseItem(file: TFile, evt: MouseEvent | KeyboardEvent) { - const _title = file.path.split(".").slice(0, file.path.split(".").length - 1).join("."); - const newNoteFilename = (_title.replace(/[|&\/\\#,+()$~%'":*?<>{}]/g, "-")) + ".md"; - let title = file.name; - title = file.name.slice(0, file.name.length - (file.extension.length + 1)); - title = title.replace(/-/g, " "); - title = title.replace(/_/g, " "); - title = title.split(" ").map((part: string) => part.charAt(0).toUpperCase() + part.slice(1, undefined)).join(" "); - this.createNewAudioNoteFile(file.path, newNoteFilename, title); - } - - async createNewAudioNoteFile(audioFilename: string, newNoteFilename: string, title: string) { - let transcriptFilename = audioFilename; - const testTranscriptFilename = transcriptFilename.split(".").slice(0, transcriptFilename.split(".").length - 1).join(".") + ".json"; - if (await this.app.vault.adapter.exists(testTranscriptFilename)) { - transcriptFilename = testTranscriptFilename; - } - const newNoteContents = `\`\`\`audio-note -audio: ${audioFilename} -transcript: ${transcriptFilename} -title: ${title} -\`\`\` -`; - const numberOfLines = 5; - this.app.vault.create(newNoteFilename, newNoteContents).then((newNote: TFile) => { - // Create the file and open it in the active leaf - const leaf = this.app.workspace.getLeaf(false); - leaf.openFile(newNote).then(() => { - const view = leaf.view; - if (view && view instanceof MarkdownView) { - view.editor.setCursor(numberOfLines); - } - }); - }).catch((error: any) => { - new Notice(`Could not create new audio note file: ${newNoteFilename}`); - new Notice(`${error}`); - }); - } -} +import { FuzzySuggestModal, App, TFile, Notice, MarkdownView, request } from "obsidian"; +import queryString from "query-string"; + +import type { ApiKeyInfo } from "./AudioNotesSettings"; +import { createAudioNoteFilenameFromUrl, createAudioNoteTitleFromUrl, createDeepgramQueryParams, createNewAudioNoteFile } from "./AudioNotesUtils"; +import type { DeepgramTranscriptionResponse } from "./Deepgram"; +import { getTranscriptFromDGResponse } from "./Transcript"; +import { createSelect, DG_LANGUAGE_CODES, Podcast, PodcastEpisode, WHISPER_LANGUAGE_CODES } from "./utils"; + + +export class CreateNewAudioNoteInNewFileModal extends FuzzySuggestModal { + constructor(app: App, private mp3Files: TFile[], private audioNotesApiKey: string, private apiKeyInfo: Promise, private DGApiKey: string) { + super(app); + // this.setInstructions([{ "command": "Select mp3 file from vault or enter a URL to an mp3 file online", "purpose": "" }]); + this.setPlaceholder("or select an mp3 file from your vault using the dropdown below:") + } + + getItems(): TFile[] { + return this.mp3Files; + } + + getItemText(file: TFile): string { + return file.path; + } + + async onOpen(): Promise { + super.onOpen(); + const prompt = Array.from(this.containerEl.childNodes)[1]; + const header = createEl("h1", { text: "Create Audio Note in new file", cls: "create-new-audio-note-file-title" }) + const fuzzySelectNodes = [prompt.childNodes[0], prompt.childNodes[1]]; + + let transcriptionOptionsContainer: HTMLDivElement | undefined = undefined; + let submitTranscription: ((url: string) => void) | undefined = undefined; + let transcribeCheckbox: HTMLInputElement | undefined = undefined; + let createFileOnSubmit = true; + // Check if the user has an API key + const apiKeyInfo = await this.apiKeyInfo; + if (apiKeyInfo) { + transcribeCheckbox = createEl("input", { type: "checkbox" }); + transcribeCheckbox.checked = false; + + const baseOrHigher = ["BASE", "SMALL", "MEDIUM", "LARGE"]; + const smallOrHigher = ["SMALL", "MEDIUM", "LARGE"]; + const mediumOrHigher = ["MEDIUM", "LARGE"]; + const largeOrHigher = ["LARGE"]; + const selectModel = createEl("select", { + cls: "select-model-accuracy" + }); + const tiny = selectModel.createEl("option"); + tiny.value = "Tiny"; + tiny.textContent = "Tiny"; + if (baseOrHigher.includes(apiKeyInfo.tier)) { + const base = selectModel.createEl("option"); + base.value = "Base"; + base.textContent = "Base"; + if (smallOrHigher.includes(apiKeyInfo.tier)) { + const small = selectModel.createEl("option"); + small.value = "Small"; + small.textContent = "Small"; + if (mediumOrHigher.includes(apiKeyInfo.tier)) { + const medium = selectModel.createEl("option"); + medium.value = "Medium"; + medium.textContent = "Medium"; + if (largeOrHigher.includes(apiKeyInfo.tier)) { + const large = selectModel.createEl("option"); + large.value = "Large"; + large.textContent = "Large"; + } + } + } + } + + const selectLanguage = createEl("select", { + cls: "select-model-accuracy" + }); + for (const langs of WHISPER_LANGUAGE_CODES) { + const langCode = langs[0]; + const langName = langs[1]; + const option = selectLanguage.createEl("option"); + option.value = langCode; + option.textContent = langName; + } + + transcribeCheckbox.onclick = () => { + selectModel!.disabled = !(transcribeCheckbox!.checked); + selectLanguage!.disabled = !(transcribeCheckbox!.checked); + } + + submitTranscription = (url: string) => { + if (selectModel && selectModel.value && selectLanguage && selectLanguage.value && url && transcribeCheckbox!.checked) { + const splitUrl = url.split("?"); + const endsWithMp3 = splitUrl[0].endsWith(".mp3") || splitUrl[0].endsWith(".m4b") || splitUrl[0].endsWith(".m4a"); + if (endsWithMp3) { + // Make the request to enqueue the item + request({ + url: 'https://iszrj6j2vk.execute-api.us-east-1.amazonaws.com/prod/queue', + method: 'POST', + headers: { + 'x-api-key': this.audioNotesApiKey, + }, + contentType: 'application/json', + body: JSON.stringify({ + "url": url, + "model": selectModel.value.toUpperCase(), + "language": selectLanguage.value.toLowerCase(), + }) + }).then((r: any) => { + new Notice("Successfully queued .mp3 file for transcription"); + }).finally(() => { + this.close(); + }); + } else { + new Notice("Make sure your URL is an .mp3, .m4b, or .m4a file. It should end in one of those extensions (excluding everything after an optional question mark).", 10000) + } + } else { + new Notice("Please specify a .mp3 URL, an accuracy level, and a language.") + } + } + + transcriptionOptionsContainer = createDiv({ cls: "transcription-options-container-for-new-audio-note" }); + const text = createEl("span"); + text.textContent = "Submit for transcription?"; + transcriptionOptionsContainer.setChildrenInPlace([text, transcribeCheckbox!, selectModel!, selectLanguage!]); + } else if (this.DGApiKey) { + submitTranscription = (url: string) => { + if (selectLanguage && selectLanguage.value && url && transcribeCheckbox!.checked) { + const splitUrl = url.split("?"); + const endsWithMp3 = splitUrl[0].endsWith(".mp3") || splitUrl[0].endsWith(".m4b") || splitUrl[0].endsWith(".m4a"); + if (endsWithMp3) { + // Make the request to enqueue the item + const queryParams = createDeepgramQueryParams(selectLanguage.value); + new Notice(`Transcribing audio using Deepgram...`); + const req = { + url: `https://api.deepgram.com/v1/listen?${queryString.stringify(queryParams)}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + "User-Agent": "Deepgram Obsidian Audio Notes Plugin", + Authorization: `token ${this.DGApiKey}`, + }, + contentType: 'application/json', + body: JSON.stringify({ + url: url + }) + }; + request(req).then(async (dgResponseString: string) => { + const dgResponse: DeepgramTranscriptionResponse = JSON.parse(dgResponseString); + const folder = "transcripts"; + try { + await app.vault.createFolder(folder); + } catch (err) { + console.info("Audio Notes: Folder exists. Skipping creation."); + } + // Create the file that contains the transcript. + const newNoteFilename = createAudioNoteFilenameFromUrl(url); + const transcriptFilename = `${folder}/${newNoteFilename}`.replace(/.md/, ".json"); + const transcriptFileExists = await app.vault.adapter.exists(transcriptFilename); + if (!transcriptFileExists) { // only send the request if the file doesn't exist + const transcript = getTranscriptFromDGResponse(dgResponse); + const transcriptFile = await app.vault.create( + transcriptFilename, + `"{"segments": ${transcript.toJSON()}}`, + ); + new Notice(`${newNoteFilename} saved!`); + } else { + new Notice(`${transcriptFilename} already exists! Did not re-submit for transcription.`) + } + // Create the file with the actual Audio Note. + const title = createAudioNoteTitleFromUrl(url); + createNewAudioNoteFile(app, url, transcriptFilename, newNoteFilename, title); + // await navigator.clipboard.writeText(`![[${newNoteFilename}]]`); + }).catch((error) => { + console.error("Could not transcribe audio:") + console.error(error); + }).finally(() => { + this.close(); + }); + } else { + new Notice("Make sure your URL is an .mp3, .m4b, or .m4a file. It should end in one of those extensions (excluding everything after an optional question mark).", 10000) + } + } else { + new Notice("Please specify a .mp3 URL, an accuracy level, and a language.") + } + } + + transcribeCheckbox = createEl("input", { type: "checkbox" }); + transcribeCheckbox.checked = false; + + const selectLanguage = createEl("select", { + cls: "select-model-accuracy" + }); + for (const langs of DG_LANGUAGE_CODES) { + const langCode = langs[0]; + const langName = langs[1]; + const option = selectLanguage.createEl("option"); + option.value = langCode; + option.textContent = langName; + } + + transcribeCheckbox.onclick = () => { + selectLanguage!.disabled = !(transcribeCheckbox!.checked); + createFileOnSubmit = !(transcribeCheckbox!.checked); + } + + transcriptionOptionsContainer = createDiv({ cls: "transcription-options-container-for-new-audio-note" }); + const text = createEl("span"); + if (this.DGApiKey) { + text.textContent = "Submit for transcription using Deepgram?"; + } else { + text.textContent = "Submit for transcription using Audio Note API key?"; + } + transcriptionOptionsContainer.setChildrenInPlace([text, transcribeCheckbox!, selectLanguage!]); + } + + const fromUrl = async () => { + const pasteUrlContainer = createDiv({ cls: "create-new-audio-note-file-url-container" }); + const urlInputContainer = pasteUrlContainer.createDiv({ cls: "prompt-input-container create-new-audio-note-file-prompt-input-container" }); + const urlInput = urlInputContainer.createEl("input", { placeholder: `Paste a URL to an online mp3 file...`, cls: "prompt-input create-new-audio-note-file-input-element" }) + const submitUrlButton = pasteUrlContainer.createEl("button", { cls: "mod-cta create-new-audio-note-file-submit-button", text: "Create new note from URL" }); + + submitUrlButton.addEventListener('click', () => { + const url = urlInput.value; + if (createFileOnSubmit) { + const title = createAudioNoteTitleFromUrl(url); + const newNoteFilename = createAudioNoteFilenameFromUrl(url); + createNewAudioNoteFile(app, url, undefined, newNoteFilename, title); + } + if (transcribeCheckbox && transcribeCheckbox.checked && submitTranscription) { + submitTranscription(url); + } + this.close(); + }); + + // Set the content for the user to see + const nodes: Node[] = [header, tab, pasteUrlContainer]; + if (transcriptionOptionsContainer) { + nodes.push(transcriptionOptionsContainer); + } + prompt.setChildrenInPlace(nodes); + } + + const fromLocalFile = () => { + const nodes: Node[] = [header, tab, ...fuzzySelectNodes]; + prompt.setChildrenInPlace(nodes); + } + + const fromPodcast = () => { + const podcastInputDiv = createDiv({ cls: "podcast-input-div" }); + const podcastInputSpan = podcastInputDiv.createSpan({ text: "Search for a podcast:", cls: "span-podcast-input-text" }); + const podcastSearch = podcastInputDiv.createEl("input", { type: "text", cls: "podcast-search-input" }); + + const podcastSelectDiv = createDiv({ cls: "podcast-select-div" }); + const podcastSelectSpan = podcastSelectDiv.createSpan({ text: "Choose a podcast:", cls: "span-podcast-select-text" }); + let podcastResults = podcastSelectDiv.createEl("select", { cls: "select-podcast-results" }); + podcastResults.disabled = true; + + const episodeSelectDiv = createDiv({ cls: "podcast-episode-select-div" }); + const episodeSelectSpan = episodeSelectDiv.createSpan({ text: "Choose an episode:", cls: "span-podcast-episode-select-text" }); + let episodeResults = episodeSelectDiv.createEl("select", { cls: "select-podcast-episode-results" }); + episodeResults.disabled = true; + + let podcastSearchRetrievalTimer: NodeJS.Timeout | undefined = undefined; + const newGetPodcastsTimer = () => { + return setTimeout(() => { + request({ + url: 'https://iszrj6j2vk.execute-api.us-east-1.amazonaws.com/prod/podcast/search', + method: 'POST', + contentType: 'application/json', + body: JSON.stringify({ + "search": podcastSearch.value, + }) + }).then((result: string) => { + const podcasts = JSON.parse(result) as Podcast[]; + const podcastKeys = podcasts.map((p: Podcast) => `${p.name} - ${p.author}`); + const podcastValues = podcasts.map((p: Podcast) => `{"name": "${p.name}", "author": "${p.author}", "feedUrl": "${p.feedUrl}"}`); + podcastResults = createSelect(podcastKeys, podcastValues, "select-podcast-results", true); + podcastSelectDiv.setChildrenInPlace([podcastSelectSpan, podcastResults]); + + podcastResults.onchange = () => { + request({ + url: 'https://iszrj6j2vk.execute-api.us-east-1.amazonaws.com/prod/podcast/episode/search', + method: 'POST', + contentType: 'application/json', + body: JSON.stringify({ + "podcast": JSON.parse(podcastResults.value), + }) + }).then((result2: string) => { + const episodes = JSON.parse(result2); + const episodeKeys = episodes.map((e: PodcastEpisode) => e.title); + const episodeValues = episodes.map((e: PodcastEpisode) => e.url); + episodeResults = createSelect(episodeKeys, episodeValues, "select-podcast-episode-results", true); + episodeResults.onchange = () => { + submitUrlButton.disabled = false; + } + episodeSelectDiv.setChildrenInPlace([episodeSelectSpan, episodeResults]); + }); + } + }) + }, 1000); + } + podcastSearch.oninput = () => { + if (podcastSearchRetrievalTimer) { + clearTimeout(podcastSearchRetrievalTimer); + } + podcastSearchRetrievalTimer = newGetPodcastsTimer(); + } + + const submitUrlButton = createEl("button", { cls: "mod-cta create-new-audio-note-file-submit-button", text: "Create new note from Podcast" }); + submitUrlButton.disabled = true; + submitUrlButton.addEventListener('click', () => { + const url = episodeResults.value; + const title = episodeResults.options[episodeResults.selectedIndex].text; + const newNoteFilename = (title.replace(/[|&\/\\#,+()$~%'":*?<>{}]/g, "-")) + ".md"; + createNewAudioNoteFile(app, url, undefined, newNoteFilename, title); + if (transcribeCheckbox && transcribeCheckbox.checked && submitTranscription) { + submitTranscription(url); + } + this.close(); + }); + + const nodes: Node[] = [header, tab, podcastInputDiv, podcastSelectDiv, episodeSelectDiv, submitUrlButton]; + if (transcriptionOptionsContainer) { + nodes.push(transcriptionOptionsContainer); + } + prompt.setChildrenInPlace(nodes); + } + + const tab = createDiv({ cls: "tab" }); + const tab1 = tab.createEl("button", { cls: "tablinks" }); + tab1.onclick = fromLocalFile; + tab1.textContent = "Local File"; + const tab2 = tab.createEl("button", { cls: "tablinks" }); + tab2.onclick = fromUrl; + tab2.textContent = "URL"; + const tab3 = tab.createEl("button", { cls: "tablinks" }); + tab3.onclick = fromPodcast; + tab3.textContent = "Podcast Search"; + + fromLocalFile(); + } + + onChooseItem(file: TFile, evt: MouseEvent | KeyboardEvent) { + const _title = file.path.split(".").slice(0, file.path.split(".").length - 1).join("."); + const newNoteFilename = (_title.replace(/[|&\/\\#,+()$~%'":*?<>{}]/g, "-")) + ".md"; + let title = file.name; + title = file.name.slice(0, file.name.length - (file.extension.length + 1)); + title = title.replace(/-/g, " "); + title = title.replace(/_/g, " "); + title = title.split(" ").map((part: string) => part.charAt(0).toUpperCase() + part.slice(1, undefined)).join(" "); + createNewAudioNoteFile(app, file.path, undefined, newNoteFilename, title); + } +} diff --git a/src/DGQuickAudioNote.svelte b/src/DGQuickAudioNote.svelte index 7da954b..233c357 100644 --- a/src/DGQuickAudioNote.svelte +++ b/src/DGQuickAudioNote.svelte @@ -106,7 +106,7 @@ blobdata ); await navigator.clipboard.writeText(`![[${recordingFilename}]]`); - new Notice(`${recordingFilename} saved ! Link copied to clipboard`); + new Notice(`${recordingFilename} saved! Link copied to clipboard`); modal.close(); const mdString = makeTranscriptBlock( transcript || "", diff --git a/src/Deepgram.ts b/src/Deepgram.ts new file mode 100644 index 0000000..4cfbbae --- /dev/null +++ b/src/Deepgram.ts @@ -0,0 +1,50 @@ +export interface DeepgramTranscriptionResponse { + metadata: { + transaction_key: string, + request_id: string, + sha256: string, + created: string, + duration: number, + channels: number, + models: [ + string + ], + }, + results: { + channels: [ + { + search: [ + { + query: string, + hits: [ + { + confidence: number, + start: number, + end: number, + snippet: string + } + ] + } + ], + alternatives: [ + DeepgramAlternative + ] + } + ] + } +} + + +export interface DeepgramAlternative { + transcript: string, + confidence: number, + words: [ + { + word: string, + start: number, + end: number, + confidence: number, + punctuated_word: string + } + ] +} \ No newline at end of file diff --git a/src/EnqueueAudioModal.ts b/src/EnqueueAudioModal.ts index 2f4c8b2..97938b4 100644 --- a/src/EnqueueAudioModal.ts +++ b/src/EnqueueAudioModal.ts @@ -1,117 +1,206 @@ -import { Modal, App, Setting, Notice, request } from "obsidian"; -import type { ApiKeyInfo } from "./AudioNotesSettings"; -import { WHISPER_LANGUAGE_CODES } from "./utils"; - - -export class EnqueueAudioModal extends Modal { - url: string; - - constructor(app: App, private audioNotesApiKey: string, private apiKeyInfo: Promise) { - super(app); - } - - onOpen() { - const { contentEl } = this; - - contentEl.createEl("h1", { text: "Add an mp3 file to transcribe" }); - - this.apiKeyInfo.then((apiKeyInfo) => { - if (apiKeyInfo) { - new Setting(contentEl) - .setName("URL to .mp3 file:") - .setDesc("The .mp3 must be publicly available, so it cannot require a login or other authentication to access. The .mp3 file cannot be on your computer, it must be online.") - .addText((text) => - text.onChange((value) => { - this.url = value - })); - - const baseOrHigher = ["BASE", "SMALL", "MEDIUM", "LARGE"]; - const smallOrHigher = ["SMALL", "MEDIUM", "LARGE"]; - const mediumOrHigher = ["MEDIUM", "LARGE"]; - const largeOrHigher = ["LARGE"]; - const select = contentEl.createEl("select", { - cls: "select-model-accuracy" - }); - const tiny = select.createEl("option"); - tiny.value = "Tiny"; - tiny.textContent = "Tiny"; - if (baseOrHigher.includes(apiKeyInfo.tier)) { - const base = select.createEl("option"); - base.value = "Base"; - base.textContent = "Base"; - if (smallOrHigher.includes(apiKeyInfo.tier)) { - const small = select.createEl("option"); - small.value = "Small"; - small.textContent = "Small"; - if (mediumOrHigher.includes(apiKeyInfo.tier)) { - const medium = select.createEl("option"); - medium.value = "Medium"; - medium.textContent = "Medium"; - if (largeOrHigher.includes(apiKeyInfo.tier)) { - const large = select.createEl("option"); - large.value = "Large"; - large.textContent = "Large"; - } - } - } - } - - const selectLanguage = contentEl.createEl("select", { - cls: "select-model-accuracy" - }); - for (const langs of WHISPER_LANGUAGE_CODES) { - const langCode = langs[0]; - const langName = langs[1]; - const option = selectLanguage.createEl("option"); - option.value = langCode; - option.textContent = langName; - } - - new Setting(contentEl) - .addButton((btn) => - btn - .setButtonText("Add to Queue") - .setCta() - .onClick(() => { - if (select.value && this.url) { - const splitUrl = this.url.split("?"); - const endsWithMp3 = splitUrl[0].endsWith(".mp3") || splitUrl[0].endsWith(".m4b") || splitUrl[0].endsWith(".m4a"); - if (endsWithMp3) { - // Make the request to enqueue the item - request({ - url: 'https://iszrj6j2vk.execute-api.us-east-1.amazonaws.com/prod/queue', - method: 'POST', - headers: { - 'x-api-key': this.audioNotesApiKey, - }, - contentType: 'application/json', - body: JSON.stringify({ - "url": this.url, - "model": select.value.toUpperCase(), - "language": selectLanguage.value.toLowerCase(), - }) - }).then((r: any) => { - new Notice("Successfully queued .mp3 file for transcription"); - }).finally(() => { - this.close(); - }); - } else { - new Notice("Make sure your URL is an .mp3, .m4b, or .m4a file. It should end in one of those extensions (excluding everything after an optional question mark).", 10000) - } - } else { - new Notice("Please specify a .mp3 URL, an accuracy level, and a language.") - } - }) - ); - } else { - contentEl.createEl("p", { text: "Please set a valid Audio Notes API key in the settings." }); - contentEl.createEl("p", { text: "If you do not have an API key, contact the maintainer of this plugin. See the README at https://github.com/jjmaldonis/obsidian-audio-notes for more information." }); - } - }); - } - - onClose() { - let { contentEl } = this; - contentEl.empty(); - } -} +import { Modal, App, Setting, Notice, request } from "obsidian"; +import queryString from "query-string"; + +import type { ApiKeyInfo } from "./AudioNotesSettings"; +import { createAudioNoteFilenameFromUrl, createDeepgramQueryParams } from "./AudioNotesUtils"; +import type { DeepgramTranscriptionResponse } from "./Deepgram"; +import { getTranscriptFromDGResponse } from "./Transcript"; +import { WHISPER_LANGUAGE_CODES, DG_LANGUAGE_CODES } from "./utils"; + + +export class EnqueueAudioModal extends Modal { + url: string; + + constructor(app: App, private audioNotesApiKey: string, private apiKeyInfo: Promise, private DGApiKey: string) { + super(app); + } + + onOpen() { + const { contentEl } = this; + + contentEl.createEl("h1", { text: "Add an mp3 file to transcribe" }); + + this.apiKeyInfo.then((apiKeyInfo) => { + if (apiKeyInfo) { + new Setting(contentEl) + .setName("URL to .mp3 file:") + .setDesc("The .mp3 must be publicly available, so it cannot require a login or other authentication to access. The .mp3 file cannot be on your computer, it must be online.") + .addText((text) => + text.onChange((value) => { + this.url = value + })); + + const baseOrHigher = ["BASE", "SMALL", "MEDIUM", "LARGE"]; + const smallOrHigher = ["SMALL", "MEDIUM", "LARGE"]; + const mediumOrHigher = ["MEDIUM", "LARGE"]; + const largeOrHigher = ["LARGE"]; + const select = contentEl.createEl("select", { + cls: "select-model-accuracy" + }); + const tiny = select.createEl("option"); + tiny.value = "Tiny"; + tiny.textContent = "Tiny"; + if (baseOrHigher.includes(apiKeyInfo.tier)) { + const base = select.createEl("option"); + base.value = "Base"; + base.textContent = "Base"; + if (smallOrHigher.includes(apiKeyInfo.tier)) { + const small = select.createEl("option"); + small.value = "Small"; + small.textContent = "Small"; + if (mediumOrHigher.includes(apiKeyInfo.tier)) { + const medium = select.createEl("option"); + medium.value = "Medium"; + medium.textContent = "Medium"; + if (largeOrHigher.includes(apiKeyInfo.tier)) { + const large = select.createEl("option"); + large.value = "Large"; + large.textContent = "Large"; + } + } + } + } + + const selectLanguage = contentEl.createEl("select", { + cls: "select-model-accuracy" + }); + for (const langs of WHISPER_LANGUAGE_CODES) { + const langCode = langs[0]; + const langName = langs[1]; + const option = selectLanguage.createEl("option"); + option.value = langCode; + option.textContent = langName; + } + + new Setting(contentEl) + .addButton((btn) => + btn + .setButtonText("Add to Queue") + .setCta() + .onClick(() => { + if (select.value && this.url) { + const splitUrl = this.url.split("?"); + const endsWithMp3 = splitUrl[0].endsWith(".mp3") || splitUrl[0].endsWith(".m4b") || splitUrl[0].endsWith(".m4a"); + if (endsWithMp3) { + // Make the request to enqueue the item + request({ + url: 'https://iszrj6j2vk.execute-api.us-east-1.amazonaws.com/prod/queue', + method: 'POST', + headers: { + 'x-api-key': this.audioNotesApiKey, + }, + contentType: 'application/json', + body: JSON.stringify({ + "url": this.url, + "model": select.value.toUpperCase(), + "language": selectLanguage.value.toLowerCase(), + }) + }).then((r: any) => { + new Notice("Successfully queued .mp3 file for transcription"); + }).finally(() => { + this.close(); + }); + } else { + new Notice("Make sure your URL is an .mp3, .m4b, or .m4a file. It should end in one of those extensions (excluding everything after an optional question mark).", 10000) + } + } else { + new Notice("Please specify a .mp3 URL, an accuracy level, and a language.") + } + }) + ); + } else if (this.DGApiKey) { + new Setting(contentEl) + .setName("URL to .mp3 file:") + .setDesc("The .mp3 must be publicly available, so it cannot require a login or other authentication to access. The .mp3 file cannot be on your computer, it must be online.") + .addText((text) => + text.onChange((value) => { + this.url = value + })); + + const selectLanguage = contentEl.createEl("select", { + cls: "select-model-accuracy" + }); + for (const langs of DG_LANGUAGE_CODES) { + const langCode = langs[0]; + const langName = langs[1]; + const option = selectLanguage.createEl("option"); + option.value = langCode; + option.textContent = langName; + } + + new Setting(contentEl) + .addButton((btn) => + btn + .setButtonText("Transcribe using Deepgram") + .setCta() + .onClick(() => { + if (selectLanguage.value && this.url) { + const splitUrl = this.url.split("?"); + const endsWithMp3 = splitUrl[0].endsWith(".mp3") || splitUrl[0].endsWith(".m4b") || splitUrl[0].endsWith(".m4a"); + if (endsWithMp3) { + // Make the request to enqueue the item + const queryParams = createDeepgramQueryParams(selectLanguage.value); + new Notice(`Transcribing audio using Deepgram...`); + const req = { + url: `https://api.deepgram.com/v1/listen?${queryString.stringify(queryParams)}`, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + "User-Agent": "Deepgram Obsidian Audio Notes Plugin", + Authorization: `token ${this.DGApiKey}`, + }, + contentType: 'application/json', + body: JSON.stringify({ + url: this.url + }) + }; + request(req).then(async (dgResponseString: string) => { + const dgResponse: DeepgramTranscriptionResponse = JSON.parse(dgResponseString); + const folder = "transcripts"; + try { + await app.vault.createFolder(folder); + } catch (err) { + console.info("Audio Notes: Folder exists. Skipping creation."); + } + // Create the file that contains the transcript. + const newNoteFilename = createAudioNoteFilenameFromUrl(this.url); + const transcriptFilename = `${folder}/${newNoteFilename}`.replace(/.md/, ".json"); + const transcriptFileExists = await app.vault.adapter.exists(transcriptFilename); + if (!transcriptFileExists) { // only send the request if the file doesn't exist + const transcript = getTranscriptFromDGResponse(dgResponse); + const transcriptFile = await app.vault.create( + transcriptFilename, + `"{"segments": ${transcript.toJSON()}}`, + ); + new Notice(`${newNoteFilename} saved!`); + } else { + new Notice(`${transcriptFilename} already exists! Did not re-submit for transcription.`) + } + await navigator.clipboard.writeText(transcriptFilename); + new Notice(`Transcript filename copied to clipboard`); + }).catch((error) => { + console.error("Could not transcribe audio:") + console.error(error); + }).finally(() => { + this.close(); + }); + } else { + new Notice("Make sure your URL is an .mp3, .m4b, or .m4a file. It should end in one of those extensions (excluding everything after an optional question mark).", 10000) + } + } else { + new Notice("Please specify a .mp3 URL, an accuracy level, and a language.") + } + }) + ); + } else { + contentEl.createEl("p", { text: "Please set a valid Audio Notes API key in the settings." }); + contentEl.createEl("p", { text: "If you do not have an API key, contact the maintainer of this plugin. See the README at https://github.com/jjmaldonis/obsidian-audio-notes for more information." }); + } + }); + } + + onClose() { + let { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/Transcript.ts b/src/Transcript.ts index 665f262..d6d4050 100644 --- a/src/Transcript.ts +++ b/src/Transcript.ts @@ -1,333 +1,373 @@ -import { Notice, Plugin, request } from "obsidian"; -import { XMLParser } from 'fast-xml-parser'; -import type { AudioNotesSettings } from "./AudioNotesSettings"; - - -export class Transcript { - constructor( - public segments: TranscriptSegment[], - ) { } - - public getQuote(quoteStart: number, quoteEnd: number): [number, number, string] { - // Get the relevant part of the transcript. - const segments = this.segments; - const result = []; - let start = undefined; - let end = undefined; - for (let segment of segments) { - const text = segment.text; - const segmentStart = segment.start; - const segmentEnd = segment.end; - // If either the segment's start or end is inside the range specified by the user... - if ((quoteStart <= segmentStart && segmentStart < quoteEnd) || (quoteStart < segmentEnd && segmentEnd <= quoteEnd)) { - result.push(text); - if (start === undefined) { - start = segmentStart; - } - end = segmentEnd; - } - // If the range specified by the user is entirely within the segment... - if (quoteStart >= segmentStart && quoteEnd <= segmentEnd) { - result.push(text); - if (start === undefined) { - start = segmentStart; - } - end = segmentEnd; - } - } - let quoteText = result.join(" ").trim(); - if (quoteText) { - // For some reason double spaces are often in the text. Remove them because they get removed by the HTML rendering anyway. - let i = 0; - while (quoteText.includes(" ")) { - quoteText = quoteText.replace(new RegExp(" "), " "); - // Make sure we don't hit an infinite loop, even though it should be impossible. - i += 1; - if (i > 100) { - break; - } - } - } - if (start === undefined || end === undefined) { - new Notice("Transcript file does not have start or end times for at least one text entry."); - console.error(segments); - throw new Error("Transcript file does not have start or end times for at least one text entry."); - } - return [start, end, quoteText]; - } - - public getSegmentAt(time: number): [number | undefined, TranscriptSegment | undefined] { - for (let i = 0; i < this.segments.length; i++) { - const segment = this.segments[i]; - if (segment.start <= time && time < segment.end) { - return [i, segment]; - } - } - return [undefined, undefined]; // if not found - } -} - - -export class TranscriptSegment { - constructor( - public id: number | string, // we don't use this. if we do, probably make it a number. - public start: number, // time in seconds - public end: number, // time in seconds - public text: string, - ) { } -} - - -export function parseTranscript(contents: string): Transcript { - // We don't always have a filename (e.g. if the transcript was pulled from online). Assume JSON, and fallback to SRT. - try { - return new Transcript(JSON.parse(contents).segments); - } catch { - return new SrtParser().fromSrt(contents); - } -} - - -export async function getYouTubeTranscript(url: string): Promise { - const html: string = await request({ - url: url, - method: 'GET' - }); - - function unescapeText(s: string): string { - var re = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g; - var unescaped: Map = new Map([ - ['&', '&'], - ['&', '&'], - ['<', '<'], - ['<', '<'], - ['>', '>'], - ['>', '>'], - [''', "'"], - [''', "'"], - ['"', '"'], - ['"', '"'], - ]); - return s.replace(re, function (m: string): string { - return unescaped.get(m) || m; - }); - } - - const captionsMetadata = JSON.parse(html.split(`"captions":`)[1].split(`,"videoDetails`)[0]).playerCaptionsTracklistRenderer.captionTracks; - for (const capmeta of captionsMetadata) { - if (capmeta.languageCode === 'en') { - const xml: string = await request({ - url: capmeta.baseUrl, - method: 'GET' - }); - const xmlParser = new XMLParser({ ignoreAttributes: false }); - const parsed = xmlParser.parse(xml).transcript.text; - let id = 0; - const segments: TranscriptSegment[] = []; - for (const line of parsed) { - const start = parseFloat(line['@_start']); - const duration = parseFloat(line['@_dur']); - let text = unescapeText(line['#text']); - text = text.replace("\n", " "); - text = text.toLowerCase(); - const sentenceCaseRegex = /(^\w{1}|\.\s*\w{1})/gi; - text = text.replace(sentenceCaseRegex, function (toReplace: string): string { return toReplace.toUpperCase(); }); - const end = start + duration; - const segment = new TranscriptSegment(id, start, end, text); - segments.push(segment); - id = id + 1; - } - const transcript = new Transcript(segments); - return transcript; - } - } - return undefined; -} - - -class SrtParser { - seperator = ","; - - timestampToSeconds(srtTimestamp: string) { - const [rest, millisecondsString] = srtTimestamp.split(","); - const milliseconds = parseInt(millisecondsString); - const [hours, minutes, seconds] = rest.split(":").map((x) => parseInt(x)); - const result = milliseconds * 0.001 + seconds + 60 * minutes + 3600 * hours; - - // fix odd JS roundings, e.g. timestamp '00:01:20,460' result is 80.46000000000001 - return Math.round(result * 1000) / 1000; - }; - - correctFormat(time: string) { - // Fix the format if the format is wrong - // 00:00:28.9670 Become 00:00:28,967 - // 00:00:28.967 Become 00:00:28,967 - // 00:00:28.96 Become 00:00:28,960 - // 00:00:28.9 Become 00:00:28,900 - - // 00:00:28,96 Become 00:00:28,960 - // 00:00:28,9 Become 00:00:28,900 - // 00:00:28,0 Become 00:00:28,000 - // 00:00:28,01 Become 00:00:28,010 - // 0:00:10,500 Become 00:00:10,500 - let str = time.replace(".", ","); - - var hour = null; - var minute = null; - var second = null; - var millisecond = null; - - // Handle millisecond - var [front, ms] = str.split(","); - millisecond = this.fixed_str_digit(3, ms); - - // Handle hour - var [a_hour, a_minute, a_second] = front.split(":"); - hour = this.fixed_str_digit(2, a_hour, false); - minute = this.fixed_str_digit(2, a_minute, false); - second = this.fixed_str_digit(2, a_second, false); - - return `${hour}:${minute}:${second},${millisecond}`; - } - - /* - // make sure string is 'how_many_digit' long - // if str is shorter than how_many_digit, pad with 0 - // if str is longer than how_many_digit, slice from the beginning - // Example: - Input: fixed_str_digit(3, '100') - Output: 100 - Explain: unchanged, because "100" is 3 digit - Input: fixed_str_digit(3, '50') - Output: 500 - Explain: pad end with 0 - Input: fixed_str_digit(3, '50', false) - Output: 050 - Explain: pad start with 0 - Input: fixed_str_digit(3, '7771') - Output: 777 - Explain: slice from beginning - */ - private fixed_str_digit( - how_many_digit: number, - str: string, - padEnd: boolean = true - ) { - if (str.length == how_many_digit) { - return str; - } - if (str.length > how_many_digit) { - return str.slice(0, how_many_digit); - } - if (str.length < how_many_digit) { - if (padEnd) { - return str.padEnd(how_many_digit, "0"); - } else { - return str.padStart(how_many_digit, "0"); - } - } - } - - private tryComma(data: string) { - data = data.replace(/\r/g, ""); - var regex = - /(\d+)\n(\d{1,2}:\d{2}:\d{2},\d{1,3}) --> (\d{1,2}:\d{2}:\d{2},\d{1,3})/g; - let data_array = data.split(regex); - data_array.shift(); // remove first '' in array - return data_array; - } - - private tryDot(data: string) { - data = data.replace(/\r/g, ""); - var regex = - /(\d+)\n(\d{1,2}:\d{2}:\d{2}\.\d{1,3}) --> (\d{1,2}:\d{2}:\d{2}\.\d{1,3})/g; - let data_array = data.split(regex); - data_array.shift(); // remove first '' in array - this.seperator = "."; - return data_array; - } - - fromSrt(data: string): Transcript { - var originalData = data; - var data_array = this.tryComma(originalData); - if (data_array.length == 0) { - data_array = this.tryDot(originalData); - } - - var segments = []; - for (var i = 0; i < data_array.length; i += 4) { - const startTime = this.correctFormat(data_array[i + 1].trim()); - const endTime = this.correctFormat(data_array[i + 2].trim()); - let text = data_array[i + 3].trim(); - text = text.replace(/\n/, " "); - const segment = new TranscriptSegment( - data_array[i].trim(), - this.timestampToSeconds(startTime), - this.timestampToSeconds(endTime), - text, - ); - segments.push(segment); - } - - return new Transcript(segments); - } -} - - -export class TranscriptsCache { - cache: Map = new Map(); - constructor(private settings: AudioNotesSettings, private loadFiles: (filenames: string[]) => Promise>) { } - - async getTranscript(transcriptFilename: string | undefined): Promise { - if (transcriptFilename === undefined) { - return undefined; - } - - // Check the cache first. - if (this.cache.has(transcriptFilename)) { - return this.cache.get(transcriptFilename); - } - - let transcriptContents: string | undefined = undefined; - let transcript: Transcript | undefined = undefined; - // Check if the transcript is a file. - if (transcriptFilename.endsWith(".json") || transcriptFilename.endsWith(".srt")) { - const translationFilesContents = await this.loadFiles([transcriptFilename]); - transcriptContents = translationFilesContents.get(transcriptFilename); - if (transcriptContents !== undefined) { - transcript = parseTranscript(transcriptContents); - } - // Check if the transcript is a youtube video's subtitles. - } else if (transcriptFilename.includes("youtube.com")) { - const urlParts = transcriptFilename.split("?"); - const urlParams: Map = new Map(); - for (const param of urlParts[1].split("&")) { - const [key, value] = param.split("="); - urlParams.set(key, value); - } - const url = `${urlParts[0]}?v=${urlParams.get("v")}`; - transcript = await getYouTubeTranscript(url); - } - // Check if the transcript can be found online. - if (transcript === undefined && this.settings.audioNotesApiKey) { - transcriptContents = await request({ - url: 'https://iszrj6j2vk.execute-api.us-east-1.amazonaws.com/prod/transcriptions', - method: 'GET', - headers: { - 'x-api-key': this.settings.audioNotesApiKey, - "url": transcriptFilename, - }, - contentType: 'application/json', - }); - if (transcriptContents) { - transcript = parseTranscript(transcriptContents); - } - } - - // Put the result in the cache before returning it. - if (transcript) { - this.cache.set(transcriptFilename, transcript); - } - return transcript; - } -} +import { Notice, Plugin, request } from "obsidian"; +import { XMLParser } from 'fast-xml-parser'; +import type { AudioNotesSettings } from "./AudioNotesSettings"; +import type { DeepgramAlternative, DeepgramTranscriptionResponse } from "./Deepgram"; + + +export class Transcript { + constructor( + public segments: TranscriptSegment[], + ) { } + + public getQuote(quoteStart: number, quoteEnd: number): [number, number, string] { + // Get the relevant part of the transcript. + const segments = this.segments; + const result = []; + let start = undefined; + let end = undefined; + for (let segment of segments) { + const text = segment.text; + const segmentStart = segment.start; + const segmentEnd = segment.end; + // If either the segment's start or end is inside the range specified by the user... + if ((quoteStart <= segmentStart && segmentStart < quoteEnd) || (quoteStart < segmentEnd && segmentEnd <= quoteEnd)) { + result.push(text); + if (start === undefined) { + start = segmentStart; + } + end = segmentEnd; + } + // If the range specified by the user is entirely within the segment... + if (quoteStart >= segmentStart && quoteEnd <= segmentEnd) { + result.push(text); + if (start === undefined) { + start = segmentStart; + } + end = segmentEnd; + } + } + let quoteText = result.join(" ").trim(); + if (quoteText) { + // For some reason double spaces are often in the text. Remove them because they get removed by the HTML rendering anyway. + let i = 0; + while (quoteText.includes(" ")) { + quoteText = quoteText.replace(new RegExp(" "), " "); + // Make sure we don't hit an infinite loop, even though it should be impossible. + i += 1; + if (i > 100) { + break; + } + } + } + if (start === undefined || end === undefined) { + new Notice("Transcript file does not have start or end times for at least one text entry."); + console.error(segments); + throw new Error("Transcript file does not have start or end times for at least one text entry."); + } + return [start, end, quoteText]; + } + + public getSegmentAt(time: number): [number | undefined, TranscriptSegment | undefined] { + for (let i = 0; i < this.segments.length; i++) { + const segment = this.segments[i]; + if (segment.start <= time && time < segment.end) { + return [i, segment]; + } + } + return [undefined, undefined]; // if not found + } + + public toJSON(): string { + const result = []; + for (const segment of this.segments) { + result.push({id: segment.id, start: segment.start, end: segment.end, text: segment.text}); + } + return JSON.stringify(result, undefined, 2); + } +} + + +export class TranscriptSegment { + constructor( + public id: number | string, // we don't use this. if we do, probably make it a number. + public start: number, // time in seconds + public end: number, // time in seconds + public text: string, + ) { } +} + + +export function parseTranscript(contents: string): Transcript { + // We don't always have a filename (e.g. if the transcript was pulled from online). Assume JSON, and fallback to SRT. + try { + return new Transcript(JSON.parse(contents).segments); + } catch { + return new SrtParser().fromSrt(contents); + } +} + + +export function getTranscriptFromDGResponse(response: DeepgramTranscriptionResponse): Transcript { + let bestAlternative: DeepgramAlternative | undefined = undefined; + let bestConfidence = 0; + const alternatives = response.results.channels[0].alternatives; + for (const alt of alternatives) { + if (alt.confidence > bestConfidence) { + bestAlternative = alt; + } + } + const firstWord = bestAlternative!.words[0]; + const segments: TranscriptSegment[] = [new TranscriptSegment(0, firstWord.start, firstWord.start, firstWord.punctuated_word)]; + let id = 1; + for (let i = 1; i < bestAlternative!.words.length - 2; i++) { + const word = bestAlternative!.words[i]; + const lastSegment = segments[segments.length - 1]; + lastSegment.text += " " + word.punctuated_word; + lastSegment.end = word.end; + if ([".", "!", "?"].includes(word.punctuated_word[word.punctuated_word.length - 1])) { + const nextWord = bestAlternative!.words[i + 1]; + segments.push(new TranscriptSegment(id, nextWord.start, nextWord.start, nextWord.punctuated_word)); + id++; + } + } + const lastWord = bestAlternative!.words[bestAlternative!.words.length - 1]; + const lastSegment = segments[segments.length - 1]; + lastSegment.text += " " + lastWord.punctuated_word; + lastSegment.end = lastWord.end; + return new Transcript(segments); +} + + +export async function getYouTubeTranscript(url: string): Promise { + const html: string = await request({ + url: url, + method: 'GET' + }); + + function unescapeText(s: string): string { + var re = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g; + var unescaped: Map = new Map([ + ['&', '&'], + ['&', '&'], + ['<', '<'], + ['<', '<'], + ['>', '>'], + ['>', '>'], + [''', "'"], + [''', "'"], + ['"', '"'], + ['"', '"'], + ]); + return s.replace(re, function (m: string): string { + return unescaped.get(m) || m; + }); + } + + const captionsMetadata = JSON.parse(html.split(`"captions":`)[1].split(`,"videoDetails`)[0]).playerCaptionsTracklistRenderer.captionTracks; + for (const capmeta of captionsMetadata) { + if (capmeta.languageCode === 'en') { + const xml: string = await request({ + url: capmeta.baseUrl, + method: 'GET' + }); + const xmlParser = new XMLParser({ ignoreAttributes: false }); + const parsed = xmlParser.parse(xml).transcript.text; + let id = 0; + const segments: TranscriptSegment[] = []; + for (const line of parsed) { + const start = parseFloat(line['@_start']); + const duration = parseFloat(line['@_dur']); + let text = unescapeText(line['#text']); + text = text.replace("\n", " "); + text = text.toLowerCase(); + const sentenceCaseRegex = /(^\w{1}|\.\s*\w{1})/gi; + text = text.replace(sentenceCaseRegex, function (toReplace: string): string { return toReplace.toUpperCase(); }); + const end = start + duration; + const segment = new TranscriptSegment(id, start, end, text); + segments.push(segment); + id = id + 1; + } + const transcript = new Transcript(segments); + return transcript; + } + } + return undefined; +} + + +class SrtParser { + seperator = ","; + + timestampToSeconds(srtTimestamp: string) { + const [rest, millisecondsString] = srtTimestamp.split(","); + const milliseconds = parseInt(millisecondsString); + const [hours, minutes, seconds] = rest.split(":").map((x) => parseInt(x)); + const result = milliseconds * 0.001 + seconds + 60 * minutes + 3600 * hours; + + // fix odd JS roundings, e.g. timestamp '00:01:20,460' result is 80.46000000000001 + return Math.round(result * 1000) / 1000; + }; + + correctFormat(time: string) { + // Fix the format if the format is wrong + // 00:00:28.9670 Become 00:00:28,967 + // 00:00:28.967 Become 00:00:28,967 + // 00:00:28.96 Become 00:00:28,960 + // 00:00:28.9 Become 00:00:28,900 + + // 00:00:28,96 Become 00:00:28,960 + // 00:00:28,9 Become 00:00:28,900 + // 00:00:28,0 Become 00:00:28,000 + // 00:00:28,01 Become 00:00:28,010 + // 0:00:10,500 Become 00:00:10,500 + let str = time.replace(".", ","); + + var hour = null; + var minute = null; + var second = null; + var millisecond = null; + + // Handle millisecond + var [front, ms] = str.split(","); + millisecond = this.fixed_str_digit(3, ms); + + // Handle hour + var [a_hour, a_minute, a_second] = front.split(":"); + hour = this.fixed_str_digit(2, a_hour, false); + minute = this.fixed_str_digit(2, a_minute, false); + second = this.fixed_str_digit(2, a_second, false); + + return `${hour}:${minute}:${second},${millisecond}`; + } + + /* + // make sure string is 'how_many_digit' long + // if str is shorter than how_many_digit, pad with 0 + // if str is longer than how_many_digit, slice from the beginning + // Example: + Input: fixed_str_digit(3, '100') + Output: 100 + Explain: unchanged, because "100" is 3 digit + Input: fixed_str_digit(3, '50') + Output: 500 + Explain: pad end with 0 + Input: fixed_str_digit(3, '50', false) + Output: 050 + Explain: pad start with 0 + Input: fixed_str_digit(3, '7771') + Output: 777 + Explain: slice from beginning + */ + private fixed_str_digit( + how_many_digit: number, + str: string, + padEnd: boolean = true + ) { + if (str.length == how_many_digit) { + return str; + } + if (str.length > how_many_digit) { + return str.slice(0, how_many_digit); + } + if (str.length < how_many_digit) { + if (padEnd) { + return str.padEnd(how_many_digit, "0"); + } else { + return str.padStart(how_many_digit, "0"); + } + } + } + + private tryComma(data: string) { + data = data.replace(/\r/g, ""); + var regex = + /(\d+)\n(\d{1,2}:\d{2}:\d{2},\d{1,3}) --> (\d{1,2}:\d{2}:\d{2},\d{1,3})/g; + let data_array = data.split(regex); + data_array.shift(); // remove first '' in array + return data_array; + } + + private tryDot(data: string) { + data = data.replace(/\r/g, ""); + var regex = + /(\d+)\n(\d{1,2}:\d{2}:\d{2}\.\d{1,3}) --> (\d{1,2}:\d{2}:\d{2}\.\d{1,3})/g; + let data_array = data.split(regex); + data_array.shift(); // remove first '' in array + this.seperator = "."; + return data_array; + } + + fromSrt(data: string): Transcript { + var originalData = data; + var data_array = this.tryComma(originalData); + if (data_array.length == 0) { + data_array = this.tryDot(originalData); + } + + var segments = []; + for (var i = 0; i < data_array.length; i += 4) { + const startTime = this.correctFormat(data_array[i + 1].trim()); + const endTime = this.correctFormat(data_array[i + 2].trim()); + let text = data_array[i + 3].trim(); + text = text.replace(/\n/, " "); + const segment = new TranscriptSegment( + data_array[i].trim(), + this.timestampToSeconds(startTime), + this.timestampToSeconds(endTime), + text, + ); + segments.push(segment); + } + + return new Transcript(segments); + } +} + + +export class TranscriptsCache { + cache: Map = new Map(); + constructor(private settings: AudioNotesSettings, private loadFiles: (filenames: string[]) => Promise>) { } + + async getTranscript(transcriptFilename: string | undefined): Promise { + if (transcriptFilename === undefined) { + return undefined; + } + + // Check the cache first. + if (this.cache.has(transcriptFilename)) { + return this.cache.get(transcriptFilename); + } + + let transcriptContents: string | undefined = undefined; + let transcript: Transcript | undefined = undefined; + // Check if the transcript is a file. + if (transcriptFilename.endsWith(".json") || transcriptFilename.endsWith(".srt")) { + const translationFilesContents = await this.loadFiles([transcriptFilename]); + transcriptContents = translationFilesContents.get(transcriptFilename); + if (transcriptContents !== undefined) { + transcript = parseTranscript(transcriptContents); + } + // Check if the transcript is a youtube video's subtitles. + } else if (transcriptFilename.includes("youtube.com")) { + const urlParts = transcriptFilename.split("?"); + const urlParams: Map = new Map(); + for (const param of urlParts[1].split("&")) { + const [key, value] = param.split("="); + urlParams.set(key, value); + } + const url = `${urlParts[0]}?v=${urlParams.get("v")}`; + transcript = await getYouTubeTranscript(url); + } + // Check if the transcript can be found online. + if (transcript === undefined && this.settings.audioNotesApiKey) { + transcriptContents = await request({ + url: 'https://iszrj6j2vk.execute-api.us-east-1.amazonaws.com/prod/transcriptions', + method: 'GET', + headers: { + 'x-api-key': this.settings.audioNotesApiKey, + "url": transcriptFilename, + }, + contentType: 'application/json', + }); + if (transcriptContents) { + transcript = parseTranscript(transcriptContents); + } + } + + // Put the result in the cache before returning it. + if (transcript) { + this.cache.set(transcriptFilename, transcript); + } + return transcript; + } +} diff --git a/src/main.ts b/src/main.ts index a47a733..916b511 100644 --- a/src/main.ts +++ b/src/main.ts @@ -103,7 +103,7 @@ export default class AutomaticAudioNotes extends Plugin { loadedData["_audioNotesApiKey"], loadedData["_debugMode"], loadedData["_DGApiKey"], - loadedData["_showDeepgramLogo"] + loadedData["_DGTranscriptFolder"], ); this.settings = AudioNotesSettings.overrideDefaultSettings(newSettings); } @@ -499,7 +499,8 @@ export default class AutomaticAudioNotes extends Plugin { this.app, mp3Files, this.settings.audioNotesApiKey, - this.settings.getInfoByApiKey() + this.settings.getInfoByApiKey(), + this.settings.DGApiKey, ).open(); this._updateCounts(); }, @@ -600,7 +601,8 @@ export default class AutomaticAudioNotes extends Plugin { new EnqueueAudioModal( this.app, this.settings.audioNotesApiKey, - this.settings.getInfoByApiKey() + this.settings.getInfoByApiKey(), + this.settings.DGApiKey, ).open(); }, }); diff --git a/src/utils.ts b/src/utils.ts index 031b6c9..cb5809e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,251 +1,286 @@ -import { findIconDefinition, icon as getFAIcon } from "@fortawesome/fontawesome-svg-core"; -import type { IconName } from "@fortawesome/fontawesome-svg-core"; -import type { IconPrefix } from "@fortawesome/free-regular-svg-icons"; - - -export class DefaultMap extends Map { - /** Usage - * new DefaultMap(() => 0) - * new DefaultMap(() => []) - */ - constructor(private defaultFactory: () => V) { - super(); - } - - get(key: K): V { - if (!super.has(key)) { - super.set(key, this.defaultFactory()); - } - return super.get(key)!; - } -} - - -export class Podcast { - constructor(public name: string, public author: string, public feedUrl: string) { } -} - - -export class PodcastEpisode { - constructor(public title: string, public url: string) { } -} - - -export function generateRandomString(length: number) { - let result = ''; - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - return result; -} - - -export function getIcon(iconName: string) { - for (const prefix of ["fas", "far", "fab", "fa"] as IconPrefix[]) { - const definition = findIconDefinition({ - iconName: iconName as IconName, - prefix - }); - if (definition) return getFAIcon(definition).node[0]; - } -} - - -export function secondsToTimeString(totalSeconds: number, truncateMilliseconds: boolean): string { - if (totalSeconds === 0) { - return "00:00"; - } - let hours = Math.floor(totalSeconds / 3600); - let minutes = Math.floor((totalSeconds / 60 - (hours * 60))); - let seconds = totalSeconds - (hours * 3600 + minutes * 60); - let s = ""; - if (hours > 0) { - if (hours >= 10) { - s += hours.toString() + ":"; - } else { - s += "0" + hours.toString() + ":"; - } - } - if (minutes >= 10) { - s += minutes.toString() + ":"; - } else { - s += "0" + minutes.toString() + ":"; - } - seconds = Math.round(seconds * 100) / 100; // round to 2 decimal places - if (seconds >= 10) { - s += seconds.toString(); - } else { - s += "0" + seconds.toString(); - } - if (Number.isNaN(hours) || Number.isNaN(minutes) || Number.isNaN(seconds) || hours === undefined || minutes === undefined || seconds === undefined) { - throw new Error(`Failed to convert seconds to time string: ${totalSeconds}`); - } - if (truncateMilliseconds && s.includes(".")) { - s = s.slice(0, s.indexOf(".")); - } - return s; -} - - -export function timeStringToSeconds(s: string): number { - let hours = 0; - let minutes = 0; - let seconds = 0; - const split = s.split(":"); - if (split.length > 2) { - hours = parseInt(split[0]); - minutes = parseInt(split[1]); - seconds = parseFloat(split[2]); - } else if (split.length > 1) { - minutes = parseInt(split[0]); - seconds = parseFloat(split[1]); - } else { - seconds = parseFloat(split[0]); - } - if (Number.isNaN(hours) || Number.isNaN(minutes) || Number.isNaN(seconds) || hours === undefined || minutes === undefined || seconds === undefined) { - throw new Error(`Failed to convert time string to seconds: ${s}`); - } - return (hours * 3600) + (minutes * 60) + seconds; -} - - -/** - * generate groups of 4 random characters - * @example getUniqueId(1) : 607f - * @example getUniqueId(4) : 95ca-361a-f8a1-1e73 - */ -export function getUniqueId(parts: number): string { - const stringArr = []; - for (let i = 0; i < parts; i++) { - // tslint:disable-next-line:no-bitwise - const S4 = (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); - stringArr.push(S4); - } - return stringArr.join('-'); -} - - -export function createSelect(keys: string[], values: string[], cls: string, noDefault: boolean) { - const select = createEl("select", { - cls: cls - }); - if (noDefault) { - const o = select.createEl("option"); - o.selected = true; - o.disabled = true; - o.hidden = true; - } - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - const value = values[i]; - const o = select.createEl("option"); - o.textContent = key; - o.value = value; - } - return select; -} - - -export const WHISPER_LANGUAGE_CODES = [ - ["en", "English"], - ["zh", "Chinese"], - ["de", "German"], - ["es", "Spanish"], - ["ru", "Russian"], - ["ko", "Korean"], - ["fr", "French"], - ["ja", "Japanese"], - ["pt", "Portuguese"], - ["tr", "Turkish"], - ["pl", "Polish"], - ["ca", "Catalan"], - ["nl", "Dutch"], - // ["ar", "Arabic"], - // ["sv", "Swedish"], - ["it", "Italian"], - // ["id", "Indonesian"], - ["hi", "Hindi"], - // ["fi", "Finnish"], - ["vi", "Vietnamese"], - // ["he", "Hebrew"], - ["uk", "Ukrainian"], - ["el", "Greek"], - // ["ms", "Malay"], - ["cs", "Czech"], - ["ro", "Romanian"], - ["da", "Danish"], - ["hu", "Hungarian"], - // ["ta", "Tamil"], - // ["no", "Norwegian"], - ["th", "Thai"], - // ["ur", "Urdu"], - // ["hr", "Croatian"], - // ["bg", "Bulgarian"], - // ["lt", "Lithuanian"], - // ["la", "Latin"], - // ["mi", "Maori"], - // ["ml", "Malayalam"], - // ["cy", "Welsh"], - // ["sk", "Slovak"], - // ["te", "Telugu"], - // ["fa", "Persian"], - // ["lv", "Latvian"], - // ["bn", "Bengali"], - // ["sr", "Serbian"], - // ["az", "Azerbaijani"], - // ["sl", "Slovenian"], - // ["kn", "Kannada"], - // ["et", "Estonian"], - // ["mk", "Macedonian"], - // ["br", "Breton"], - // ["eu", "Basque"], - // ["is", "Icelandic"], - // ["hy", "Armenian"], - // ["ne", "Nepali"], - // ["mn", "Mongolian"], - // ["bs", "Bosnian"], - // ["kk", "Kazakh"], - // ["sq", "Albanian"], - // ["sw", "Swahili"], - // ["gl", "Galician"], - // ["mr", "Marathi"], - // ["pa", "Punjabi"], - // ["si", "Sinhala"], - // ["km", "Khmer"], - // ["sn", "Shona"], - // ["yo", "Yoruba"], - // ["so", "Somali"], - // ["af", "Afrikaans"], - // ["oc", "Occitan"], - // ["ka", "Georgian"], - // ["be", "Belarusian"], - // ["tg", "Tajik"], - // ["sd", "Sindhi"], - // ["gu", "Gujarati"], - // ["am", "Amharic"], - // ["yi", "Yiddish"], - // ["lo", "Lao"], - // ["uz", "Uzbek"], - // ["fo", "Faroese"], - // ["ht", "Haitian creole"], - // ["ps", "Pashto"], - // ["tk", "Turkmen"], - // ["nn", "Nynorsk"], - // ["mt", "Maltese"], - // ["sa", "Sanskrit"], - // ["lb", "Luxembourgish"], - // ["my", "Myanmar"], - // ["bo", "Tibetan"], - // ["tl", "Tagalog"], - // ["mg", "Malagasy"], - // ["as", "Assamese"], - // ["tt", "Tatar"], - // ["haw", "Hawaiian"], - // ["ln", "Lingala"], - // ["ha", "Hausa"], - // ["ba", "Bashkir"], - // ["jw", "Javanese"], - // ["su", "Sundanese"], -] +import { findIconDefinition, icon as getFAIcon } from "@fortawesome/fontawesome-svg-core"; +import type { IconName } from "@fortawesome/fontawesome-svg-core"; +import type { IconPrefix } from "@fortawesome/free-regular-svg-icons"; + + +export class DefaultMap extends Map { + /** Usage + * new DefaultMap(() => 0) + * new DefaultMap(() => []) + */ + constructor(private defaultFactory: () => V) { + super(); + } + + get(key: K): V { + if (!super.has(key)) { + super.set(key, this.defaultFactory()); + } + return super.get(key)!; + } +} + + +export class Podcast { + constructor(public name: string, public author: string, public feedUrl: string) { } +} + + +export class PodcastEpisode { + constructor(public title: string, public url: string) { } +} + + +export function generateRandomString(length: number) { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + + +export function getIcon(iconName: string) { + for (const prefix of ["fas", "far", "fab", "fa"] as IconPrefix[]) { + const definition = findIconDefinition({ + iconName: iconName as IconName, + prefix + }); + if (definition) return getFAIcon(definition).node[0]; + } +} + + +export function secondsToTimeString(totalSeconds: number, truncateMilliseconds: boolean): string { + if (totalSeconds === 0) { + return "00:00"; + } + let hours = Math.floor(totalSeconds / 3600); + let minutes = Math.floor((totalSeconds / 60 - (hours * 60))); + let seconds = totalSeconds - (hours * 3600 + minutes * 60); + let s = ""; + if (hours > 0) { + if (hours >= 10) { + s += hours.toString() + ":"; + } else { + s += "0" + hours.toString() + ":"; + } + } + if (minutes >= 10) { + s += minutes.toString() + ":"; + } else { + s += "0" + minutes.toString() + ":"; + } + seconds = Math.round(seconds * 100) / 100; // round to 2 decimal places + if (seconds >= 10) { + s += seconds.toString(); + } else { + s += "0" + seconds.toString(); + } + if (Number.isNaN(hours) || Number.isNaN(minutes) || Number.isNaN(seconds) || hours === undefined || minutes === undefined || seconds === undefined) { + throw new Error(`Failed to convert seconds to time string: ${totalSeconds}`); + } + if (truncateMilliseconds && s.includes(".")) { + s = s.slice(0, s.indexOf(".")); + } + return s; +} + + +export function timeStringToSeconds(s: string): number { + let hours = 0; + let minutes = 0; + let seconds = 0; + const split = s.split(":"); + if (split.length > 2) { + hours = parseInt(split[0]); + minutes = parseInt(split[1]); + seconds = parseFloat(split[2]); + } else if (split.length > 1) { + minutes = parseInt(split[0]); + seconds = parseFloat(split[1]); + } else { + seconds = parseFloat(split[0]); + } + if (Number.isNaN(hours) || Number.isNaN(minutes) || Number.isNaN(seconds) || hours === undefined || minutes === undefined || seconds === undefined) { + throw new Error(`Failed to convert time string to seconds: ${s}`); + } + return (hours * 3600) + (minutes * 60) + seconds; +} + + +/** + * generate groups of 4 random characters + * @example getUniqueId(1) : 607f + * @example getUniqueId(4) : 95ca-361a-f8a1-1e73 + */ +export function getUniqueId(parts: number): string { + const stringArr = []; + for (let i = 0; i < parts; i++) { + // tslint:disable-next-line:no-bitwise + const S4 = (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); + stringArr.push(S4); + } + return stringArr.join('-'); +} + + +export function createSelect(keys: string[], values: string[], cls: string, noDefault: boolean) { + const select = createEl("select", { + cls: cls + }); + if (noDefault) { + const o = select.createEl("option"); + o.selected = true; + o.disabled = true; + o.hidden = true; + } + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = values[i]; + const o = select.createEl("option"); + o.textContent = key; + o.value = value; + } + return select; +} + + +export const WHISPER_LANGUAGE_CODES = [ + ["en", "English"], + ["zh", "Chinese"], + ["de", "German"], + ["es", "Spanish"], + ["ru", "Russian"], + ["ko", "Korean"], + ["fr", "French"], + ["ja", "Japanese"], + ["pt", "Portuguese"], + ["tr", "Turkish"], + ["pl", "Polish"], + ["ca", "Catalan"], + ["nl", "Dutch"], + // ["ar", "Arabic"], + // ["sv", "Swedish"], + ["it", "Italian"], + // ["id", "Indonesian"], + ["hi", "Hindi"], + // ["fi", "Finnish"], + ["vi", "Vietnamese"], + // ["he", "Hebrew"], + ["uk", "Ukrainian"], + ["el", "Greek"], + // ["ms", "Malay"], + ["cs", "Czech"], + ["ro", "Romanian"], + ["da", "Danish"], + ["hu", "Hungarian"], + // ["ta", "Tamil"], + // ["no", "Norwegian"], + ["th", "Thai"], + // ["ur", "Urdu"], + // ["hr", "Croatian"], + // ["bg", "Bulgarian"], + // ["lt", "Lithuanian"], + // ["la", "Latin"], + // ["mi", "Maori"], + // ["ml", "Malayalam"], + // ["cy", "Welsh"], + // ["sk", "Slovak"], + // ["te", "Telugu"], + // ["fa", "Persian"], + // ["lv", "Latvian"], + // ["bn", "Bengali"], + // ["sr", "Serbian"], + // ["az", "Azerbaijani"], + // ["sl", "Slovenian"], + // ["kn", "Kannada"], + // ["et", "Estonian"], + // ["mk", "Macedonian"], + // ["br", "Breton"], + // ["eu", "Basque"], + // ["is", "Icelandic"], + // ["hy", "Armenian"], + // ["ne", "Nepali"], + // ["mn", "Mongolian"], + // ["bs", "Bosnian"], + // ["kk", "Kazakh"], + // ["sq", "Albanian"], + // ["sw", "Swahili"], + // ["gl", "Galician"], + // ["mr", "Marathi"], + // ["pa", "Punjabi"], + // ["si", "Sinhala"], + // ["km", "Khmer"], + // ["sn", "Shona"], + // ["yo", "Yoruba"], + // ["so", "Somali"], + // ["af", "Afrikaans"], + // ["oc", "Occitan"], + // ["ka", "Georgian"], + // ["be", "Belarusian"], + // ["tg", "Tajik"], + // ["sd", "Sindhi"], + // ["gu", "Gujarati"], + // ["am", "Amharic"], + // ["yi", "Yiddish"], + // ["lo", "Lao"], + // ["uz", "Uzbek"], + // ["fo", "Faroese"], + // ["ht", "Haitian creole"], + // ["ps", "Pashto"], + // ["tk", "Turkmen"], + // ["nn", "Nynorsk"], + // ["mt", "Maltese"], + // ["sa", "Sanskrit"], + // ["lb", "Luxembourgish"], + // ["my", "Myanmar"], + // ["bo", "Tibetan"], + // ["tl", "Tagalog"], + // ["mg", "Malagasy"], + // ["as", "Assamese"], + // ["tt", "Tatar"], + // ["haw", "Hawaiian"], + // ["ln", "Lingala"], + // ["ha", "Hausa"], + // ["ba", "Bashkir"], + // ["jw", "Javanese"], + // ["su", "Sundanese"], +]; + +export const DG_LANGUAGE_CODES = [ + ["en-US", "English (United States)"], // put at the top so this is the default for dropdowns + ["zh", "Chinese"], + ["zh-CN", "Chinese (China)"], + ["zh-TW", "Chinese (Taiwan)"], + ["da", "Danish"], + ["nl", "Dutch"], + ["en", "English"], + ["en-AU", "English (Australia)"], + ["en-GB", "English (United Kingdom)"], + ["en-IN", "English (India)"], + ["en-NZ", "English (New Zealand)"], + ["nl", "Flemish"], + ["fr", "French"], + ["fr-CA", "French (Canada)"], + ["de", "German"], + ["hi", "Hindi"], + ["hi-Latn", "Hindi (Roman Script)"], + ["id", "Indonesian"], + ["it", "Italian"], + ["ja", "Japanese"], + ["ko", "Korean"], + ["no", "Norwegian"], + ["pl", "Polish"], + ["pt", "Portuguese"], + ["pt-BR", "Portuguese (Brazil)"], + ["pt-PT", "Portuguese (Portugal)"], + ["ru", "Russian"], + ["es", "Spanish"], + ["es-419", "Spanish (Latin America)"], + ["sv", "Swedish"], + ["tr", "Turkish"], + ["uk", "Ukrainian"], +]; diff --git a/styles.css b/styles.css index a065567..cca42c7 100644 --- a/styles.css +++ b/styles.css @@ -1,472 +1,477 @@ -/* - -This CSS file will be included with your plugin, and -available in the app when your plugin is enabled. - -If your plugin does not need CSS, delete this file. - -*/ - -.audio-note { - padding-right: 24px !important; -} - -.audio-note audio { - margin-top: 16px; -} - -.audio-note .audio-note-title { - padding: var(--callout-title-padding); - display: flex; - gap: var(--size-4-1); - font-size: var(--callout-title-size); - color: rgb(var(--callout-color)); - line-height: var(--line-height-tight); - margin-bottom: 0px; -} - -.audio-note .audio-note-title .audio-note-icon { - flex: 0 0 auto; - display: flex; - align-self: center; -} - -.audio-note .audio-note-title .audio-note-title-inner { - font-weight: var(--bold-weight); - color: var(--callout-title-color); -} - -.audio-note p { - margin-top: 16px; - margin-bottom: 0px; -} - -.audio-note .audio-note-title p { - margin-top: 0px; - margin-bottom: 0px; -} - -.audio-note .audio-note-author p { - margin-top: 4px; - margin-bottom: 0px; -} - -/* */ -.audio-note-play-button, -.audio-note-mute-button, -.audio-note-forward-button, -.audio-note-backward-button, -.audio-note-reset-button { - padding: 0 !important; - border: 0; - background: transparent; - background-color: transparent !important; - box-shadow: none !important; - cursor: pointer; - outline: none; - width: fit-content !important; - height: 40px; - margin: 0; -} - -.audio-note-play-button { - float: left; - margin-left: 16px; - margin-right: 8px; -} - -.audio-note-backward-button { - margin-left: 16px; - margin-right: 0px; -} - -.audio-note-forward-button { - margin-left: 12px; - margin-right: 0px; -} - -.audio-note-reset-button { - margin-left: 12px; - margin-right: 24px; -} - -.audio-note-mute-button { - margin-left: auto; - margin-right: 16px; - min-width: 15px; - max-width: 15px; -} - -.audio-player-container { - display: flex; - align-items: center; - margin-top: 16px; - margin-bottom: 4px; - margin-left: 8px; - margin-right: 8px; - --seek-before-width: 0%; - --volume-before-width: 100%; - --buffered-width: 0%; - position: relative; - width: calc(100% - 8px - 8px); - border: solid 1px rgba(255, 255, 255, 0.15); - border-radius: 12px; -} - -.audio-player-container-mobile { - display: block; - margin-top: 16px; - margin-bottom: 4px; - margin-left: 8px; - margin-right: 8px; - --seek-before-width: 0%; - --volume-before-width: 100%; - --buffered-width: 0%; - position: relative; - width: calc(100% - 8px - 8px); - height: fit-content; - border: solid 1px rgba(255, 255, 255, 0.15); - border-radius: 12px; -} - -.audio-player-container-top { - display: flex; - align-items: center; -} - -.audio-player-container-bottom { - display: block; - align-items: center; - width: fit-content; - margin-left: auto; - margin-right: auto; -} - -.seek-slider { - width: -webkit-fill-available !important; -} - -.volume-slider { - width: 100%; -} - -.audio-player-container::before { - position: absolute; - content: ""; - width: calc(100% + 4px); - height: calc(100% + 4px); - left: -2px; - top: -2px; - background: transparent; - z-index: -1; -} - -.time { - width: fit-content; - min-width: fit-content; - max-width: fit-content; - margin-bottom: 1px; - margin-left: 8px; -} - -.audio-controls-output { - display: inline-block; - width: 32px; - text-align: center; - font-size: 20px; - clear: left; - margin-top: 0px; - margin-bottom: 0px; - margin-left: 8px; - margin-right: 0px; -} - -.audio-player-container input[type="range"] { - width: unset; - background: transparent; - position: relative; - -webkit-appearance: none; - margin-top: 0px; - margin-bottom: 0px; - margin-left: 8px; - margin-right: 8px; - padding: 0; - float: left; - outline: none; -} - -.audio-player-container input[type="range"]::-webkit-slider-runnable-track { - height: 3px; - cursor: pointer; - background: rgba(255, 255, 255, 0.5); -} - -.audio-player-container input[type="range"]::before { - position: absolute; - content: ""; - top: 8px; - left: 0; - width: var(--seek-before-width); - height: 3px; - background-color: #007db5; - cursor: pointer; -} - -.audio-player-container input[type="range"]::-webkit-slider-thumb { - position: relative; - -webkit-appearance: none; - box-sizing: content-box; - height: 15px; - width: 15px; - border-radius: 50%; - outline: none; - border: none; - background-color: #fff; - cursor: pointer; -} - -.audio-player-container input[type="range"]::-webkit-slider-thumb { - margin: -1px 0 0 0; -} - -.audio-player-container-mobile input[type="range"]::-webkit-slider-thumb { - margin: 2.5px 0 0 0; -} - -.audio-player-container input[type="range"]::-moz-range-track { - width: 100%; - height: 3px; - cursor: pointer; - background: #fff; -} - -.audio-player-container input[type="range"]::-moz-focus-outer { - border: 0; -} - -.audio-player-container input[type="range"]::-moz-range-thumb { - box-sizing: content-box; - height: 15px; - width: 15px; - border-radius: 50%; - outline: none; - border: none; - background-color: #fff; - cursor: pointer; -} - -.audio-player-container input[type="range"]::-ms-track { - width: 100%; - height: 3px; - cursor: pointer; - background: transparent; - border: solid transparent; - color: transparent; -} - -.audio-player-container input[type="range"]::-ms-thumb { - box-sizing: content-box; - height: 15px; - width: 15px; - border-radius: 50%; - outline: none; - border: none; - background-color: #fff; - cursor: pointer; - margin: 2.5px 0 0 0; -} - -.audio-player-container input[type="range"]::before { - background-color: transparent; -} - -select.select-model-accuracy { - margin-left: auto; - display: flex; - margin-bottom: 12px; -} - -/* Create new audio note prompt */ -.create-new-audio-note-file-title { - margin-left: 12px; - margin-right: 12px; - text-align: center; -} - -.create-new-audio-note-file-input-element { -} - -.create-new-audio-note-file-url-container { - display: flex; -} - -.create-new-audio-note-file-submit-button { - width: fit-content; - align-self: center; - margin-top: 12px; - margin-bottom: 12px; - margin-left: 12px; - margin-right: 12px; -} - -.create-new-audio-note-file-submit-button:disabled { - opacity: 30%; -} - -.create-new-audio-note-file-prompt-input-container { - width: 100%; -} - -/*START tabs*/ -.tab { - padding: var(--size-4-3); - text-align: center; - border-bottom: 1px solid var(--background-secondary); - width: 100%; -} - -button.tablinks:not(:last-child) { - margin-right: 16px; -} - -.transcription-options-container-for-new-audio-note { - padding: var(--size-4-3); - display: flex; - justify-content: space-between; -} - -.transcription-options-container-for-new-audio-note span { - margin-left: 264px; - align-self: center; - margin-bottom: 12px; -} - -.transcription-options-container-for-new-audio-note input[type="checkbox"] { - margin-left: 16px; - margin-bottom: 12px; - align-self: center; -} - -.modal-container .prompt .podcast-input-div { - margin: var(--size-4-3); - display: flex; -} - -.modal-container .prompt .podcast-input-div .span-podcast-input-text { - width: 200px; - align-self: center; -} - -.modal-container .prompt .podcast-input-div .podcast-search-input { - margin-left: auto; - width: -webkit-fill-available; - max-width: calc(100% - 153px); -} - -.modal-container .prompt .podcast-select-div { - margin: var(--size-4-3); - display: flex; -} - -.modal-container .prompt .podcast-select-div .span-podcast-select-text { - width: 200px; - align-self: center; -} - -.modal-container .prompt .podcast-select-div .select-podcast-results { - margin-left: auto; - width: -webkit-fill-available; - max-width: calc(100% - 153px); -} - -.modal-container .prompt .podcast-episode-select-div { - margin: var(--size-4-3); - display: flex; -} - -.modal-container - .prompt - .podcast-episode-select-div - .span-podcast-episode-select-text { - width: 200px; - align-self: center; -} - -.modal-container - .prompt - .podcast-episode-select-div - .select-podcast-episode-results { - margin-left: auto; - width: -webkit-fill-available; - max-width: calc(100% - 153px); -} - -/* END tabs*/ - -/* START DG Quick Audio Note */ -.dg-audio-note { - border: 1px solid var(--background-modifier-border); - padding: 10px; -} - -.dg-audio-note-title { - font-weight: 600; -} - -.dg-audio-note-audio { - margin-top: 10px; -} - -.modal-main-div { - display: flex; - justify-content: center; - flex-direction: column; -} - -input.title-input { - padding: 6px 5px; -} -.button-container { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); -} - -.button-container button { - margin-right: 5px; - margin-left: 5px; -} - -.dg-audio-note-powered-by { - /* background-image: url("https://res.cloudinary.com/deepgram/image/upload/v1676406242/blog/DG-powered-by-logo-black-red-horizontal-rgb_wqhltl.svg"); */ - /* height: 20px; */ - width: auto; - - /* background-size: contain; */ - margin-left: auto; - /* background-repeat: no-repeat; - background-position: right; */ - display: flex; - justify-content: center; - align-items: center; -} -.dg-audio-note-logo.theme-light { - background-color: whitesmoke; - height: 20px; - width: auto; - margin-left: auto; -} - -.dg-audio-note-powered-by.hidden { - display: none; -} - -.dg-quick-note-modal { - --dialog-width: 75vw; - height: auto; - --dialog-max-height: 75vh; -} -.info svg { - width: 11px; - margin-left: 3px; -} -/* END DG Quick Audio Note */ +/* + +This CSS file will be included with your plugin, and +available in the app when your plugin is enabled. + +If your plugin does not need CSS, delete this file. + +*/ + +.audio-note { + padding-right: 24px !important; +} + +.audio-note audio { + margin-top: 16px; +} + +.audio-note .audio-note-title { + padding: var(--callout-title-padding); + display: flex; + gap: var(--size-4-1); + font-size: var(--callout-title-size); + color: rgb(var(--callout-color)); + line-height: var(--line-height-tight); + margin-bottom: 0px; +} + +.audio-note .audio-note-title .audio-note-icon { + flex: 0 0 auto; + display: flex; + align-self: center; +} + +.audio-note .audio-note-title .audio-note-title-inner { + font-weight: var(--bold-weight); + color: var(--callout-title-color); +} + +.audio-note p { + margin-top: 16px; + margin-bottom: 0px; +} + +.audio-note .audio-note-title p { + margin-top: 0px; + margin-bottom: 0px; +} + +.audio-note .audio-note-author p { + margin-top: 4px; + margin-bottom: 0px; +} + +/* */ +.audio-note-play-button, +.audio-note-mute-button, +.audio-note-forward-button, +.audio-note-backward-button, +.audio-note-reset-button { + padding: 0 !important; + border: 0; + background: transparent; + background-color: transparent !important; + box-shadow: none !important; + cursor: pointer; + outline: none; + width: fit-content !important; + height: 40px; + margin: 0; +} + +.audio-note-play-button { + float: left; + margin-left: 16px; + margin-right: 8px; +} + +.audio-note-backward-button { + margin-left: 16px; + margin-right: 0px; +} + +.audio-note-forward-button { + margin-left: 12px; + margin-right: 0px; +} + +.audio-note-reset-button { + margin-left: 12px; + margin-right: 24px; +} + +.audio-note-mute-button { + margin-left: auto; + margin-right: 16px; + min-width: 15px; + max-width: 15px; +} + +.audio-player-container { + display: flex; + align-items: center; + margin-top: 16px; + margin-bottom: 4px; + margin-left: 8px; + margin-right: 8px; + --seek-before-width: 0%; + --volume-before-width: 100%; + --buffered-width: 0%; + position: relative; + width: calc(100% - 8px - 8px); + border: solid 1px rgba(255, 255, 255, 0.15); + border-radius: 12px; +} + +.audio-player-container-mobile { + display: block; + margin-top: 16px; + margin-bottom: 4px; + margin-left: 8px; + margin-right: 8px; + --seek-before-width: 0%; + --volume-before-width: 100%; + --buffered-width: 0%; + position: relative; + width: calc(100% - 8px - 8px); + height: fit-content; + border: solid 1px rgba(255, 255, 255, 0.15); + border-radius: 12px; +} + +.audio-player-container-top { + display: flex; + align-items: center; +} + +.audio-player-container-bottom { + display: block; + align-items: center; + width: fit-content; + margin-left: auto; + margin-right: auto; +} + +.seek-slider { + width: -webkit-fill-available !important; +} + +.volume-slider { + width: 100%; +} + +.audio-player-container::before { + position: absolute; + content: ""; + width: calc(100% + 4px); + height: calc(100% + 4px); + left: -2px; + top: -2px; + background: transparent; + z-index: -1; +} + +.time { + width: fit-content; + min-width: fit-content; + max-width: fit-content; + margin-bottom: 1px; + margin-left: 8px; +} + +.audio-controls-output { + display: inline-block; + width: 32px; + text-align: center; + font-size: 20px; + clear: left; + margin-top: 0px; + margin-bottom: 0px; + margin-left: 8px; + margin-right: 0px; +} + +.audio-player-container input[type="range"] { + width: unset; + background: transparent; + position: relative; + -webkit-appearance: none; + margin-top: 0px; + margin-bottom: 0px; + margin-left: 8px; + margin-right: 8px; + padding: 0; + float: left; + outline: none; +} + +.audio-player-container input[type="range"]::-webkit-slider-runnable-track { + height: 3px; + cursor: pointer; + background: rgba(255, 255, 255, 0.5); +} + +.audio-player-container input[type="range"]::before { + position: absolute; + content: ""; + top: 8px; + left: 0; + width: var(--seek-before-width); + height: 3px; + background-color: #007db5; + cursor: pointer; +} + +.audio-player-container input[type="range"]::-webkit-slider-thumb { + position: relative; + -webkit-appearance: none; + box-sizing: content-box; + height: 15px; + width: 15px; + border-radius: 50%; + outline: none; + border: none; + background-color: #fff; + cursor: pointer; +} + +.audio-player-container input[type="range"]::-webkit-slider-thumb { + margin: -1px 0 0 0; +} + +.audio-player-container-mobile input[type="range"]::-webkit-slider-thumb { + margin: 2.5px 0 0 0; +} + +.audio-player-container input[type="range"]::-moz-range-track { + width: 100%; + height: 3px; + cursor: pointer; + background: #fff; +} + +.audio-player-container input[type="range"]::-moz-focus-outer { + border: 0; +} + +.audio-player-container input[type="range"]::-moz-range-thumb { + box-sizing: content-box; + height: 15px; + width: 15px; + border-radius: 50%; + outline: none; + border: none; + background-color: #fff; + cursor: pointer; +} + +.audio-player-container input[type="range"]::-ms-track { + width: 100%; + height: 3px; + cursor: pointer; + background: transparent; + border: solid transparent; + color: transparent; +} + +.audio-player-container input[type="range"]::-ms-thumb { + box-sizing: content-box; + height: 15px; + width: 15px; + border-radius: 50%; + outline: none; + border: none; + background-color: #fff; + cursor: pointer; + margin: 2.5px 0 0 0; +} + +.audio-player-container input[type="range"]::before { + background-color: transparent; +} + +select.select-model-accuracy { + margin-left: auto; + display: flex; + margin-bottom: 12px; +} + +/* Create new audio note prompt */ +.create-new-audio-note-file-title { + margin-left: 12px; + margin-right: 12px; + text-align: center; +} + +.create-new-audio-note-file-input-element { +} + +.create-new-audio-note-file-url-container { + display: flex; +} + +.create-new-audio-note-file-submit-button { + width: fit-content; + align-self: center; + margin-top: 12px; + margin-bottom: 12px; + margin-left: 12px; + margin-right: 12px; +} + +.create-new-audio-note-file-submit-button:disabled { + opacity: 30%; +} + +.create-new-audio-note-file-prompt-input-container { + width: 100%; +} + +/*START tabs*/ +.tab { + padding: var(--size-4-3); + text-align: center; + border-bottom: 1px solid var(--background-secondary); + width: 100%; +} + +button.tablinks:not(:last-child) { + margin-right: 16px; +} + +.transcription-options-container-for-new-audio-note { + padding: var(--size-4-3); + display: flex; + justify-content: space-between; +} + +.transcription-options-container-for-new-audio-note span { + margin-left: auto; + align-self: center; + margin-bottom: 12px; +} + +.transcription-options-container-for-new-audio-note input[type="checkbox"] { + margin-left: 16px; + margin-right: 16px; + margin-bottom: 12px; + align-self: center; +} + +.transcription-options-container-for-new-audio-note select { + margin-left: 0px; +} + +.modal-container .prompt .podcast-input-div { + margin: var(--size-4-3); + display: flex; +} + +.modal-container .prompt .podcast-input-div .span-podcast-input-text { + width: 200px; + align-self: center; +} + +.modal-container .prompt .podcast-input-div .podcast-search-input { + margin-left: auto; + width: -webkit-fill-available; + max-width: calc(100% - 153px); +} + +.modal-container .prompt .podcast-select-div { + margin: var(--size-4-3); + display: flex; +} + +.modal-container .prompt .podcast-select-div .span-podcast-select-text { + width: 200px; + align-self: center; +} + +.modal-container .prompt .podcast-select-div .select-podcast-results { + margin-left: auto; + width: -webkit-fill-available; + max-width: calc(100% - 153px); +} + +.modal-container .prompt .podcast-episode-select-div { + margin: var(--size-4-3); + display: flex; +} + +.modal-container + .prompt + .podcast-episode-select-div + .span-podcast-episode-select-text { + width: 200px; + align-self: center; +} + +.modal-container + .prompt + .podcast-episode-select-div + .select-podcast-episode-results { + margin-left: auto; + width: -webkit-fill-available; + max-width: calc(100% - 153px); +} + +/* END tabs*/ + +/* START DG Quick Audio Note */ +.dg-audio-note { + border: 1px solid var(--background-modifier-border); + padding: 10px; +} + +.dg-audio-note-title { + font-weight: 600; +} + +.dg-audio-note-audio { + margin-top: 10px; +} + +.modal-main-div { + display: flex; + justify-content: center; + flex-direction: column; +} + +input.title-input { + padding: 6px 5px; +} +.button-container { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.button-container button { + margin-right: 5px; + margin-left: 5px; +} + +.dg-audio-note-powered-by { + /* background-image: url("https://res.cloudinary.com/deepgram/image/upload/v1676406242/blog/DG-powered-by-logo-black-red-horizontal-rgb_wqhltl.svg"); */ + /* height: 20px; */ + width: auto; + + /* background-size: contain; */ + margin-left: auto; + /* background-repeat: no-repeat; + background-position: right; */ + display: flex; + justify-content: center; + align-items: center; +} +.dg-audio-note-logo.theme-light { + background-color: whitesmoke; + height: 20px; + width: auto; + margin-left: auto; +} + +.dg-audio-note-powered-by.hidden { + display: none; +} + +.dg-quick-note-modal { + --dialog-width: 75vw; + height: auto; + --dialog-max-height: 75vh; +} +.info svg { + width: 11px; + margin-left: 3px; +} +/* END DG Quick Audio Note */