# Project export: Texty Bear

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2024
- Tagline: Need to cram for a test? Revolutionize studying with our AI! Input a youtube URL and receive concise notes. Enhance learning efficiency effortlessly.
- Devpost: https://devpost.com/software/texty-bear
- GitHub: https://github.com/DenialCant/CruzHack2024
- Video: https://www.youtube.com/embed/jyjv_fyq56M?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Set Paing (21 commits), Jia Jun Yu (16 commits)

## Devpost submission (written by the team)

### Inspiration

Life is tough and sometimes you don't have time to watch full lectures. Texty Bear allows you to still learn that information in a fraction of a time.

### What it does

By submitting a YouTube lecture URL, Texty Bear will automatically look for key points within the lecture and create notes just for you!

### How we built it

We utilized the PyTube python package and AssemblyAI to parse YouTube files into notes for our backend. For our front end, we used Flask to incorporate Python scripts into our HTML.

### Challenges we ran into

This our our first Hackathon so there are many challenges :). Here are some of our major ones: Git version control Trouble learning the Flask package and how it interacts with HTML Tight deadlines with little project experience(skill issue) It's very cold here... Had to pivot from our original idea as it wasn't feasible to do ##

### Accomplishments we're proud of

Cute website aesthetics Learned new packages and libraries Fixed many bugs :) Gaining real-life experiences with working in a team and with tight deadlines ##

### What we learned

Learned Flask AI is easier to use than I thought... I finally fixed my Python virtual environments :D ##

### What's next

Allow users to upload both media files and YouTube URLs Use and train our own model to take better, more concise notes Continue improving UI/UX of our website Optimize Youtube data parser and API calls

## README (from the GitHub repository)

# Texty Bear - Flask Application

Welcome to **Texty Bear** Flask application!

Life is tough and sometimes you don't have time to watch full lectures. Texty Bear allows you to still learn that information in a fraction of a time.

By submitting a YouTube lecture URL, Texty Bear will automatically look for key points within the lecture and create notes just for you!

## Installation
1. **Create Virtual Environment:**

Run the following command to create a virtual environment:
```bash
cd path/to/your/project
```

Create a virtual environment named `venv`:
```bash
python -m venv venv
```

2. **Activate the Virtual Environment:**

On Linux or macOS:
```bash
source venv/bin/activate
```
On Windows (Command Prompt):
```bash
venv\Scripts\activate
```
On Windows (PowerShell):
```bash
.\venv\Scripts\Activate.ps1
```

3. **Install Dependencies:**

While the virtual environment is activated, install the required dependencies from the requirements.txt file:
```bash
pip install -r requirements.txt
```

Run the Flask Application:
```bash
python app.py
```
This will start your Flask application. Visit `http://127.0.0.1:5000/` in your browser to access the app.

## Detected evidence (automated analysis)

Indexed codebase: 9 recognized source files, 16 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (11 of 11)

```
.gitignore
app.py
getVideos.py
README.md
requirements.txt
static/reset.css
static/style.css
templates/base.html
templates/home.html
templates/yt_transcribe.html
transcribe.py
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- some-initialization-animations
- final
- we-dont-have-mp3-support-sadge
- infinite-loading-fixed
- final-version-i-guess
- loading-spinner
- test
- DANiel gang
- probably-last-version-without-loading-screen
- new-colorsssss
- Merge branch 'main' of https://github.com/DenialCant/CruzHack2024
- sadas
- added download button
- added download button
- download-txt-feature
- Merge branch 'main' of https://github.com/DenialCant/CruzHack2024
- download-txt-btn
- EZZZ
- test
- test

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
��a n n o t a t e d - t y p e s = = 0 . 6 . 0  
 a n y i o = = 4 . 2 . 0  
 a s s e m b l y a i = = 0 . 2 0 . 2  
 b l i n k e r = = 1 . 7 . 0  
 c e r t i f i = = 2 0 2 3 . 1 1 . 1 7  
 c l i c k = = 8 . 1 . 7  
 c o l o r a m a = = 0 . 4 . 6  
 e x c e p t i o n g r o u p = = 1 . 2 . 0  
 F l a s k = = 3 . 0 . 1  
 h 1 1 = = 0 . 1 4 . 0  
 h t t p c o r e = = 1 . 0 . 2  
 h t t p x = = 0 . 2 6 . 0  
 i d n a = = 3 . 6  
 i t s d a n g e r o u s = = 2 . 1 . 2  
 J i n j a 2 = = 3 . 1 . 3  
 M a r k u p S a f e = = 2 . 1 . 4  
 p y d a n t i c = = 2 . 5 . 3  
 p y d a n t i c _ c o r e = = 2 . 1 4 . 6  
 p y t u b e = = 1 5 . 0 . 0  
 s n i f f i o = = 1 . 3 . 0  
 t y p i n g _ e x t e n s i o n s = = 4 . 9 . 0  
 w e b s o c k e t s = = 1 2 . 0  
 W e r k z e u g = = 3 . 0 . 1  
 
```

### app.py

```python
from flask import Flask, render_template, request, url_for, flash, redirect, send_file
from transcribe import audio_to_text, download_notes
from getVideos import yt_to_mp3
import os

app = Flask(__name__)
app.config['SECRET_KEY'] = '3f500e5c4689bb1221d1bbc545931d898f30c1d9c2a9ad6a'

transcript_results = []

@app.route('/')
def home():
    transcript_results.clear()
    return render_template('home.html')

name = ""
tran_rs = ""

@app.route('/yt_transcribe/', methods=('GET', 'POST'))
def yt_transcribe():
    global name, tran_rs

    if request.method == 'POST':
        if 'yt_url' in request.form:
            title = request.form['yt_url']

            file_name, name = yt_to_mp3(title)
            if file_name != 0:
                tran_rs = audio_to_text(file_name)
                transcript_results.append({'title': name, 'content': tran_rs})
                try:
                    os.remove(file_name)
                finally:
                    print("File DOE")
            else:
                flash('Error: YouTube Link is not working')
        else:
            if 'return_code' in request.form:
                transcript_results.clear()
            elif 'download_code' in request.form:
                download_file()

    return render_template('yt_transcribe.html', transcript_results=transcript_results)

@app.route('/download')
def download_file():
    global name, tran_rs
    content = f"{name}\n\n{tran_rs}"

    with open(name + '.txt', 'w') as file:
        file.write(content)

    return send_file(name + '.txt', as_attachment=True)



if __name__ == '__main__':
    app.run(debug=True)

```

### getVideos.py

```python
# importing packages 
from pytube import YouTube 
import os

def yt_to_mp3(url_text):
	try:
		URL =  YouTube(url_text)
	except Exception:
		print("hello")
		return 0, 0
	else:
		video = URL.streams.filter(only_audio=True).first()

		destination = 'Videos/'

		# download the file 
		out_file = video.download(output_path=destination) 

		# save the file 
		base, ext = os.path.splitext(out_file) 
		new_file = base + '.mp3'
		
		try:
			os.rename(out_file, new_file)
		except FileExistsError:
			try:
				os.remove(base + ext)
			finally:
				print("ERROR. MP3 not found")
			print("File alreadly exists. Pulling that file...")
		finally:
			# result of success 
			print(URL.title + " has been successfully downloaded.")
			return new_file, URL.title




```

### transcribe.py

```python
# Start by making sure the `assemblyai` package is installed.
# If not, you can install it by running the following command:
# pip install -U assemblyai
#
# Note: Some macOS users may need to use `pip3` instead of `pip`.
from flask import send_file
import assemblyai as aai

# Replace with your API key
aai.settings.api_key = "db2eac9430074de2b97c0d7d83a017ee"

def audio_to_text(url):
    # URL of the file to transcribe
    FILE_URL = url
    #"https://github.com/AssemblyAI-Examples/audio-examples/raw/main/20230607_me_canadian_wildfires.mp3"

    # You can also transcribe a local file by passing in a file path
    # FILE_URL = './path/to/file.mp3'
    transcriber = aai.Transcriber()
    transcript = transcriber.transcribe(FILE_URL)
    
    
    config = aai.TranscriptionConfig(
    summarization=True,
    summary_model=aai.SummarizationModel.informative,
    summary_type=aai.SummarizationType.bullets
    )

    transcript = aai.Transcriber().transcribe(FILE_URL, config)

    return summary_parser(transcript.summary)

def download_notes(title, body):
    if not title:
        title = "Sample"
    if not body:
        body = "Sample"
    content = f"{title}\n\n{body}"

    with open('sample_file.txt', 'w') as file:
        file.write(content)

    return send_file('sample_file.txt', as_attachment=True)

def summary_parser(input_string):
    if input_string:
        result_list = input_string.split("-")
        
        # Remove leading and trailing whitespaces, including newline characters
        result_list = [element.strip("\n") for element in result_list]
        
        # Remove empty strings from the list
        result_list = [element for element in result_list if element]
        
        # Add back the hyphen to each element except the last one
        result_list = ["-" + element  for element in result_list[:-1]] + [result_list[-1]]
        
        return result_list
    else:
        return ""
```

### static/reset.css

```css
/*
YUI 3.18.1 (build f7e7bcb)
Copyright 2014 Yahoo! Inc. All rights reserved.
Licensed under the BSD License.
http://yuilibrary.com/license/
*/

html {
	color: #000;
	background: #FFF;
}
body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6,
pre, code, form, fieldset, legend, input, textarea, p, blockquote, th, td {
	margin: 0;
	padding: 0;
}
table {
	border-collapse: collapse;
	border-spacing: 0;
}
fieldset, img {
	border: 0;
}
address, caption, cite, code, dfn, em, strong, th, var {
	font-style: normal;
	font-weight: normal;
}
ol, ul {
	list-style: none;
}
caption, th {
	text-align: left;
}
h1, h2, h3, h4, h5, h6 {
	font-size: 100%;
	font-weight: normal;
}
q:before, q:after {
	content: '';
}
abbr, acronym {
	border: 0;
	font-variant: normal;
}
sup {
	vertical-align: text-top;
}
sub {
	vertical-align: text-bottom;
}
input, textarea, select {
	font-family: inherit;
	font-size: inherit;
	font-weight: inherit;
	/*font-size: 100%;*/
}
legend {
	color: #000;
}
#yui3-css-stamp.cssreset {
	display: none;
}
```

### templates/base.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Texty Bear</title>
    <link rel="icon" href="{{ url_for('static', filename='images/favicon.png') }}">
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='reset.css') }}">
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="header-bar flex-hori">
    
        <div class="logo-holder">
            <a href="{{ url_for('home') }}">
                <img class="logo" src="{{ url_for('static', filename='images/logo.png') }}">
            </a>  
        </div>
    </div>

    <div class="vertical-space"></div>
    <div class="vertical-space"></div>

    <div class="main-body" id="main-body">
        <div class="wrapper flex-vert" id="content-to-disable">
            {% block content %} {% endblock %}
            
            {% for message in get_flashed_messages() %}
                <div class="alert-box">{{ message }}</div>
            {% endfor %}

            {% block script %} {% endblock %}
        </div>
    </div>
</body>
</html>
```

### templates/home.html

```html
{% extends 'base.html' %}

{% block content %}

    <div class="body-big-txt-holder flex-hori mid-holder">
        <div class="body-big-txt-txt text-align-center appear-animation">
            Video Notetaker
        </div>
    </div>

    <div class="vertical-space"></div>

    <div class="body-mid-txt-holder flex-hori mid-holder">
        <div class="body-mid-txt-txt text-align-center appear-animation" style="animation-delay: 0.1s;">
            Tired of having to watch video lectures?
        </div>
    </div>

    <div class="body-mid-txt-holder flex-hori mid-holder">
        <div class="body-mid-txt-txt text-align-center appear-animation" style="animation-delay: 0.2s;">
            Enhance productivity with our AI video to note converter
        </div>
    </div>
    
    <div class="vertical-space"></div>
    <div class="vertical-space"></div>

    <div class="body-small-txt-holder flex-hori mid-holder">
        <div class="body-small-txt-txt text-align-center appear-animation" style="animation-delay: 0.3s;">
            To get started, simply just enter a youtube URL and watch the magic happens.
        </div>
    </div>

    <div class="vertical-space"></div>

    <div class="mid-holder flex-hori">
        <a href="{{ url_for('yt_transcribe') }}">
            <button class="main-big-button appear-animation" style="animation-delay: 0.4s;" type="submit">
                Get Started
            </button>
        </a>
    </div>
{% endblock %}
```

### templates/yt_transcribe.html

```html
{% extends 'base.html' %}

{% block content %}

    {% if transcript_results|length == 0 %}

    <div class="body-big-txt-holder flex-hori mid-holder">
        <div class="body-big-txt-txt text-align-center appear-animation">
            Convert Video to Text
        </div>
    </div>

    <div class="vertical-space"></div>

    <form method="post">
        <input class="url-input-field appear-animation" style="animation-delay: 0.1s;" type="text" name="yt_url"
               placeholder="Enter YouTube URL here"
               value="{{ request.form['title'] }}"></input>
        <br>
        <div class="mid-holder flex-hori">
            <button class="main-button appear-animation" style="animation-delay: 0.2s;"
                type="submit" onclick="disable_and_loader()">Transcribe</button>
        </div>
    </form>
    {% else %}
        {% for transcript_result in transcript_results %}
            <div class="body-mid-txt-holder">
                <div class="body-mid-txt-txt appear-animation">
                    {{ transcript_result['title'] }}
                </div>
            </div>
            <div class="result_card appear-animation" style="animation-delay: 0.1s;">
                {% for tr_rs_idv in transcript_result['content'] %}
                    {{ tr_rs_idv }}<br><br>
                {% endfor %}
            </div>
        {% endfor %}
    <div class="mid-holder flex-hori">
        <form method="post">    
            <button class="main-button appear-animation" style="animation-delay: 0.2s;"
            name="return_code" type="submit">Return</button>       
        </form>
        <form action="/download" method="get">
            <button class="main-button appear-animation" style="animation-delay: 0.2s;"
            type="submit" name="download_code">Download File</button>
        </form>
    </div>
    {% endif %}
    
{% endblock %}

{% block script %}
    <script>
        function disable_and_loader() {
            document.getElementById('content-to-disable').style.display = 'none';

            let loader_box = document.createElement("div"); loader_box.className = "loader-box";
            let loader = document.createElement("div"); loader.className = "loader";
            let loader_holder = document.createElement("div"); loader_holder.className = "loader-holder";
            let loader_text = document.createElement("p"); loader_text.className = "loader-text appear-animation";
            loader_text.innerHTML = "Loading...";

            loader_holder.appendChild(loader);
            loader_box.appendChild(loader_holder);
            loader_box.appendChild(loader_text);
            document.getElementById('main-body').appendChild(loader_box);
        }
    </script>
{% endblock %}
```

### static/style.css

```css
:root{
	--c0: #422318;
	--c1: #753D1F;
	--c2: #594031;
	--c3: #956038;
	--c9: #a27b5e;
}

@keyframes loader-spin{
	0% {transform: translateX(-40px) translateY(-40px) rotate(270deg); 
		border: 2px solid #CCCCCC; border-radius: 4px;}
	25% {transform: translateX(40px) translateY(-40px) rotate(90deg);
		border: 30px solid #CCCCCC; border-radius: 50%;}
	50% {transform: translateX(40px) translateY(40px) rotate(0deg);
		border: 30px solid #CCCCCC; border-radius: 4px;}
	75% {transform: translateX(-40px) translateY(40px) rotate(-180deg);
		border: 2px solid #CCCCCC; border-radius: 50%;}
	100% {transform: translateX(-40px) translateY(-40px) rotate(-270deg);
		border: 2px solid #CCCCCC; border-radius: 4px;}
}
@keyframes appearAni {
    0% {transform: scale(0.95) translateY(20px); filter: opacity(0);}
    100% {transform: scale(1) translateY(0px); filter: opacity(1);}
    
}

@font-face{font-family: FontComfortaa; src: url('/static/fonts/Comfortaa.ttf');}
@font-face{font-family: FontBellefair; src: url('/static/fonts/Bellefair.ttf');}

html{min-width: 440px; scroll-behavior: smooth; background-color: var(--c1);}
body{
	font-family: FontComfortaa; cursor: default;
	background: var(--c1); color: #FFF;
}
body::-webkit-scrollbar {display: none;}
*{box-sizing: border-box; outline: none; font: inherit;}

.wrapper{margin-left: 10px; margin-right: 10px; display: flex;}
.wrapper-body{margin-left: 10px; margin-right: 10px;}
@media (min-width: 768px) {
	.wrapper{margin-left: 6%; margin-right: 6%;}
	.wrapper-body{margin-left: 6%; margin-right: 6%;}
}
@media (min-width: 992px) {
	.wrapper{margin-left: 8%; margin-right: 8%;}
	.wrapper-body{margin-left: 6%; margin-right: 6%;}
}
@media (min-width: 1200px) {
	.wrapper{margin-left: 12%; margin-right: 12%;}
	.wrapper-body{margin-left: 12%; margin-right: 12%;}
}
.normal-text{font-size: 14px; line-height: 20px;}
.vertical-space{height: 20px;}

.logo-holder{justify-content: center; padding: 10px 20px;}
.logo{height: 80px; transition: 0.4s;}
.logo:hover{transform: scale(1.02);}
.logo:active{transform: scale(0.98);}

.flex-vert{display: flex; flex-direction: column;}
.flex-hori{display: flex; flex-direction: row;}
.mid-holder{justify-content: center;}

.body-big-txt-holder{width: 100%;}
.body-big-txt-txt{font-family: FontBellefair; font-size: 70px;}

.body-mid-txt-holder{width: 100%;}
.body-mid-txt-txt{font-family: FontBellefair; font-size: 30px;}

.body-small-txt-holder{width: 100%;}
.body-small-txt-txt{font-family: FontBellefair; font-size: 24px;}

.text-align-center{text-align: center;}

.result_card{width: 100%; background-color: var(--c3); padding: 20px; border-radius: 4px;
	display: flex; flex-direction: column; margin: 4px 0px;}

.url-input-field{
	width: 100%;
	padding: 12px 20px;
	margin: 8px 0;
	display: inline-block;
	border: 1px solid #ccc;
	border-radius: 4px;
}
.main-button{background-color: var(--c3); border: none; padding: 10px; font-size: 18px;
	color: #FFF; border-radius: 6px; transition: 0.4s; margin: 6px 8px;}
.main-button:hover{background-color: var(--c9);}

.main-big-button{background-color: var(--c3); border: none; padding: 12px 32px; font-size: 24px;
	color: #FFF; border-radius: 8px; transition: 0.4s; margin: 6px 8px;}
.main-big-button:hover{background-color: var(--c9);}

.loader-box{
	position: relative; width: 180px;
	padding: 0px 10px;
	left: 50%; top: 50%;
	transform: translateX(-50%) translateY(50%);
}

.loader-holder{
	width: 160px; height: 160px;
	padding: 50px;
	justify-content: center;
}

.loader{
	border: 2px solid #CCCCCC;
	width: 60px; height: 60px;
	animation: loader-spin 2.5s ease-in-out infinite;
}

.loader-text{
	text-align: center; line-height: 40px;
	font-family: FontComfortaa; font-size: 20px;
}

.appear-animation{
	animation-fill-mode: both;
	animation-name: appearAni;
	animation-duration: 0.4s;
	animation-direction: normal;
	animation-iteration-count: 1;
}

.alert-box{
	padding: 12px 20px;
	margin: 8px 0;
	color: #970020;
	border-radius: 4px;
	background-color: #ffd5de;
}

.main-body{min-height: 400px;}
```