chore: Remove unused youtube summarizer
This commit is contained in:
1 parent
9627aaa31f
commit
4d3b6ec06d
1 file changed
-365
-365
@@ -1,365 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>YouTube Video Summarizer</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
color-scheme: light dark;
|
|
||||||
--bg: #fff;
|
|
||||||
--bg-alt: #f5f5f5;
|
|
||||||
--text: #000;
|
|
||||||
--text-muted: #666;
|
|
||||||
--border: #ddd;
|
|
||||||
--accent: #4285f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--bg: #1a1a1a;
|
|
||||||
--bg-alt: #2a2a2a;
|
|
||||||
--text: #fff;
|
|
||||||
--text-muted: #aaa;
|
|
||||||
--border: #444;
|
|
||||||
--accent: #5a9fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: "JetBrains Mono", monospace;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 2rem;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
transition:
|
|
||||||
background 0.2s,
|
|
||||||
color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
border: 3px solid var(--bg-alt);
|
|
||||||
border-top: 3px solid var(--accent);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#summary {
|
|
||||||
margin-top: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#summary :is(ul, ol) {
|
|
||||||
margin: 1rem 0;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#summary :is(ul, ol) :is(ul, ol) {
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#summary li {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#summary blockquote {
|
|
||||||
border-left: 3px solid var(--border);
|
|
||||||
padding-left: 1rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
#summary h2,
|
|
||||||
#summary h3 {
|
|
||||||
margin: 1.5rem 0 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background: #c33;
|
|
||||||
color: #fff;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="videoUrl" placeholder="https://www.youtube.com/watch?v=..." />
|
|
||||||
<textarea id="video" placeholder="Video transcript"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="summarizeBtn" style="display: none"></button>
|
|
||||||
|
|
||||||
<div id="loading" class="loading" style="display: none">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
Generating summary...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="error" class="error" style="display: none"></div>
|
|
||||||
|
|
||||||
<div id="summary"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
|
|
||||||
<script>
|
|
||||||
const videoUrlInput = document.getElementById("videoUrl");
|
|
||||||
const textInput = document.getElementById("text");
|
|
||||||
const summarizeBtn = document.getElementById("summarizeBtn");
|
|
||||||
const loadingDiv = document.getElementById("loading");
|
|
||||||
const errorDiv = document.getElementById("error");
|
|
||||||
const summaryDiv = document.getElementById("summary");
|
|
||||||
|
|
||||||
// Pre-fill with YouTube URL if available and auto-submit
|
|
||||||
if (window.env?.YOUTUBE_URL || window.env?.YOUTUBE_TRANSCRIPT) {
|
|
||||||
if (window.env?.YOUTUBE_URL) videoUrlInput.value = window.env.YOUTUBE_URL;
|
|
||||||
if (document.readyState === "loading") {
|
|
||||||
document.addEventListener("DOMContentLoaded", () => summarizeBtn.click());
|
|
||||||
} else {
|
|
||||||
summarizeBtn.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
summarizeBtn.addEventListener("click", async () => {
|
|
||||||
const url = videoUrlInput.value.trim();
|
|
||||||
const transcript = window.env?.YOUTUBE_TRANSCRIPT;
|
|
||||||
|
|
||||||
if (!transcript && !url) {
|
|
||||||
showError("Please enter a YouTube URL");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!transcript) {
|
|
||||||
// Validate YouTube URL
|
|
||||||
const youtubeUrlPattern = /^https?:\/\/(www\.)?youtube\.com\/watch\?v=.+$|^https?:\/\/youtu\.be\/.+$/;
|
|
||||||
if (!youtubeUrlPattern.test(url)) {
|
|
||||||
showError("Please enter a valid YouTube URL");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await summarize(url, transcript);
|
|
||||||
});
|
|
||||||
|
|
||||||
function showError(message) {
|
|
||||||
errorDiv.textContent = message;
|
|
||||||
errorDiv.style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideError() {
|
|
||||||
errorDiv.style.display = "none";
|
|
||||||
errorDiv.textContent = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function summarize(url, transcript) {
|
|
||||||
hideError();
|
|
||||||
summaryDiv.innerHTML = "";
|
|
||||||
loadingDiv.style.display = "block";
|
|
||||||
summarizeBtn.disabled = true;
|
|
||||||
|
|
||||||
const prompt = `
|
|
||||||
Give me keypoints of this video transcript, with enough context, also with quotes from the content.
|
|
||||||
Only the summary please.
|
|
||||||
Skip promotions.
|
|
||||||
Do not censor profanity.
|
|
||||||
Formatted in valid markdown without any wrappers or code blocks.
|
|
||||||
`;
|
|
||||||
|
|
||||||
let fullResponse = "";
|
|
||||||
const abortController = new AbortController();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await chatWithGemini({
|
|
||||||
prompt: prompt,
|
|
||||||
messages: [{ role: "user", content: transcript ?? url }],
|
|
||||||
model: "gemini-3-flash-preview",
|
|
||||||
onContent: (text) => {
|
|
||||||
fullResponse += text;
|
|
||||||
summaryDiv.innerHTML = markdownToHtml(fullResponse);
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
showError(`Error: ${err.message}`);
|
|
||||||
loadingDiv.style.display = "none";
|
|
||||||
summarizeBtn.disabled = false;
|
|
||||||
},
|
|
||||||
signal: abortController.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
loadingDiv.style.display = "none";
|
|
||||||
} catch (err) {
|
|
||||||
showError(`Error: ${err.message}`);
|
|
||||||
loadingDiv.style.display = "none";
|
|
||||||
} finally {
|
|
||||||
summarizeBtn.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const md = markdownit({ html: false });
|
|
||||||
function markdownToHtml(markdown) {
|
|
||||||
return md.render(markdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function chatWithGemini({ prompt, onContent, onError, messages, signal, apiKey = window.env.GEMINI_API_KEY, model }) {
|
|
||||||
const contents = messages.map((msg, i, arr) => {
|
|
||||||
const role = msg.role === "assistant" ? "model" : "user";
|
|
||||||
let parts = [{ text: msg.content }];
|
|
||||||
|
|
||||||
// Add YouTube URLs as file data for the final user message
|
|
||||||
const isLastMessage = i === arr.length - 1;
|
|
||||||
if (isLastMessage && role === "user") {
|
|
||||||
const youtubeUrlPattern = /https?:\/\/(www\.)?youtube\.com\/watch\?v=[\w-]+|https?:\/\/youtu\.be\/[\w-]+/g;
|
|
||||||
const youtubeUrls = msg.content.match(youtubeUrlPattern) || [];
|
|
||||||
const fileParts = youtubeUrls.map((url) => ({ file_data: { file_uri: url } }));
|
|
||||||
parts = [...parts, ...fileParts];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
role,
|
|
||||||
parts,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?key=${apiKey}`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
...(prompt ? { system_instruction: { parts: [{ text: prompt }] } } : {}),
|
|
||||||
contents,
|
|
||||||
generationConfig: {
|
|
||||||
thinkingConfig: { thinkingLevel: "low" },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
await onError?.(new Error(`Gemini API Error ${response.status}: ${text}`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = "";
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true });
|
|
||||||
|
|
||||||
let pos = 0;
|
|
||||||
while (pos < buffer.length) {
|
|
||||||
// Find start of JSON object
|
|
||||||
const start = buffer.indexOf("{", pos);
|
|
||||||
if (start === -1) break;
|
|
||||||
|
|
||||||
// Find matching closing brace
|
|
||||||
let depth = 0;
|
|
||||||
let inString = false;
|
|
||||||
let escaped = false;
|
|
||||||
let end = -1;
|
|
||||||
|
|
||||||
for (let i = start; i < buffer.length; i++) {
|
|
||||||
const ch = buffer[i];
|
|
||||||
|
|
||||||
if (escaped) {
|
|
||||||
escaped = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ch === "\\") {
|
|
||||||
escaped = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ch === '"') {
|
|
||||||
inString = !inString;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inString) {
|
|
||||||
if (ch === "{") depth++;
|
|
||||||
else if (ch === "}") {
|
|
||||||
depth--;
|
|
||||||
if (depth === 0) {
|
|
||||||
end = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end === -1) {
|
|
||||||
// Incomplete JSON, keep it in buffer
|
|
||||||
buffer = buffer.slice(start);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and process JSON
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(buffer.slice(start, end + 1));
|
|
||||||
const parts = json.candidates?.[0]?.content?.parts || [];
|
|
||||||
const text = parts
|
|
||||||
.filter((p) => p.text)
|
|
||||||
.map((p) => p.text)
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
if (text) await onContent(text);
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore parse errors
|
|
||||||
}
|
|
||||||
|
|
||||||
pos = end + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep only unparsed remainder
|
|
||||||
buffer = buffer.slice(pos);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err.name !== "AbortError") await onError?.(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in new issue
Block a user