Skip to content

Commit

Permalink
highlight current utterances
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbos committed Aug 25, 2023
1 parent 6ba0b3a commit d835589
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 50 deletions.
3 changes: 2 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ model AiShowNote {
show Show @relation(fields: [show_number], references: [number])
show_number Int @unique
title String
description String @db.VarChar(750)
description String @db.VarChar(1500)
summary AiSummaryEntry[]
tweets AiTweet[]
links Link[]
Expand Down Expand Up @@ -183,6 +183,7 @@ model Link {
id Int @id @default(autoincrement())
name String
url String
timestamp String?
showNote Int
aiShowNote AiShowNote @relation(fields: [showNote], references: [id], onDelete: Cascade)
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/player/Player.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
src={$player.current_show?.url}
bind:this={$player.audio}
preload="metadata"
bind:currentTime={$player.currentTime}
/>
<media-control-bar class="media-bar">
<div class="media-controls">
Expand Down
65 changes: 59 additions & 6 deletions src/lib/transcript/Transcript.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { AINoteWithFriends, TranscriptWithUtterances } from '$server/ai/queries';
import { getSlimUtterances } from '$server/transcripts/utils';
import { SlimUtterance, getSlimUtterances } from '$server/transcripts/utils';
import format_time, { tsToS } from '$utilities/format_time';
import { Prisma } from '@prisma/client';
import { time } from 'console';
Expand All @@ -9,18 +9,66 @@
const slim_transcript = getSlimUtterances(transcript.utterances, 1).filter(
(utterance) => utterance.speakerId !== 99
);
console.log(aiShowNote);
import { player } from '$state/player';
$: currentUtterance = slim_transcript.find((utterance, index) => {
const nextUtteranceStart = slim_transcript[index + 1]?.start || utterance.end;
return $player.currentTime >= utterance.start && $player.currentTime <= nextUtteranceStart;
});
const words = transcript.utterances
.map((utt) => utt.words)
.flat()
.sort((a, b) => a.start - b.start);
$: currentWordIndex = words.findIndex((word, index, words) => {
const nextWordStart = words[index + 1]?.start || word.end;
const currentWord = $player.currentTime >= word.start && $player.currentTime <= nextWordStart;
return currentWord;
});
let wordCount = 3;
$: highlight_words = words
.slice(
Math.floor(currentWordIndex / wordCount) * wordCount,
Math.floor(currentWordIndex / wordCount) * wordCount + wordCount
)
.map((word) => word.word)
.join(' ');
</script>

<p><mark>{highlight_words}</mark></p>
<h2>{$player.current_show?.title}</h2>
<h2>{$player.currentTime}</h2>
<p>{words[currentWordIndex]?.word}</p>
<p>{currentUtterance?.transcript}</p>
<p>{currentUtterance?.utteranceIndex}</p>

<div>
<ul>
{#each aiShowNote?.summary || [] as summary}
<li>
{summary.text} - {tsToS(summary.time)}
</li>
{/each}
</ul>
</div>

<div>
{#each slim_transcript as utterance, i}
{@const summary = aiShowNote?.summary?.find((summary) => {
const timestamp = tsToS(summary.time);
if (timestamp >= utterance.start && timestamp <= utterance.end) {
const lastUtterance = slim_transcript[i - 1];
const timestamp_between_last_and_next =
lastUtterance && timestamp >= lastUtterance.end && timestamp <= utterance.start;
if (
(timestamp >= utterance.start && timestamp <= utterance.end) ||
timestamp_between_last_and_next
) {
return summary;
}
})}
<div>
<div class={utterance === currentUtterance ? 'active' : ''}>
{#if summary}
<h4>{summary.text}</h4>
{/if}
Expand All @@ -29,9 +77,14 @@
utterance.start
)}</strong
>
{utterance.start}
{utterance.end}

<p>{utterance.transcript}</p>
</div>
{/each}
</div>

<style>
.active {
background: red;
}
</style>
22 changes: 10 additions & 12 deletions src/routes/shows/[show_number]/[slug]/[[tab]]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import Icon from '$lib/Icon.svelte';
import NewsletterForm from '$lib/NewsletterForm.svelte';
import Transcript from '$lib/transcript/Transcript.svelte';
console.log($page);
export let data;
$: ({ show } = data);
Expand Down Expand Up @@ -62,19 +61,18 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<section class="layout full" on:click|preventDefault={handleClick}>
<div class="main">
{#if $page.params.tab === 'transcript'}
<Transcript aiShowNote={show.aiShowNote} transcript={show.transcript} />
{:else}
{#if $page.params.tab === 'transcript'}
<Transcript aiShowNote={show.aiShowNote} transcript={show.transcript} />
{:else}
<div class="main">
{@html show.show_notes}
{/if}
</div>

<div class="sidebar">
<div class="sticky">
<NewsletterForm />
<div class="sidebar">
<div class="sticky">
<NewsletterForm />
</div>
</div>
</div>
</div>
{/if}
</section>

<style lang="postcss">
Expand Down
58 changes: 40 additions & 18 deletions src/server/ai/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type CreateChatCompletionRequest,
OpenAIApi
} from 'openai';
import { createCondensePrompt, summarizePrompt } from './prompts';
import { createCondensePrompt, summarizePrompt, summarizePrompt2 } from './prompts';
import {
SlimUtterance,
TranscribedShow,
Expand All @@ -18,10 +18,11 @@ import { exists } from '$utilities/file_utilities/exists';
import wait from 'waait';
import { Prisma } from '@prisma/client';

export const TOKEN_LIMIT = 16000;
export const TOKEN_LIMIT = 7000;
export const COMPLETION_TOKEN_IDEAL = 1500; // how many tokens we should reserve to the completion - otherwise the responses are poor quality
const TOKEN_INPUT_LIMIT = TOKEN_LIMIT - COMPLETION_TOKEN_IDEAL;
export const MODEL = 'gpt-3.5-turbo-16k'; // Was gpt-4 before token limit was increased
// export const MODEL = 'gpt-3.5-turbo-16k'; // Was gpt-4 before token limit was increased
export const MODEL = 'gpt-4';
export const EMBEDDING_MODEL = 'text-embedding-ada-002';
export const CONDENSE_THRESHOLD = 100;
const configuration = new Configuration({
Expand Down Expand Up @@ -49,33 +50,37 @@ export async function condense(
console.log(`Token input limit: ${TOKEN_INPUT_LIMIT}`);
const factorSmaller = 1 - TOKEN_INPUT_LIMIT / inputTokensLength;
console.log(`Factor smaller: ${factorSmaller}`);
// Group utterances
const slimUtterances = getSlimUtterances(show.utterances, show.number, false);
console.log(slimUtterances.length, show.utterances.length);
// Split the transcript into hunks
const utteranceFuncs = show.utterances.map((utterance, index) => {
const utteranceFuncs = slimUtterances.map((utterance, index) => {
return async function getCondenseUtterance(): Promise<SlimUtterance> {
// Wait a random amount of time to avoid rate limiting. Between 0 and 10 seconds
const waitTime = Math.floor(Math.random() * 10000);
await wait(waitTime);
console.time(`Condensing ${index} of ${show.utterances.length}`);
console.time(`Condensing ${index} of ${slimUtterances.length}`);
const size = encode(utterance.transcript).length;
// If under 50 chars, leave it alone. Return it via a promise
if (utterance.transcript.length < CONDENSE_THRESHOLD) {
console.log(`Skipping condensing of ${index} of ${show.utterances.length}`);
console.log(`Skipping condensing of ${index} of ${slimUtterances.length}`);
return Promise.resolve(utterance);
}
// If it's over 50 chars, condense it via openAI
const input: CreateChatCompletionRequest = {
model: MODEL,
model: 'gpt-3.5-turbo', // Summarize
messages: [
// { "role": "system", "content": `You are a helpful service that condenses text.` },
{ role: 'system', content: createCondensePrompt(`${Math.floor(factorSmaller * 100)}%`) },
{ role: 'user', content: utterance.transcript }
]
// "max_tokens": size * factorSmaller,
],
max_tokens: Math.round(size * factorSmaller)
// "temperature": 0.3
};
console.log(`Condensing`, index, `of`, show.utterances.length);
console.log(`Condensing`, index, `of`, slimUtterances.length);
const completion = await openai.createChatCompletion(input).catch((err) => {
// Catch the error in transcribing so we can at least save the utterance without the condensed transcript
console.log(`❗️ Error Condensing`, index, `of`, show.utterances.length);
console.log(`❗️ Error Condensing`, index, `of`, slimUtterances.length);
console.dir(err.response.data);
console.dir(err.response.headers);
});
Expand All @@ -84,17 +89,24 @@ export async function condense(
if (condensed) {
utterance.condensedTranscript = condensed;
}
if (condensed.length > utterance.transcript.length) {
console.log(`Condensed transcript is longer than original transcript.
Condensed: ${condensed}
Original: ${utterance.transcript}
`);
return Promise.resolve(utterance);
}
const smaller = encode(condensed || '').length;
const original = encode(utterance.transcript).length;
console.log(
index,
'/',
show.utterances.length,
slimUtterances.length,
`Condensed from ${original} to ${smaller} tokens - ${Math.round(
(smaller / original) * 100
)}% of original`
);
console.timeEnd(`Condensing ${index} of ${show.utterances.length}`);
console.timeEnd(`Condensing ${index} of ${slimUtterances.length}`);
// Return the modifined utterance
return utterance;
};
Expand All @@ -109,7 +121,7 @@ export async function condense(
})
.map((result) => result.value);

console.log(`Finished condensng ${show.number}`);
console.log(`Finished Condensing ${show.number}`);
return utterances;
}

Expand Down Expand Up @@ -141,23 +153,33 @@ export async function generate_ai_notes(
utterances: slimUtterance
});
const condensedTranscript = formatAsTranscript(slimUtterancesWithCondensed);
const links = show.show_notes
.match(/\[([^\[]+)\](\(.*\))/g)
.filter((link) => link.includes('http'));

const input: CreateChatCompletionRequest = {
model: MODEL,
// model: 'gpt-4',
model: 'gpt-3.5-turbo-16k',
temperature: 0,
messages: [
{ role: 'system', content: 'You summarize web development podcasts' },
{
role: 'system',
content: 'You summarize web development podcasts. Your tone is casual and humorous'
},
{
role: 'user',
content: 'Syntax is a podcast about web development. Available at https://Syntax.fm'
},
{ role: 'user', content: `This episode is #${show.number} entitled ${show.title}` },
{ role: 'user', content: summarizePrompt },
{ role: 'user', content: summarizePrompt2 },
{ role: 'user', content: condensedTranscript }
]
};

console.log(`Creating AI notes for ${show.number}`);
const completion = await openai.createChatCompletion(input);
const completion = await openai.createChatCompletion(input).catch((err) => {
console.dir(err.response.data.error, { depth: null });
});
const maybeJSON = completion.data.choices.at(0)?.message?.content;
console.log(maybeJSON);
const parsed = JSON.parse(maybeJSON || '') as AIPodcastSummaryResponse;
Expand Down
46 changes: 40 additions & 6 deletions src/server/ai/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ export const misspellings = `
Please replace all instances of the following words with the correct spelling:
1. Replace "Century" with "Sentry"
2. Replace "Cintax" and "Cintacs" with "Syntax"
2. Replace "Cintax", "Cintech", and "Cintacs" with "Syntax"
`;

export const summarizePrompt = `Summarize the provided podcast transcript into very succinct bullet points, each containing just a few words. The bullet points should correspond to sections or topics discussed in the podcast with points at least every 3-5 minutes. For each bullet point, you may also provide a longer 1-2 sentence summary of the topic if necessary, which may also include the host's opinions, names and thoughts. Do not skip topics.
export const summarizePrompt = `Summarize the provided podcast transcript into very succinct bullet points, each containing just a few words. The bullet points should correspond to sections, questions or topics discussed in the podcast with points at least every 3-5 minutes. For each bullet point, you may also provide a longer 1-2 sentence summary of the topic if necessary, which may also include the host's opinions, names and thoughts. Do not skip topics.
Remember, the key here is to read through the transcript carefully, identify all points, topics, questions and even banter, and then condense each one into a very brief, clear statement. It's also important to include timestamps if they're provided in the transcript, as they can help give a sense of the flow and structure of the podcast.
Expand All @@ -23,8 +23,7 @@ Additionally, Please create the following for this podcast episode:
3. 6-7 tweets about this podcast that can be tweeted by listeners of the show. Some examples are "This is a fantastic episode about ___", or "I really enjoyed the part about ____". They should mention @syntaxfm. Do not add any hashtags or emojis. Use exclamation points sparingly.
4. 3-4 hashtags that categorize the episode. these will be used as topic tags on the website.
5. A summary of the podcast into a title. It should be catchy, slightly click-baity, mention a few topics covered and make the listener want to stop what they are doing and watch it.
6. Keep track of any links or websites mentioned in the podcast. These will be used as links on the website. If you can't find the link, provide only the name of the website.
7. tally the time each speaker speaks by using the provided timestamps. Do not guess.
6. Keep track of any links or websites mentioned in the podcast. These will be used as links on the website. If we don't explicitly mention a website, but instead mention a company, service or resource, find the associated website. Provide the timestamp of when the link is first mentioned. Do not skip links.
8. Provide a list of guest names. If the transcript doesn't include their real name, try to infer it.
${misspellings}
Expand All @@ -39,8 +38,43 @@ Return each of these things in JSON format that looks like this:
"notes": "...",
"summary": [{"time": "02:33", "text": "...", "description": "..."}],
"tweets": ["..."],
"listener_tweets": ["..."],
"topics": ["...", "..."],
"links": [{ "name": "Name of link", "url": "https://example.com"}, "..."],
"links": [{ "name": "Name of link", "url": "https://example.com", "timestamp": "02:45" }],
"guests": ["..."]
}`;

export const summarizePrompt2 = `Summarize the provided podcast transcript into very succinct bullet points, each containing just a few words. The bullet points should correspond to sections, questions or topics discussed in the podcast with points at least every 3-5 minutes.
summary: For each bullet point, provide:
1. time: A timestamp of when the topic is first mentioned
2. text: A short, clear, and concise summary of the topic. This should be a short, catchy, and interesting description. It should provoke the listener to stop what they are doing and listen to the podcast. Leave off any intro wording such as "Discussion on" and "Explanation of"
3. description: 1 to 3 words MAX summarizing the previously mentioned text. Do not provide more than 3 words. If you provide
Remember, the key here is to read through the transcript carefully, identify all points, topics, questions and even banter, and then condense each one into a very brief, clear statement. It's also important to include timestamps if they're provided in the transcript, as they can help give a sense of the flow and structure of the podcast. Do not leave out any topics.
Additionally, Please create the following for this podcast episode:
1. description: 1-2 sentence description about what is covered in the podcast. This should be a short, catchy, and interesting description. It should provoke the listener to stop what they are doing and listen to the podcast.
2. title: A summary of the podcast into a title. It should be catchy, slightly click-baity, mention a few topics covered and make the listener want to stop what they are doing and watch it.
3. tweets: 6-7 tweets for this podcast episode. These tweets should be short, catchy, and interesting. They should provoke the twitter user to respond an join in the conversation. Do not add any hashtags or emojis. Use exclamation points sparingly.
4. topics: 3-4 hashtags that categorize the episode. these will be used as topic tags on the website.
6. links: Keep track of any links or websites mentioned in the podcast. These will be used as links on the website. If we don't explicitly mention a website, but instead mention a company, service or resource, find the associated website. Provide the timestamp of when the link is first mentioned. Do not skip links.
8. guests: Provide a list of guest names. If the transcript doesn't include their real name, try to infer it.
${misspellings}
If you, the AI, have feedback or clarifications on your response, please put them in the notes section.
Return each of these things in JSON format that looks like this:
{
"summary": [{"time": "02:33", "text": "...", "description": "..."}],
"description": "...",
"title": "...",
"notes": "...",
"tweets": ["..."],
"topics": ["...", "..."],
"links": [{ "name": "Name of link", "url": "https://example.com", "timestamp": "02:45" }],
"guests": ["..."]
}`;
7 changes: 4 additions & 3 deletions src/server/ai/requestHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { error, type RequestEvent } from '@sveltejs/kit';
import { ai_note_with_transcript, transcript_with_utterances } from './queries';
import { transcript_with_utterances } from './queries';
import { generate_ai_notes } from './openai';

export async function aiNoteRequestHandler({ request, locals }: RequestEvent) {
Expand Down Expand Up @@ -42,7 +42,7 @@ export async function aiNoteRequestHandler({ request, locals }: RequestEvent) {
}
},
title: result.title,
description: result.description,
description: result.description || result.short_description,
summary: {
create: result.summary
},
Expand All @@ -55,7 +55,8 @@ export async function aiNoteRequestHandler({ request, locals }: RequestEvent) {
links: {
create: result.links.map((link) => ({
name: link.name,
url: link.url
url: link.url,
timestamp: link.timestamp
}))
}
}
Expand Down
Loading

0 comments on commit d835589

Please sign in to comment.