195 lines
6.4 KiB
Python
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)
|