Created
September 23, 2023 17:31
-
-
Save Explosion-Scratch/e8857930850ff5b8b963a05c40fea2fb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Bard | |
// @version 0.1 | |
// @description try to take over the world! | |
// @author You | |
// @match https://*.youtube.com/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=textarea.online | |
// @grant GM_xmlhttpRequest | |
// @grant unsafeWindow | |
// ==/UserScript== | |
(async function () { | |
"use strict"; | |
console.log("ACTIVATED - Bard running"); | |
let captions = {}; | |
let captionText = ""; | |
let elements = {}; | |
let addEl = once(addElement); | |
interceptXHR((response, url, method, body) => { | |
if (url.includes("timedtext")) { | |
captions = JSON.parse(response); | |
captionText = captions.events | |
.filter((i) => i.segs) | |
.map((i) => | |
i.segs | |
.map((j) => j.utf8) | |
.filter((j) => j.trim().length) | |
.join(" ") | |
) | |
.join(" ") | |
.replace(/\s+/g, " "); | |
unsafeWindow.captions = captions; | |
if (elements.textarea) { | |
elements.textarea.value = captionText; | |
} | |
addEl(captionText, elements); | |
elements.button.onclick = () => summarize(elements, b, captionText); | |
} | |
}, unsafeWindow); | |
await new Promise((r) => r()); | |
console.log(BardBot); | |
let b = new BardBot({ xmlhttprequest: GM_xmlhttpRequest }); | |
unsafeWindow.bard = b; | |
console.log(unsafeWindow.bard, unsafeWindow, unsafeWindow.title); | |
return; | |
console.log("Working: ", b); | |
let a = await b.doSendMessage({ prompt: "list the last 3 presidents" }); | |
console.log("Response: ", a); | |
})(); | |
async function summarize(elements, bot, captionText) { | |
elements.button.innerText = "Summarizing..."; | |
const prompt = `Summarize the following transcript from a video called "${ | |
unsafeWindow.document.querySelector("#title:has(h1)").innerText | |
}" in a few short bullet points.\nTranscript:\n${captionText}`; | |
console.log("Summarizing", prompt, bot); | |
let r = await bot.query({ prompt }); | |
console.log("Got output:", r); | |
let text = r.text; | |
let style = elements.style.innerHTML; | |
elements.div.innerHTML = ` | |
<h3>Transcript</h3> | |
<div class="result"></div> | |
<style></style> | |
`; | |
elements.style = elements.div.querySelector("style"); | |
elements.output = elements.div.querySelector(".result"); | |
elements.style.innerHTML = style; | |
elements.output.innerText = text; | |
} | |
async function addElement(captions, elements) { | |
let div = document.createElement("div"); | |
div.innerHTML = ` | |
<h3>Summarize YouTube video</h3> | |
<span class='desc'>Make sure that the correct captions track has been selected as this is used for summarization</span> | |
<i><b>Current captions</b></i> | |
<textarea></textarea> | |
<button>Summarize</button> | |
<style></style> | |
`; | |
div.id = "yt_summarize_userscript"; | |
elements.textarea = div.querySelector("textarea"); | |
elements.button = div.querySelector("button"); | |
elements.style = div.querySelector("style"); | |
elements.div = div; | |
elements.style.innerHTML = | |
`h3 { | |
font-size: 1.2em; | |
width: 100%; | |
text-align: center; | |
} | |
.desc { | |
font-size: 0.8em; | |
color: gray; | |
margin-top: 0.5em; | |
margin-bottom: 1em; | |
font-style: italic; | |
text-align: center; | |
display: block; | |
width: 100%; | |
text-align: left; | |
} | |
textarea { | |
width: 100%; | |
height: 100px; | |
font-size: .7em; | |
margin-bottom: 1em; | |
resize: none; | |
border-radius: 5px; | |
border: 1px solid gray; | |
padding: .3em; | |
box-sizing: border-box; | |
background-color: white; | |
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); | |
} | |
button { | |
padding: .5em 1em; | |
border-radius: 5px; | |
border: 1px solid gray; | |
background-color: black; | |
font-size: .7em; | |
color: white; | |
font-weight: bold; | |
cursor: pointer; | |
font-size: 1em; | |
text-transform: uppercase; | |
width: 100%; | |
display: block; | |
text-align: center; | |
transition: all .3s ease-in-out; | |
} | |
button:hover { | |
background-color: white; | |
color: black; | |
}`.replace(/\n(.+)\{/gi, `\n#${div.id} $1 {`) + | |
`\n\n#${div.id}{margin: 30px 0; border-radius: 6px; border: 1px dashed gray; display: flex; justify-content: center; flex-direction: column; font-size: 15px; background: white; padding: 20px 40px;}`; | |
console.log(elements.style.innerHTML); | |
elements.textarea.value = captions; | |
let ref = await waitForElement("#bottom-row", { | |
document: unsafeWindow.document, | |
}); | |
ref.style.display = "block"; | |
console.log(ref, unsafeWindow.document); | |
ref.insertAdjacentElement("afterbegin", div); | |
} | |
async function waitForElement( | |
selector, | |
{ checkInterval = 300, timeout = 30000, document = window.document } = {} | |
) { | |
const startTime = Date.now(); | |
return new Promise((resolve, reject) => { | |
const intervalId = setInterval(() => { | |
const element = document.querySelector(selector); | |
if (element) { | |
clearInterval(intervalId); | |
resolve(element); | |
} else if (Date.now() - startTime > timeout) { | |
clearInterval(intervalId); | |
reject( | |
new Error(`Timeout waiting for element with selector ${selector}`) | |
); | |
} | |
}, checkInterval); | |
}); | |
} | |
function once(fn) { | |
let called = false; | |
return function (...args) { | |
if (!called) { | |
called = true; | |
return fn(...args); | |
} | |
}; | |
} | |
class BardBot { | |
/** | |
* The conversation context. | |
* @type {Object} | |
*/ | |
conversationContext = undefined; | |
/** | |
* The logging function. | |
* @type {Function} | |
*/ | |
logFunc = undefined; | |
/** | |
* Creates a new instance of the BardBot class. | |
* @param {Object} options - The options for the BardBot instance. | |
* @param {Function} [options.log] - The logging function. | |
* @param {Object} [options.context] - The conversation context. | |
* @param {Array} [options.context.ids] - The conversation context IDs. | |
* @param {Object} [options.context.session] - The conversation context session. | |
* @param {XMLHttpRequest} options.xmlhttprequest - The XMLHttpRequest instance. | |
* @returns {BardBot} - The new BardBot instance. | |
*/ | |
constructor({ log, context: { ids, session } = {}, xmlhttprequest }) { | |
this.xmlhttprequest = xmlhttprequest; | |
if (!xmlhttprequest) { | |
this.xmlhttprequest = window.XMLHttpRequest; | |
} | |
if (ids && session) { | |
this.setContext({ ids, session }); | |
} | |
if (log) { | |
this.logFunc = log; | |
} | |
return this; | |
} | |
/** | |
* Sets the conversation context. | |
* @param {Object} options - The conversation context options. | |
* @param {Array} options.ids - The conversation context IDs. | |
* @param {Object} options.session - The conversation context session. | |
*/ | |
setContext({ ids, session }) { | |
this.conversationContext = { | |
ids, | |
requestParams: session, | |
}; | |
} | |
/** | |
* Gets the conversation context. | |
* @returns {Object} - The conversation context. | |
* @property {Array} ids - The conversation context IDs. | |
* @property {Object} session - The conversation context session. | |
*/ | |
getContext() { | |
return { | |
ids: this.conversationContext.ids, | |
session: this.conversationContext.requestParams, | |
}; | |
} | |
/** | |
* Queries the Bard chatbot. | |
* @param {Object} options - The query options. | |
* @param {Function} [options.onEvent] - The event function. | |
* @param {Boolean} [options.log] - Whether to log the query. | |
* @param {Object} [options.context] - The conversation context. | |
* @param {Object} options.prompt - The prompt for the query. | |
* @param {AbortSignal} [options.signal] - The abort signal for the query. | |
* @returns {Object} - The response from the Bard chatbot. | |
* @property {String} text - The response text. | |
* @property {Array} ids - The conversation context IDs. | |
* @property {Array} responses - The response objects. | |
* @property {Array} searches - The search objects. | |
* @property {Array} payload - The payload objects. | |
*/ | |
async query(params) { | |
params = { | |
onEvent: () => {}, | |
...params, | |
}; | |
if (params.signal) { | |
this.signal = params.signal; | |
} | |
if (params.log) { | |
this.logFunc = params.log; | |
} | |
if (params.context) { | |
this.conversationContext = params.context; | |
} | |
if (!this.conversationContext) { | |
this.conversationContext = { | |
requestParams: await this.#getParams(), | |
contextIds: ["", "", ""], | |
}; | |
} | |
const { requestParams, contextIds } = this.conversationContext; | |
if ( | |
!( | |
this.conversationContext.requestParams && | |
this.conversationContext.contextIds | |
) | |
) { | |
throw new Error("Context invalid"); | |
} | |
/*const resp = await fetch( | |
'https://bard.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?bl=' + | |
requestParams.blValue + | |
'&_reqid=' + | |
generateReqId() + | |
'&rt=c', | |
{ | |
method: 'POST', | |
signal: params.signal, | |
body: new URLSearchParams({ | |
at: requestParams.atValue, | |
'f.req': JSON.stringify([null, `[[${JSON.stringify(params.prompt)}],null,${JSON.stringify(contextIds)}]`]), | |
}), | |
}, | |
).then((res) => res.text())*/ | |
const resp = await new Promise((resolve, reject) => { | |
this.xmlhttprequest({ | |
method: "POST", | |
url: | |
"https://bard.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate?bl=" + | |
requestParams.blValue + | |
"&_reqid=" + | |
this.#generateReqId() + | |
"&rt=c", | |
data: new URLSearchParams({ | |
at: requestParams.atValue, | |
"f.req": JSON.stringify([ | |
null, | |
`[[${JSON.stringify(params.prompt)}],null,${JSON.stringify( | |
contextIds | |
)}]`, | |
]), | |
}).toString(), | |
headers: { | |
"Content-Type": "application/x-www-form-urlencoded", | |
}, | |
responseType: "text", | |
onload: function (response) { | |
resolve(response.responseText); | |
}, | |
onerror: function (response) { | |
reject(new Error("Request failed")); | |
}, | |
}); | |
}); | |
const { text, ids, ...res } = this.#parseResponse(resp); | |
this.conversationContext.contextIds = ids; | |
params.onEvent({ | |
type: "UPDATE_ANSWER", | |
data: { text }, | |
}); | |
params.onEvent({ type: "DONE" }); | |
return { text, ids, ...res }; | |
} | |
/** | |
* Sets the conversation context IDs. | |
* @param {Array} ids - The conversation context IDs. | |
*/ | |
setIds(ids) { | |
this.conversationContext.ids = ids; | |
} | |
/** | |
* Resets the conversation context. | |
*/ | |
resetConversation() { | |
this.conversationContext = undefined; | |
} | |
/** | |
* Extracts a value from HTML. | |
* @private | |
* @param {String} thing - The value to extract. | |
* @param {String} html - The HTML to extract the value from. | |
* @returns {String} - The extracted value. | |
*/ | |
#extractFromHTML(thing, html) { | |
const regex = new RegExp(`"${thing}":"([^"]+)"`); | |
const match = regex.exec(html); | |
return match?.[1]; | |
} | |
/** | |
* Gets the parameters for the Bard chatbot. | |
* @private | |
* @returns {Object} - The parameters for the Bard chatbot. | |
* @property {String} atValue - The at value for the Bard chatbot (session token or something). | |
* @property {String} blValue - The bl value for the Bard chatbot (bot version). | |
*/ | |
async #getParams() { | |
const html = await this.#fetchHtml("https://bard.google.com/faq"); | |
const atValue = this.#extractFromHTML("SNlM0e", html); | |
const blValue = this.#extractFromHTML("cfb2h", html); | |
return { atValue, blValue }; | |
} | |
/** | |
* Logs a message. | |
* @private | |
* @param {...*} args - The arguments to log. | |
*/ | |
#log(...args) { | |
if (this.logFunc) { | |
this.logFunc(...a); | |
} | |
} | |
/** | |
* Parses the response from the Bard chatbot. | |
* @private | |
* @param {String} resp - The response from the Bard chatbot. | |
* @returns {Object} - The parsed response from the Bard chatbot. | |
* @property {String} text - The response text. | |
* @property {Array} ids - The conversation context IDs. | |
* @property {Array} responses - The response objects. | |
* @property {Array} searches - The search objects. | |
* @property {Array} payload - The payload objects. | |
*/ | |
#parseResponse(resp) { | |
const data = JSON.parse(resp.split("\n")[3]); | |
const payload = JSON.parse(data[0][2]); | |
if (!payload) { | |
throw new Error("Failed to access Bard"); | |
} | |
this.#log("PAYLOAD", payload); | |
const text = payload[0][0]; | |
return { | |
text, | |
ids: [...payload[1], payload[4][0][0]], | |
responses: [ | |
{ ids: [...payload[1], payload[4][0][0]], text: payload[0][0] }, | |
...payload[4].map((i) => ({ | |
ids: [...payload[1], i[0]], | |
text: i[1][0], | |
})), | |
], | |
searches: payload[2].map((i) => i[0]), | |
payload, | |
}; | |
} | |
/** | |
* Generates a request ID. | |
* @private | |
* @returns {Number} - The generated request ID. | |
*/ | |
#generateReqId() { | |
return Math.floor(Math.random() * 900000) + 100000; | |
} | |
/** | |
* Fetches the HTML for a given URL. | |
* @private | |
* @param {String} url - The URL to fetch the HTML from. | |
* @returns {Promise<String>} - A promise that resolves to the HTML for the given URL. | |
*/ | |
async #fetchHtml(url) { | |
return new Promise((resolve, reject) => { | |
this.xmlhttprequest({ | |
method: "GET", | |
url: url, | |
onload: (response) => { | |
if (response.status === 200) { | |
resolve(response.responseText); | |
} else { | |
reject( | |
new Error( | |
`Failed to fetch HTML from ${url}. Status code: ${response.status}` | |
) | |
); | |
} | |
}, | |
onerror: (error) => { | |
reject( | |
new Error(`Failed to fetch HTML from ${url}. Error: ${error}`) | |
); | |
}, | |
}); | |
}); | |
} | |
} | |
function interceptXHR(callback, win) { | |
const originalXHR = win.XMLHttpRequest; | |
function newXHR() { | |
const xhr = new originalXHR(); | |
const originalOpen = xhr.open; | |
xhr.open = function (method, url) { | |
this.url = url; | |
this.method = method; | |
return originalOpen.apply(this, arguments); | |
}; | |
xhr.addEventListener("readystatechange", function () { | |
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) { | |
callback(xhr.responseText, xhr.url, xhr.method, xhr.requestBody); | |
} | |
}); | |
const originalSend = xhr.send; | |
xhr.send = function (body) { | |
this.requestBody = body; | |
return originalSend.apply(this, arguments); | |
}; | |
return xhr; | |
} | |
win.XMLHttpRequest = newXHR; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment