OpenRewind/openrewind/app.py
2024-08-10 17:38:57 +08:00

195 lines
6.4 KiB
Python

from threading import Thread
import numpy as np
from flask import Flask, render_template_string, request, send_from_directory
from jinja2 import BaseLoader
from openrewind.config import appdata_folder, screenshots_path
from openrewind.database import create_db, get_all_entries, get_timestamps
from openrewind.nlp import cosine_similarity, get_embedding
from openrewind.screenshot import record_screenshots_thread
from openrewind.utils import human_readable_time, timestamp_to_human_readable
app = Flask(__name__)
app.jinja_env.filters["human_readable_time"] = human_readable_time
app.jinja_env.filters["timestamp_to_human_readable"] = timestamp_to_human_readable
base_template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenRecall</title>
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
<style>
.slider-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.slider {
width: 80%;
}
.slider-value {
margin-top: 10px;
font-size: 1.2em;
}
.image-container {
margin-top: 20px;
text-align: center;
}
.image-container img {
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
<nav class="navbar navbar-light bg-light">
<div class="container">
<form class="form-inline my-2 my-lg-0 w-100 d-flex" action="/search" method="get">
<input class="form-control flex-grow-1 mr-sm-2" type="search" name="q" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-secondary my-2 my-sm-0" type="submit">
<i class="bi bi-search"></i>
</button>
</form>
</div>
</nav>
{% block content %}
{% endblock %}
<!-- Bootstrap and jQuery JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.3/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
</body>
</html>
"""
class StringLoader(BaseLoader):
def get_source(self, environment, template):
if template == "base_template":
return base_template, None, lambda: True
return None, None, None
app.jinja_env.loader = StringLoader()
@app.route("/")
def timeline():
# connect to db
timestamps = get_timestamps()
return render_template_string(
"""
{% extends "base_template" %}
{% block content %}
{% if timestamps|length > 0 %}
<div class="container">
<div class="slider-container">
<input type="range" class="slider custom-range" id="discreteSlider" min="0" max="{{timestamps|length - 1}}" step="1" value="{{timestamps|length - 1}}">
<div class="slider-value" id="sliderValue">{{timestamps[0] | timestamp_to_human_readable }}</div>
</div>
<div class="image-container">
<img id="timestampImage" src="/static/{{timestamps[0]}}.webp" alt="Image for timestamp">
</div>
</div>
<script>
const timestamps = {{ timestamps|tojson }};
const slider = document.getElementById('discreteSlider');
const sliderValue = document.getElementById('sliderValue');
const timestampImage = document.getElementById('timestampImage');
slider.addEventListener('input', function() {
const reversedIndex = timestamps.length - 1 - slider.value;
const timestamp = timestamps[reversedIndex];
sliderValue.textContent = new Date(timestamp * 1000).toLocaleString(); // Convert to human-readable format
timestampImage.src = `/static/${timestamp}.webp`;
});
// Initialize the slider with a default value
slider.value = timestamps.length - 1;
sliderValue.textContent = new Date(timestamps[0] * 1000).toLocaleString(); // Convert to human-readable format
timestampImage.src = `/static/${timestamps[0]}.webp`;
</script>
{% else %}
<div class="container">
<div class="alert alert-info" role="alert">
Nothing recorded yet, wait a few seconds.
</div>
</div>
{% endif %}
{% endblock %}
""",
timestamps=timestamps,
)
@app.route("/search")
def search():
q = request.args.get("q")
entries = get_all_entries()
embeddings = [
np.frombuffer(entry.embedding, dtype=np.float64) for entry in entries
]
query_embedding = get_embedding(q)
similarities = [cosine_similarity(query_embedding, emb) for emb in embeddings]
indices = np.argsort(similarities)[::-1]
sorted_entries = [entries[i] for i in indices]
return render_template_string(
"""
{% extends "base_template" %}
{% block content %}
<div class="container">
<div class="row">
{% for entry in entries %}
<div class="col-md-3 mb-4">
<div class="card">
<a href="#" data-toggle="modal" data-target="#modal-{{ loop.index0 }}">
<img src="/static/{{ entry['timestamp'] }}.webp" alt="Image" class="card-img-top">
</a>
</div>
</div>
<div class="modal fade" id="modal-{{ loop.index0 }}" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl" role="document" style="max-width: none; width: 100vw; height: 100vh; padding: 20px;">
<div class="modal-content" style="height: calc(100vh - 40px); width: calc(100vw - 40px); padding: 0;">
<div class="modal-body" style="padding: 0;">
<img src="/static/{{ entry['timestamp'] }}.webp" alt="Image" style="width: 100%; height: 100%; object-fit: contain; margin: 0 auto;">
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
""",
entries=sorted_entries,
)
@app.route("/static/<filename>")
def serve_image(filename):
return send_from_directory(screenshots_path, filename)
if __name__ == "__main__":
create_db()
print(f"Appdata folder: {appdata_folder}")
# Start the thread to record screenshots
t = Thread(target=record_screenshots_thread)
t.start()
app.run(port=8082)