diff --git a/app/main.py b/app/main.py index c1f5d58..1628239 100644 --- a/app/main.py +++ b/app/main.py @@ -1,102 +1,134 @@ -import os +# To parse video ids import re -import threading -import pytz -from datetime import datetime -from dotenv import load_dotenv -from youtube_transcript_api import YouTubeTranscriptApi, _errors + +# Youtube Transcript stuff import +import youtube_transcript_api._errors +from youtube_transcript_api import YouTubeTranscriptApi from youtube_transcript_api.formatters import TextFormatter -from openai import AssistantEventHandler, OpenAI + +# OpenAI API stuff import +from openai import AssistantEventHandler +from openai import OpenAI + +### For streaming from typing_extensions import override import asyncio +awaiter = asyncio.run + +# The StreamOutput class to handle streaming +class StreamOutput: + + def __init__(self): + self.delta: str = "" + self.response: str = "" + self.done: bool = False + self.buffer: list = [] + + def reset(self): + self.delta = "" + self.response = "" + self.done = False + self.buffer: list = [] + + async def send_delta(self, delta): + self.delta = delta + self.response += delta + def get_index(list): + if len(list) == 0: + return 0 + else: + return len(list)-1 + if self.buffer != []: + try: + if self.delta != self.buffer[get_index(self.buffer)]: + self.buffer.append(delta) + except IndexError as index_error: + log(f"\nCaught IndexError: {str(index_error)}") + self.buffer.append(delta) + else: self.buffer.append(delta) + +# To get the env var +from dotenv import load_dotenv +import os -# Load environment variables load_dotenv() # For logging -def log(message: str): - timestamp = datetime.now(pytz.timezone('America/New_York')).strftime('%Y-%m-%d %H:%M:%S') +import pytz +from datetime import datetime + +def log(message): + try: with open("logs/log.md", "a") as file: - file.write(f"{timestamp} - {message}\n") + file.write(message) + except FileNotFoundError: + with open("logs/log.md", "x+"): + log(message) -# StreamOutput class to handle streaming -class StreamOutput: - def __init__(self): - self.response = "" - self.done = False - self.buffer = [] - self.lock = threading.Lock() +### OpenAI Config - def reset(self): - with self.lock: - self.response = "" - self.done = False - self.buffer = [] +# Setting up OpenAI Client with API Key +client = OpenAI( + organization='org-7ANUFsqOVIXLLNju8Rvmxu3h', + project="proj_NGz8Kux8CSka7DRJucAlDCz6", + api_key=os.getenv("OPENAI_API_KEY") +) - def add_to_buffer(self, delta: str): - with self.lock: - self.response += delta - self.buffer.append(delta) +# screw bardo assistant that is configured to make notes and 5Q&A based on any given YouTube Transcript +asst_screw_bardo_id = "asst_JGFaX6uOIotqy5mIJnu3Yyp7" + +# This is copy and pasted straight up from the quickstart guide, just appending to an output buffer instead of directly printing: +class EventHandler(AssistantEventHandler): + @override + def on_text_created(self, text) -> None: + awaiter(output_stream.send_delta("Response Recieved:\n\nScrew-Bardo:\n\n")) + + @override + def on_text_delta(self, delta, snapshot): + awaiter(output_stream.send_delta(delta.value)) + + def on_tool_call_created(self, tool_call): + raise Exception("Assistant shouldn't be calling tools.") + +def create_and_stream(transcript): + with client.beta.threads.create_and_run_stream( + assistant_id=asst_screw_bardo_id, + thread={ + "messages": [{"role": "user", "content": transcript}] + }, + event_handler=EventHandler() + ) as stream: + stream.until_done() + output_stream.done = True + +def get_video_id(url): + youtu_be = r'(?<=youtu.be/)([A-Za-z0-9_-]{11})' + youtube_com = r'(?<=youtube\.com\/watch\?v=)([A-Za-z0-9_-]{11})' + + id = re.search(youtu_be, url) + if not id: + id = re.search(youtube_com, url) + + if not id: + # Couldn't parse video ID from URL + return None + + return id.group(1) + +# Takes the transcript and formats it in basic text before writing it to auto-transcript.txt +def get_auto_transcript(video_id): + trans_api_errors = youtube_transcript_api._errors + try: + transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['en'], proxies=None, cookies=None, preserve_formatting=False) + except trans_api_errors.TranscriptsDisabled as e: + log(f'\n\n# Exception while fetching transcript:\n \n{e}\n') + return None + + formatter = TextFormatter() # Ensure that you create an instance of TextFormatter + + txt_transcript = formatter.format_transcript(transcript) + return txt_transcript output_stream = StreamOutput() -# OpenAI Client Configuration -client = OpenAI( - organization='org-7ANUFsqOVIXLLNju8Rvmxu3h', - project="proj_NGz8Kux8CSka7DRJucAlDCz6", - api_key=os.getenv("OPENAI_API_KEY") -) - -asst_screw_bardo_id = "asst_JGFaX6uOIotqy5mIJnu3Yyp7" - -# Async helper -def awaiter(coro): - asyncio.run(coro) - -# EventHandler for OpenAI Assistant -class EventHandler(AssistantEventHandler): - @override - def on_text_created(self, text) -> None: - awaiter(output_stream.send_delta("Response Received:\n\nScrew-Bardo:\n\n")) - - @override - def on_text_delta(self, delta, snapshot): - awaiter(output_stream.send_delta(delta.value)) - - def on_tool_call_created(self, tool_call): - raise Exception("Assistant shouldn't be calling tools.") - -def create_and_stream(transcript: str): - try: - with client.beta.threads.create_and_run_stream( - assistant_id=asst_screw_bardo_id, - thread={ - "messages": [{"role": "user", "content": transcript}] - }, - event_handler=EventHandler() - ) as stream: - stream.until_done() - output_stream.done = True - except Exception as e: - log(f"Error in create_and_stream: {e}") - output_stream.done = True - -def get_video_id(url: str) -> str: - youtu_be = r'(?<=youtu.be/)([A-Za-z0-9_-]{11})' - youtube_com = r'(?<=youtube\.com\/watch\?v=)([A-Za-z0-9_-]{11})' - - match = re.search(youtu_be, url) or re.search(youtube_com, url) - if match: - return match.group(1) - return None - -def get_auto_transcript(video_id: str) -> str: - try: - transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=['en']) - formatter = TextFormatter() - return formatter.format_transcript(transcript) - except _errors.TranscriptsDisabled as e: - log(f'Exception while fetching transcript: {e}') - except Exception as e: - log(f'Unexpected error while fetching transcript: {e}') - return None \ No newline at end of file +log(f"\n\n# Main initilized at {datetime.now(pytz.timezone('America/New_York')).strftime('%Y-%m-%d %H:%M:%S')}. Presumeably application starting.\n") \ No newline at end of file diff --git a/app/website/index.html b/app/website/index.html index f283176..a03369a 100644 --- a/app/website/index.html +++ b/app/website/index.html @@ -2,29 +2,27 @@ - - Screw You Bardo - - - + +
+
+
Response will appear here.
+
-
-
Response will appear here.        
-
- - -
- - -
+
+
+ + +
+
+
\ No newline at end of file diff --git a/app/website/static/script.js b/app/website/static/script.js index d28aab1..ec2793a 100644 --- a/app/website/static/script.js +++ b/app/website/static/script.js @@ -1,18 +1,24 @@ -document.addEventListener("DOMContentLoaded", (event) => { - const response_area = document.getElementById('response-area'); - const submit_button = document.getElementById('submit') - submit_button.addEventListener('click', function() { - var url = document.getElementById('url_box').value; +document.addEventListener("DOMContentLoaded", () => { + const responseArea = document.getElementById('response-area'); + const submitButton = document.getElementById('submit'); + const urlForm = document.getElementById('url-form'); + const urlBox = document.getElementById('url_box'); + + urlForm.addEventListener('submit', function(event) { + event.preventDefault(); // Prevent form from submitting the traditional way + const url = urlBox.value.trim(); if (!url) { - response_area.innerText = 'Please enter a URL.'; + responseArea.innerText = 'Please enter a URL.'; return; } - else { - document.getElementById('url_box').value = ""; - } - - // First, process the URL + + // Clear the input and update UI + urlBox.value = ""; + submitButton.disabled = true; + responseArea.innerText = 'Processing...'; + + // Process the URL fetch('/process_url', { method: 'POST', headers: { @@ -24,57 +30,58 @@ document.addEventListener("DOMContentLoaded", (event) => { if (!response.ok) { throw new Error('Network response was not ok'); } - // Extract the text from the response body - return response.text(); // Use .json() if the response is JSON + return response.text(); }) .then(text => { - submit_button.style.display = "none"; if (text === "Processing started. Check /stream_output for updates.") { - streamOutput(response_area); + streamOutput(responseArea); } else { - response_area.innerText = text; // Show any other response message - submit_button.style.display = "flex"; + responseArea.innerText = text; + submitButton.disabled = false; } }) .catch(error => { console.error('Error processing URL:', error); - response_area.innerText = 'Error processing URL: ' + error.message; - submit_button.style.display = "flex"; + responseArea.innerText = 'Error processing URL: ' + error.message; + submitButton.disabled = false; }); }); + + function streamOutput(responseArea) { + // Fetch the streaming output + fetch('/stream_output') + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + + responseArea.innerHTML = ""; + + function readStream() { + reader.read().then(({ done, value }) => { + if (done) { + submitButton.disabled = false; + return; + } + const chunk = decoder.decode(value, { stream: true }); + responseArea.innerHTML += chunk; + responseArea.scrollTop = responseArea.scrollHeight; + readStream(); + }).catch(error => { + console.error('Error reading stream:', error); + responseArea.innerText = 'Error reading stream: ' + error.message; + submitButton.disabled = false; + }); + } + + readStream(); + }) + .catch(error => { + console.error('Error fetching stream:', error); + responseArea.innerText = 'Error fetching stream: ' + error.message; + submitButton.disabled = false; + }); + } }); - -function streamOutput(response_area) { - // Fetch the streaming output - const streamResponsePromise = fetch('/stream_output'); - response_area.innerHTML = "" - - streamResponsePromise - .then(response => { - const reader = response.body.getReader(); - const decoder = new TextDecoder("utf-8"); - - function readStream() { - reader.read().then(({ done, value }) => { - if(done) { - document.getElementById('submit').style.display = "flex"; - return - } - // Decode and process the chunk - const chunk = decoder.decode(value, { stream: true }); - response_area.innerHTML += chunk; - response_area.scrollTop = response_area.scrollHeight - - // Continue reading - readStream(); - }); - } - - // Start reading the stream - readStream(); - }) - .catch(error => { - console.error('Error fetching stream:', error); - response_area.innerText = 'Error fetching stream: ' + error.message; - }); -} diff --git a/app/website/static/style.css b/app/website/static/style.css index eacb116..541c0d4 100644 --- a/app/website/static/style.css +++ b/app/website/static/style.css @@ -1,7 +1,5 @@ - - @font-face { - font-family: 'nimbus_sans_d_otlight'; + font-family: 'NimbusSansD'; src: url('font-files/nimbus-sans-d-ot-light.woff2') format('woff2'), url('font-files/nimbus-sans-d-ot-light.woff') format('woff'); font-weight: normal; @@ -9,70 +7,102 @@ } * { - font-family: 'nimbus_sans_d_otlight'; - color: white; + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: 'NimbusSansD', sans-serif; + color: #FFFFFF; } body { display: flex; - flex-direction: column; - width: 100%; - max-width: 100vw; - height: 100%; - min-height: 100vh; - max-height: 100vh; - margin: 0; - background-color: rgb(31, 31, 31); + justify-content: center; + align-items: center; + height: 100vh; + background-color: #1F1F1F; } -body .content { +.container { display: flex; flex-direction: column; - align-self: center; - width: 75%; - max-width: 65vw; - height: 100%; - min-height: 100vh; - max-height: 100vh; + width: 85vw; + height: 90vh; + background-color: #2E2E2E; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + overflow: hidden; +} + +.response-section { + flex: 1; + padding: 20px; + background-color: #1E1E1E; + overflow-y: auto; + font-size: 1rem; + line-height: 1.5; +} + +.form-section { + padding: 15px 20px; + background-color: #3A3A3A; } #response-area { - display: block; - height: 90%; - min-height: 90vh; - text-wrap: wrap; - flex-wrap: wrap; - align-content: flex-end; - overflow-y: auto; + white-space: pre-wrap; } -.form_box { +#url-form { display: flex; - width: 100%; - justify-content: space-between; - align-content: space-around; + gap: 10px; } #url_box { - display: flex; - height: 5%; - min-height: 5vh; - width: 90%; - min-width: 80vh; - background-color: rgb(31, 31, 31); + flex: 1; + padding: 10px 15px; + border: none; + border-radius: 5px; + background-color: #4A4A4A; + color: #FFFFFF; + font-size: 1rem; + outline: none; +} + +#url_box::placeholder { + color: #B0B0B0; } #submit { - display: flex; - width: 5%; - min-width: 3vw; - background-color: rgb(49, 49, 49); -} -#submit:hover { + padding: 10px 20px; + border: none; + border-radius: 5px; + background-color: #5A5A5A; + color: #FFFFFF; + font-size: 1rem; cursor: pointer; - background-color: rgb(31, 31, 31); + transition: background-color 0.3s ease; } -input { - border-radius: 15px; +#submit:hover { + background-color: #7A7A7A; } + +#submit:disabled { + background-color: #3A3A3A; + cursor: not-allowed; +} + +/* Responsive Adjustments */ +@media (max-width: 600px) { + .container { + height: 95vh; + } + + #url_box { + font-size: 0.9rem; + } + + #submit { + font-size: 0.9rem; + padding: 10px; + } +} \ No newline at end of file