Skip to content

Commit

Permalink
very very rough and wip timestamped transcriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
chhoumann committed Jul 31, 2024
1 parent 2973586 commit 3f67a69
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 43 deletions.
4 changes: 2 additions & 2 deletions src/API/API.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Episode } from "src/types/Episode";
import type { Episode } from "src/types/Episode";
import { formatSeconds } from "src/utility/formatSeconds";
import { IAPI } from "./IAPI";
import type { IAPI } from "./IAPI";
import {
currentEpisode,
currentTime,
Expand Down
48 changes: 43 additions & 5 deletions src/TemplateEngine.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { htmlToMarkdown, Notice } from "obsidian";
import type { Episode } from "src/types/Episode";
import Fuse from "fuse.js";
import { plugin } from "src/store";
import { downloadedEpisodes, plugin } from "src/store";
import { get } from "svelte/store";
import getUrlExtension from "./utility/getUrlExtension";
import encodePodnotesURI from "./utility/encodePodnotesURI";
import { isLocalFile } from "./utility/isLocalFile";

interface Tags {
[tag: string]: string | ((...args: unknown[]) => string);
Expand Down Expand Up @@ -115,19 +117,55 @@ export function NoteTemplateEngine(template: string, episode: Episode) {
return replacer(template);
}

export function TimestampTemplateEngine(template: string) {
export function TimestampTemplateEngine(template: string, range: TimestampRange) {
const [replacer, addTag] = useTemplateEngine();

addTag("time", (format?: string) =>
get(plugin).api.getPodcastTimeFormatted(format ?? "HH:mm:ss"),
formatTimestamp(range.start, format ?? "HH:mm:ss")
);
addTag("linktime", (format?: string) =>
get(plugin).api.getPodcastTimeFormatted(format ?? "HH:mm:ss", true),
formatTimestampWithLink(range.start, format ?? "HH:mm:ss")
);

addTag("timerange", (format?: string) =>
`${formatTimestamp(range.start, format ?? "HH:mm:ss")} - ${formatTimestamp(range.end, format ?? "HH:mm:ss")}`
);
addTag("linktimerange", (format?: string) =>
`${formatTimestampWithLink(range.start, format ?? "HH:mm:ss")} - ${formatTimestampWithLink(range.end, format ?? "HH:mm:ss")}`
);

return replacer(template);
}


function formatTimestamp(seconds: number, format: string): string {
const date = new Date(0);
date.setSeconds(seconds);
return date.toISOString().substr(11, 8); // This gives HH:mm:ss format
// If you need more flexible formatting, you might want to use a library like moment.js
}

function formatTimestampWithLink(seconds: number, format: string): string {
const time = formatTimestamp(seconds, format);
const api = get(plugin).api;
const episode = api.podcast;

if (!episode) {
return time;
}

const feedUrl = isLocalFile(episode)
? downloadedEpisodes.getEpisode(episode)?.filePath
: episode.feedUrl;

if (!feedUrl) {
return time;
}

const url = encodePodnotesURI(episode.title, feedUrl, seconds);
return `[${time}](${url.href})`;
}

export function FilePathTemplateEngine(template: string, episode: Episode) {
const [replacer, addTag] = useTemplateEngine();

Expand Down Expand Up @@ -244,4 +282,4 @@ function replaceIllegalFileNameCharactersInString(string: string) {
.replace(/[\\,#%&{}/*<>$'":@\u2023|\\.]/g, "") // Replace illegal file name characters with empty string
.replace(/\n/, " ") // replace newlines with spaces
.replace(" ", " "); // replace multiple spaces with single space to make sure we don't have double spaces in the file name
}
}
4 changes: 4 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ export default class PodNotes extends Plugin implements IPodNotes {
const cursorPos = editor.getCursor();
const capture = TimestampTemplateEngine(
this.settings.timestamp.template,
{
start: this.api.currentTime,
end: this.api.currentTime,
},
);

editor.replaceRange(capture, cursorPos);
Expand Down
151 changes: 117 additions & 34 deletions src/services/TranscriptionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { downloadEpisode } from "../downloadEpisode";
import {
FilePathTemplateEngine,
TranscriptTemplateEngine,
TimestampTemplateEngine,
} from "../TemplateEngine";
import type { Episode } from "src/types/Episode";
import type { Transcription } from "openai/resources/audio";

function TimerNotice(heading: string, initialMessage: string) {
let currentMessage = initialMessage;
Expand Down Expand Up @@ -119,8 +121,12 @@ export class TranscriptionService {
notice.update("Starting transcription...");
const transcription = await this.transcribeChunks(files, notice.update);

notice.update("Processing timestamps...");
const formattedTranscription =
this.formatTranscriptionWithTimestamps(transcription);

notice.update("Saving transcription...");
await this.saveTranscription(currentEpisode, transcription);
await this.saveTranscription(currentEpisode, formattedTranscription);

notice.stop();
notice.update("Transcription completed and saved.");
Expand Down Expand Up @@ -177,8 +183,8 @@ export class TranscriptionService {
private async transcribeChunks(
files: File[],
updateNotice: (message: string) => void,
): Promise<string> {
const transcriptions: string[] = new Array(files.length);
): Promise<Transcription> {
const transcriptions: Transcription[] = [];
let completedChunks = 0;

const updateProgress = () => {
Expand All @@ -190,40 +196,117 @@ export class TranscriptionService {

updateProgress();

await Promise.all(
files.map(async (file, index) => {
let retries = 0;
while (retries < this.MAX_RETRIES) {
try {
const result = await this.client.audio.transcriptions.create({
model: "whisper-1",
file,
for (const file of files) {
let retries = 0;
while (retries < this.MAX_RETRIES) {
try {
const result = await this.client.audio.transcriptions.create({
file: file,
model: "whisper-1",
response_format: "verbose_json",
timestamp_granularities: ["segment", "word"],
});
transcriptions.push(result);
completedChunks++;
updateProgress();
break;
} catch (error) {
retries++;
if (retries >= this.MAX_RETRIES) {
console.error(
`Failed to transcribe chunk after ${this.MAX_RETRIES} attempts:`,
error,
);
throw error;
}
await new Promise((resolve) => setTimeout(resolve, 1000 * retries)); // Exponential backoff
}
}
}

return this.mergeTranscriptions(transcriptions);
}

private mergeTranscriptions(transcriptions: Transcription[]): Transcription {
let mergedText = "";
const mergedSegments = [];
let timeOffset = 0;

transcriptions.forEach((transcription, index) => {
if (typeof transcription === "string") {
mergedText += (index > 0 ? " " : "") + transcription;
} else if (typeof transcription === "object" && transcription.text) {
mergedText += (index > 0 ? " " : "") + transcription.text;

// Assuming the transcription object has a 'segments' property
if (transcription.segments) {
for (const segment of transcription.segments) {
mergedSegments.push({
...segment,
start: segment.start + timeOffset,
end: segment.end + timeOffset,
});
transcriptions[index] = result.text;
completedChunks++;
updateProgress();
break;
} catch (error) {
retries++;
if (retries >= this.MAX_RETRIES) {
console.error(
`Failed to transcribe chunk ${index} after ${this.MAX_RETRIES} attempts:`,
error,
);
transcriptions[index] = `[Error transcribing chunk ${index}]`;
completedChunks++;
updateProgress();
} else {
await new Promise((resolve) =>
setTimeout(resolve, 1000 * retries),
); // Exponential backoff
}
}

timeOffset +=
transcription.segments[transcription.segments.length - 1].end;
}
}),
);
}
});

return transcriptions.join(" ");
return {
text: mergedText,
segments: mergedSegments,
// Add other properties as needed
};
}

private formatTranscriptionWithTimestamps(transcription: Transcription): string {
let formattedTranscription = "";
let currentSegment = "";
let segmentStart: number | null = null;
let segmentEnd: number | null = null;

transcription.segments.forEach((segment, index) => {
if (segmentStart === null) {
segmentStart = segment.start;
}
segmentEnd = segment.end;

if (index === 0 || segment.start - transcription.segments[index - 1].end > 1.5) {
// New segment
if (currentSegment) {
const timestampRange = {
start: segmentStart!,
end: segmentEnd!
};
const formattedTimestamp = TimestampTemplateEngine("**{{linktimerange}}**\n",
timestampRange
);
formattedTranscription += `${formattedTimestamp} ${currentSegment}\n\n`;
}
currentSegment = segment.text;
segmentStart = segment.start;
} else {
// Continuing segment
currentSegment += ` ${segment.text}`;
}

// Handle the last segment
if (index === transcription.segments.length - 1) {
const timestampRange = {
start: segmentStart!,
end: segmentEnd!
};
const formattedTimestamp = TimestampTemplateEngine(
this.plugin.settings.timestamp.template,
timestampRange
);
formattedTranscription += `${formattedTimestamp} ${currentSegment}`;
}
});

return formattedTranscription;
}

private async saveTranscription(
Expand Down Expand Up @@ -261,4 +344,4 @@ export class TranscriptionService {
throw new Error("Expected a file but got a folder");
}
}
}
}
4 changes: 4 additions & 0 deletions src/types/TimestampRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface TimestampRange {
start: number;
end: number;
}
4 changes: 2 additions & 2 deletions src/utility/isLocalFile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LocalEpisode } from "src/types/LocalEpisode";
import { Episode } from "src/types/Episode";
import type { LocalEpisode } from "src/types/LocalEpisode";
import type { Episode } from "src/types/Episode";

export function isLocalFile(ep: Episode): ep is LocalEpisode {
return ep.podcastName === "local file";
Expand Down

0 comments on commit 3f67a69

Please sign in to comment.