mirror of
https://github.com/senju1337/senju.git
synced 2025-12-24 07:39:29 +00:00
commit
1a019a9e11
9 changed files with 2191 additions and 243 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.12-alpine AS base
|
FROM python:3.11 AS base
|
||||||
|
|
||||||
ENV POETRY_VIRTUALENVS_CREATE=true
|
ENV POETRY_VIRTUALENVS_CREATE=true
|
||||||
ENV FLASK_APP=senju/main.py
|
ENV FLASK_APP=senju/main.py
|
||||||
|
|
@ -10,9 +10,9 @@ WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN apk add curl bash jq
|
RUN apt update && apt install curl bash jq
|
||||||
RUN pip install poetry
|
RUN pip install poetry
|
||||||
RUN poetry install
|
RUN poetry install -v
|
||||||
|
|
||||||
# Expose development port
|
# Expose development port
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
|
||||||
1751
poetry.lock
generated
Normal file
1751
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,9 +2,12 @@
|
||||||
name = "senju"
|
name = "senju"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "API / Webservice for Phrases/Words/Kanji/Haiku"
|
description = "API / Webservice for Phrases/Words/Kanji/Haiku"
|
||||||
authors = [{ name = "Christoph J. Scherr", email = "software@cscherr.de" },{name = "Moritz Marquard", email="mrmarquard@protonmail.com"}]
|
authors = [
|
||||||
|
{ name = "Christoph J. Scherr", email = "software@cscherr.de" },
|
||||||
|
{ name = "Moritz Marquard", email = "mrmarquard@protonmail.com" },
|
||||||
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jinja2 (>=3.1.5,<4.0.0)",
|
"jinja2 (>=3.1.5,<4.0.0)",
|
||||||
"pytest>=7.0.0",
|
"pytest>=7.0.0",
|
||||||
|
|
@ -13,6 +16,9 @@ dependencies = [
|
||||||
"requests (>=2.32.3,<3.0.0)",
|
"requests (>=2.32.3,<3.0.0)",
|
||||||
"coverage (>=7.6.12,<8.0.0)",
|
"coverage (>=7.6.12,<8.0.0)",
|
||||||
"pytest-httpserver (>=1.1.2,<2.0.0)",
|
"pytest-httpserver (>=1.1.2,<2.0.0)",
|
||||||
|
"pillow (>=11.1.0,<12.0.0)",
|
||||||
|
"torch (>=2.6.0,<3.0.0)",
|
||||||
|
"transformers (>=4.50.0,<5.0.0)",
|
||||||
"waitress (>=3.0.2,<4.0.0)",
|
"waitress (>=3.0.2,<4.0.0)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
66
senju/image_reco.py
Normal file
66
senju/image_reco.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
from transformers import BlipProcessor, BlipForConditionalGeneration
|
||||||
|
|
||||||
|
|
||||||
|
class ImageDescriptionGenerator:
|
||||||
|
def __init__(self, model_name="Salesforce/blip-image-captioning-base"):
|
||||||
|
"""
|
||||||
|
Initialize an image description generator using a vision-language
|
||||||
|
model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name: The name of the model to use
|
||||||
|
(default: BLIP captioning model)
|
||||||
|
"""
|
||||||
|
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
print(f"Using device: {self.device}")
|
||||||
|
|
||||||
|
self.processor = BlipProcessor.from_pretrained(model_name)
|
||||||
|
self.model = BlipForConditionalGeneration.from_pretrained(model_name)
|
||||||
|
|
||||||
|
def generate_description(self, image_data, max_length=50):
|
||||||
|
"""
|
||||||
|
Generate a descriptive caption for the given image.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_data: Raw image data (bytes)
|
||||||
|
max_length: Maximum length of the generated caption
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary containing the generated description
|
||||||
|
and confidence score
|
||||||
|
"""
|
||||||
|
# Convert uploaded bytes to image
|
||||||
|
img = Image.open(io.BytesIO(image_data)).convert("RGB")
|
||||||
|
|
||||||
|
# Process the image
|
||||||
|
inputs = self.processor(
|
||||||
|
images=img, return_tensors="pt").to(self.device)
|
||||||
|
|
||||||
|
# Generate caption
|
||||||
|
with torch.no_grad():
|
||||||
|
output = self.model.generate(
|
||||||
|
**inputs,
|
||||||
|
max_length=max_length,
|
||||||
|
num_beams=5,
|
||||||
|
num_return_sequences=1,
|
||||||
|
temperature=1.0,
|
||||||
|
do_sample=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Decode the caption
|
||||||
|
caption = self.processor.decode(output[0], skip_special_tokens=True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"description": caption,
|
||||||
|
"confidence": None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
g_descriptor: ImageDescriptionGenerator = ImageDescriptionGenerator()
|
||||||
|
|
||||||
|
|
||||||
|
def gen_response(image_data) -> dict:
|
||||||
|
return g_descriptor.generate_description(image_data)
|
||||||
|
|
@ -53,6 +53,7 @@ from flask import (Flask, redirect, render_template, request,
|
||||||
send_from_directory, url_for)
|
send_from_directory, url_for)
|
||||||
|
|
||||||
from senju.haiku import Haiku
|
from senju.haiku import Haiku
|
||||||
|
from senju.image_reco import gen_response
|
||||||
from senju.store_manager import StoreManager
|
from senju.store_manager import StoreManager
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
@ -165,6 +166,22 @@ def scan_view():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/v1/image_reco", methods=['POST'])
|
||||||
|
def image_recognition():
|
||||||
|
# note that the classifier is a singleton
|
||||||
|
if 'image' not in request.files:
|
||||||
|
return "No image file provided", 400
|
||||||
|
|
||||||
|
image_file = request.files['image']
|
||||||
|
image_data = image_file.read()
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = gen_response(image_data)
|
||||||
|
return results
|
||||||
|
except Exception as e:
|
||||||
|
return str(e), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/v1/haiku", methods=['POST'])
|
@app.route("/api/v1/haiku", methods=['POST'])
|
||||||
def generate_haiku():
|
def generate_haiku():
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
159
senju/static/js/scan.js
Normal file
159
senju/static/js/scan.js
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
// Get all needed elements
|
||||||
|
const dropzoneFile = document.getElementById("dropzone-file");
|
||||||
|
const uploadArea = document.getElementById("upload-area");
|
||||||
|
const imagePreview = document.getElementById("image-preview");
|
||||||
|
const previewImg = document.getElementById("preview-img");
|
||||||
|
const removeImageBtn = document.getElementById("remove-image");
|
||||||
|
const responseBox = document.getElementById("response-box");
|
||||||
|
const submitButton = document.getElementById("submit-button");
|
||||||
|
const errorMessage = document.getElementById("error-message");
|
||||||
|
const yesButton = document.getElementById("yesButton");
|
||||||
|
const noButton = document.getElementById("noButton");
|
||||||
|
const generatingHaikuBox = document.getElementById("generating-haiku-box");
|
||||||
|
const generatedHaikuBox = document.getElementById("generated-haiku-box");
|
||||||
|
let haiku_prompt = "";
|
||||||
|
let imageUploaded = false;
|
||||||
|
|
||||||
|
function handleFileSelect(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
|
||||||
|
if (file && file.type.startsWith("image/")) {
|
||||||
|
// Create a URL for the selected image
|
||||||
|
const imageUrl = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
// Set the image source
|
||||||
|
previewImg.src = imageUrl;
|
||||||
|
|
||||||
|
// Hide upload area and show image preview
|
||||||
|
uploadArea.classList.add("hidden");
|
||||||
|
imagePreview.classList.remove("hidden");
|
||||||
|
errorMessage.classList.add("hidden");
|
||||||
|
|
||||||
|
// Set flag that image is uploaded
|
||||||
|
imageUploaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeImage() {
|
||||||
|
dropzoneFile.value = "";
|
||||||
|
|
||||||
|
// Hide image
|
||||||
|
imagePreview.classList.add("hidden");
|
||||||
|
uploadArea.classList.remove("hidden");
|
||||||
|
|
||||||
|
URL.revokeObjectURL(previewImg.src);
|
||||||
|
previewImg.src = "";
|
||||||
|
|
||||||
|
imageUploaded = false;
|
||||||
|
responseBox.classList.add("opacity-0");
|
||||||
|
generatingHaikuBox.classList.add("hidden");
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById("ai-response").textContent = "Waiting for input...";
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (imageUploaded) {
|
||||||
|
// Hide error
|
||||||
|
errorMessage.classList.add("hidden");
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
document.getElementById("ai-response").textContent = "Analyzing image...";
|
||||||
|
responseBox.classList.remove("opacity-0");
|
||||||
|
|
||||||
|
// Get the file from the input
|
||||||
|
const file = dropzoneFile.files[0];
|
||||||
|
|
||||||
|
// Create FormData object to send the file
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("image", file);
|
||||||
|
|
||||||
|
// Send the image to your backend API
|
||||||
|
fetch("/api/v1/image_reco", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
// Extract top result and display it
|
||||||
|
if (data.description) {
|
||||||
|
haiku_prompt = data.description;
|
||||||
|
document.getElementById("ai-response").textContent =
|
||||||
|
`Recognized: ${haiku_prompt}`;
|
||||||
|
} else {
|
||||||
|
document.getElementById("ai-response").textContent =
|
||||||
|
"Could not identify the image";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
document.getElementById("ai-response").textContent =
|
||||||
|
"Error analyzing image";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
errorMessage.classList.remove("hidden");
|
||||||
|
uploadArea.classList.add("shake");
|
||||||
|
setTimeout(() => {
|
||||||
|
uploadArea.classList.remove("shake");
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleYesClick() {
|
||||||
|
// Hide response box
|
||||||
|
responseBox.classList.add("opacity-0");
|
||||||
|
|
||||||
|
responseBox.textContent = "🤖 AI is thinking...";
|
||||||
|
responseBox.classList.remove("opacity-0");
|
||||||
|
|
||||||
|
fetch("/api/v1/haiku", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ prompt: haiku_prompt }),
|
||||||
|
})
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((data) => {
|
||||||
|
let id = parseInt(data, 10);
|
||||||
|
window.location.href = "/haiku/" + id;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
responseBox.textContent = "Error: " + error.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNoClick() {
|
||||||
|
// Reset everything
|
||||||
|
removeImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
dropzoneFile.addEventListener("change", handleFileSelect);
|
||||||
|
removeImageBtn.addEventListener("click", removeImage);
|
||||||
|
submitButton.addEventListener("click", handleSubmit);
|
||||||
|
yesButton.addEventListener("click", handleYesClick);
|
||||||
|
noButton.addEventListener("click", handleNoClick);
|
||||||
|
|
||||||
|
// Add some CSS animation
|
||||||
|
document.head.insertAdjacentHTML(
|
||||||
|
"beforeend",
|
||||||
|
`
|
||||||
|
<style>
|
||||||
|
@keyframes shake {
|
||||||
|
0% { transform: translateX(0); }
|
||||||
|
25% { transform: translateX(-5px); }
|
||||||
|
50% { transform: translateX(5px); }
|
||||||
|
75% { transform: translateX(-5px); }
|
||||||
|
100% { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
.shake {
|
||||||
|
animation: shake 0.5s ease-in-out;
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
@ -22,7 +22,8 @@
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<a href="{{ url_for('index_view') }}">
|
<a href="{{ url_for('index_view') }}">
|
||||||
<img class="size-8" src="{{ url_for('static', filename='img/senju-nobg.svg') }}"
|
<img class="size-8" src="{{ url_for('static', filename='img/senju-nobg.svg') }}"
|
||||||
alt="選集" ,>
|
alt="選集">
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:block">
|
<div class="md:block">
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,21 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="response-box" class="mt-8 bg-white text-gray-900 p-6 rounded-lg shadow-lg max-w-lg w-full text-center opacity-0 transition-opacity duration-500 ease-in-out">
|
<div
|
||||||
|
id="response-box"
|
||||||
|
class="mt-8 bg-white text-gray-900 p-6 rounded-lg shadow-lg max-w-lg w-full text-center opacity-0 transition-opacity duration-500 ease-in-out"
|
||||||
|
>
|
||||||
<h2 class="text-2xl font-semibold text-violet-700">Response:</h2>
|
<h2 class="text-2xl font-semibold text-violet-700">Response:</h2>
|
||||||
<p id="ai-response" class="text-lg text-gray-700 mt-2 italic">Waiting for input...</p>
|
<p id="ai-response" class="text-lg text-gray-700 mt-2 italic">
|
||||||
|
Waiting for input...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById("submit-btn").addEventListener("click", function() {
|
document.getElementById("submit-btn").addEventListener("click", function () {
|
||||||
let userInput = document.getElementById("user-input").value;
|
let userInput = document.getElementById("user-input").value;
|
||||||
let responseBox = document.getElementById("response-box");
|
let responseBox = document.getElementById("response-box");
|
||||||
let responseText = document.getElementById("ai-response");
|
let responseText = document.getElementById("ai-response");
|
||||||
|
|
@ -39,8 +45,9 @@ document.getElementById("submit-btn").addEventListener("click", function() {
|
||||||
|
|
||||||
if (userInput.trim() === "") {
|
if (userInput.trim() === "") {
|
||||||
responseText.textContent = "Please enter a prompt!";
|
responseText.textContent = "Please enter a prompt!";
|
||||||
}
|
} else if (userInput.length > 100) {
|
||||||
else if (userInput.trim() === "amogus") {
|
responseText.textContent = "Input must under 100 characters long!";
|
||||||
|
} else if (userInput.trim() === "amogus") {
|
||||||
responseText.textContent = "🤖 AI is thinking...";
|
responseText.textContent = "🤖 AI is thinking...";
|
||||||
responseBox.classList.remove("opacity-0");
|
responseBox.classList.remove("opacity-0");
|
||||||
|
|
||||||
|
|
@ -48,27 +55,26 @@ document.getElementById("submit-btn").addEventListener("click", function() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
responseText.textContent = "Sus imposter ඞ";
|
responseText.textContent = "Sus imposter ඞ";
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
responseText.textContent = "🤖 AI is thinking...";
|
responseText.textContent = "🤖 AI is thinking...";
|
||||||
responseBox.classList.remove("opacity-0");
|
responseBox.classList.remove("opacity-0");
|
||||||
|
|
||||||
fetch('/api/v1/haiku', {
|
fetch("/api/v1/haiku", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ 'prompt': userInput })
|
body: JSON.stringify({ prompt: userInput }),
|
||||||
})
|
})
|
||||||
.then(response => response.text())
|
.then((response) => response.text())
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
let id = parseInt(data, 10);
|
let id = parseInt(data, 10);
|
||||||
window.location.href = "/haiku/" + id;
|
window.location.href = "/haiku/" + id;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
responseText.textContent = 'Error: ' + error.message;
|
responseText.textContent = "Error: " + error.message;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,39 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %} {% block content %}
|
||||||
|
<div
|
||||||
{% block content %}
|
class="flex flex-col items-center justify-center min-h-screen bg-violet-900 text-white p-6"
|
||||||
<!DOCTYPE html>
|
>
|
||||||
<html lang="en">
|
<div
|
||||||
<head>
|
class="bg-white text-gray-900 p-8 rounded-xl shadow-lg max-w-lg w-full text-center transform transition duration-300 hover:scale-105 mb-8"
|
||||||
<meta charset="UTF-8">
|
>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Image Upload with Preview</title>
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="flex flex-col items-center justify-center min-h-screen bg-violet-900 text-white p-6">
|
|
||||||
<div class="bg-white text-gray-900 p-8 rounded-xl shadow-lg max-w-lg w-full text-center transform transition duration-300 hover:scale-105 mb-8">
|
|
||||||
|
|
||||||
<h1 class="text-3xl font-bold text-violet-700 mb-4">Upload your image</h1>
|
<h1 class="text-3xl font-bold text-violet-700 mb-4">Upload your image</h1>
|
||||||
<!-- File Upload container-->
|
<!-- File Upload container-->
|
||||||
<div id="upload-area" class="flex items-center justify-center w-full">
|
<div id="upload-area" class="flex items-center justify-center w-full">
|
||||||
<label for="dropzone-file" class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600">
|
<label
|
||||||
|
for="dropzone-file"
|
||||||
|
class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600"
|
||||||
|
>
|
||||||
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||||
<svg class="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
|
<svg
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
|
class="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 20 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
|
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400">SVG, PNG, JPG or GIF (MAX. 800x400px)</p>
|
<span class="font-semibold">Click to upload</span> or drag and drop
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
SVG, PNG, JPG or GIF (MAX. 800x400px)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input id="dropzone-file" type="file" accept="image/*" class="hidden" />
|
<input id="dropzone-file" type="file" accept="image/*" class="hidden" />
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -31,10 +42,30 @@
|
||||||
<!-- Image Preview container-->
|
<!-- Image Preview container-->
|
||||||
<div id="image-preview" class="w-full hidden">
|
<div id="image-preview" class="w-full hidden">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<img id="preview-img" src="" alt="Preview" class="w-full h-auto rounded-lg">
|
<img
|
||||||
<button id="remove-image" class="absolute top-2 right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600" title="Remove image">
|
id="preview-img"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
src=""
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
alt="Preview"
|
||||||
|
class="w-full h-auto rounded-lg"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="remove-image"
|
||||||
|
class="absolute top-2 right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600"
|
||||||
|
title="Remove image"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -45,26 +76,48 @@
|
||||||
Please upload an image first.
|
Please upload an image first.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="submit-button" type="submit" class="mt-6 bg-violet-600 hover:bg-violet-700 text-white font-bold py-2 px-4 rounded transition duration-300">
|
<button
|
||||||
|
id="submit-button"
|
||||||
|
type="submit"
|
||||||
|
class="mt-6 bg-violet-600 hover:bg-violet-700 text-white font-bold py-2 px-4 rounded transition duration-300"
|
||||||
|
>
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="response-box" class="mt-8 bg-white text-gray-900 p-6 rounded-lg shadow-lg max-w-lg w-full text-center opacity-0 transition-opacity duration-500 ease-in-out">
|
<div
|
||||||
<h2 class="text-2xl font-semibold text-violet-700">AI recognized the following:</h2>
|
id="response-box"
|
||||||
<p id="ai-response" class="text-lg text-gray-700 mt-2 italic">Waiting for input...</p>
|
class="mt-8 bg-white text-gray-900 p-6 rounded-lg shadow-lg max-w-lg w-full text-center opacity-0 transition-opacity duration-500 ease-in-out"
|
||||||
|
>
|
||||||
|
<h2 class="text-2xl font-semibold text-violet-700">
|
||||||
|
AI recognized the following:
|
||||||
|
</h2>
|
||||||
|
<p id="ai-response" class="text-lg text-gray-700 mt-2 italic">
|
||||||
|
Waiting for input...
|
||||||
|
</p>
|
||||||
<div class="flex justify-center space-x-4">
|
<div class="flex justify-center space-x-4">
|
||||||
<button id="yes-button" type="button" class="mt-6 bg-violet-600 hover:bg-violet-700 text-white font-bold py-2 px-4 rounded transition duration-300">
|
<button
|
||||||
|
id="yesButton"
|
||||||
|
type="button"
|
||||||
|
class="mt-6 bg-violet-600 hover:bg-violet-700 text-white font-bold py-2 px-4 rounded transition duration-300"
|
||||||
|
>
|
||||||
Generate Haiku
|
Generate Haiku
|
||||||
</button>
|
</button>
|
||||||
<button id="no-button" type="button" class="mt-6 bg-violet-600 hover:bg-violet-700 text-white font-bold py-2 px-4 rounded transition duration-300">
|
<button
|
||||||
|
id="noButton"
|
||||||
|
type="button"
|
||||||
|
class="mt-6 bg-violet-600 hover:bg-violet-700 text-white font-bold py-2 px-4 rounded transition duration-300"
|
||||||
|
>
|
||||||
Input new image
|
Input new image
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New generating haiku div that appears after "Yes" is clicked -->
|
<!-- New generating haiku div that appears after "Yes" is clicked -->
|
||||||
<div id="generating-haiku-box" class="mt-8 bg-white text-gray-900 p-6 rounded-lg shadow-lg max-w-lg w-full text-center hidden transition-opacity duration-500 ease-in-out">
|
<div
|
||||||
|
id="generating-haiku-box"
|
||||||
|
class="mt-8 bg-white text-gray-900 p-6 rounded-lg shadow-lg max-w-lg w-full text-center hidden transition-opacity duration-500 ease-in-out"
|
||||||
|
>
|
||||||
<h2 class="text-2xl font-semibold text-violet-700">Generating Haiku</h2>
|
<h2 class="text-2xl font-semibold text-violet-700">Generating Haiku</h2>
|
||||||
<div class="flex justify-center mt-4">
|
<div class="flex justify-center mt-4">
|
||||||
<div class="loader animate-pulse flex space-x-4">
|
<div class="loader animate-pulse flex space-x-4">
|
||||||
|
|
@ -73,12 +126,19 @@
|
||||||
<div class="w-3 h-3 bg-violet-600 rounded-full"></div>
|
<div class="w-3 h-3 bg-violet-600 rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-lg text-gray-700 mt-4 italic">Creating a beautiful haiku based on your image...</p>
|
<p class="text-lg text-gray-700 mt-4 italic">
|
||||||
|
Creating a beautiful haiku based on your image...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="generated-haiku-box" class="mt-8 bg-white text-gray-900 p-6 rounded-lg shadow-lg max-w-lg w-full text-center hidden transition-opacity duration-500 ease-in-out">
|
<div
|
||||||
<h2 class="text-2xl font-semibold text-violet-700">Red suit, vents unseen,<br>
|
id="generated-haiku-box"
|
||||||
Sus behavior, crew unsure,<br>
|
class="mt-8 bg-white text-gray-900 p-6 rounded-lg shadow-lg max-w-lg w-full text-center hidden transition-opacity duration-500 ease-in-out"
|
||||||
Vote him, task complete.</h2>
|
>
|
||||||
|
<h2 class="text-2xl font-semibold text-violet-700">
|
||||||
|
Red suit, vents unseen,<br />
|
||||||
|
Sus behavior, crew unsure,<br />
|
||||||
|
Vote him, task complete.
|
||||||
|
</h2>
|
||||||
<div class="flex justify-center mt-4">
|
<div class="flex justify-center mt-4">
|
||||||
<div class="loader animate-pulse flex space-x-4">
|
<div class="loader animate-pulse flex space-x-4">
|
||||||
<div class="w-3 h-3 bg-violet-600 rounded-full"></div>
|
<div class="w-3 h-3 bg-violet-600 rounded-full"></div>
|
||||||
|
|
@ -86,129 +146,11 @@ Vote him, task complete.</h2>
|
||||||
<div class="w-3 h-3 bg-violet-600 rounded-full"></div>
|
<div class="w-3 h-3 bg-violet-600 rounded-full"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-lg text-gray-700 mt-4 italic">Creating a beautiful haiku based on your image...</p>
|
<p class="text-lg text-gray-700 mt-4 italic">
|
||||||
</div>
|
Creating a beautiful haiku based on your image...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script src="/static/js/scan.js"></script>
|
||||||
// Get all needed elements
|
|
||||||
const dropzoneFile = document.getElementById('dropzone-file');
|
|
||||||
const uploadArea = document.getElementById('upload-area');
|
|
||||||
const imagePreview = document.getElementById('image-preview');
|
|
||||||
const previewImg = document.getElementById('preview-img');
|
|
||||||
const removeImageBtn = document.getElementById('remove-image');
|
|
||||||
const responseBox = document.getElementById('response-box');
|
|
||||||
const submitButton = document.getElementById('submit-button');
|
|
||||||
const errorMessage = document.getElementById('error-message');
|
|
||||||
const yesButton = document.getElementById('yes-button');
|
|
||||||
const noButton = document.getElementById('no-button');
|
|
||||||
const generatingHaikuBox = document.getElementById('generating-haiku-box');
|
|
||||||
const generatedHaikuBox = document.getElementById('generated-haiku-box');
|
|
||||||
let imageUploaded = false;
|
|
||||||
|
|
||||||
function handleFileSelect(event) {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
|
|
||||||
if (file && file.type.startsWith('image/')) {
|
|
||||||
// Create a URL for the selected image
|
|
||||||
const imageUrl = URL.createObjectURL(file);
|
|
||||||
|
|
||||||
// Set the image source
|
|
||||||
previewImg.src = imageUrl;
|
|
||||||
|
|
||||||
// Hide upload area and show image preview
|
|
||||||
uploadArea.classList.add('hidden');
|
|
||||||
imagePreview.classList.remove('hidden');
|
|
||||||
errorMessage.classList.add('hidden');
|
|
||||||
|
|
||||||
// Set flag that image is uploaded
|
|
||||||
imageUploaded = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeImage() {
|
|
||||||
dropzoneFile.value = '';
|
|
||||||
|
|
||||||
// Hide image
|
|
||||||
imagePreview.classList.add('hidden');
|
|
||||||
uploadArea.classList.remove('hidden');
|
|
||||||
|
|
||||||
URL.revokeObjectURL(previewImg.src);
|
|
||||||
previewImg.src = '';
|
|
||||||
|
|
||||||
imageUploaded = false;
|
|
||||||
responseBox.classList.add('opacity-0');
|
|
||||||
generatingHaikuBox.classList.add('hidden');
|
|
||||||
setTimeout(() => {
|
|
||||||
document.getElementById('ai-response').textContent = 'Waiting for input...';
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSubmit() {
|
|
||||||
if (imageUploaded) {
|
|
||||||
// Hide error
|
|
||||||
errorMessage.classList.add('hidden');
|
|
||||||
|
|
||||||
// Show response box
|
|
||||||
responseBox.classList.remove('opacity-0');
|
|
||||||
|
|
||||||
// Example response
|
|
||||||
document.getElementById('ai-response').textContent = 'Example text';
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
errorMessage.classList.remove('hidden');
|
|
||||||
|
|
||||||
uploadArea.classList.add('shake');
|
|
||||||
setTimeout(() => {
|
|
||||||
uploadArea.classList.remove('shake');
|
|
||||||
}, 600);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleYesClick() {
|
|
||||||
// Hide response box
|
|
||||||
responseBox.classList.add('opacity-0');
|
|
||||||
|
|
||||||
// Show generating haiku box first
|
|
||||||
setTimeout(() => {
|
|
||||||
responseBox.classList.add('hidden');
|
|
||||||
generatingHaikuBox.classList.remove('hidden');
|
|
||||||
|
|
||||||
// After a delay, hide generating box and show result
|
|
||||||
setTimeout(() => {
|
|
||||||
generatingHaikuBox.classList.add('hidden');
|
|
||||||
generatedHaikuBox.classList.remove('hidden');
|
|
||||||
}, 3000); // Show loading animation for 3 seconds before revealing the haiku
|
|
||||||
}, 500); // Wait for response box fade out
|
|
||||||
} function handleNoClick() {
|
|
||||||
// Reset everything
|
|
||||||
removeImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
dropzoneFile.addEventListener('change', handleFileSelect);
|
|
||||||
removeImageBtn.addEventListener('click', removeImage);
|
|
||||||
submitButton.addEventListener('click', handleSubmit);
|
|
||||||
yesButton.addEventListener('click', handleYesClick);
|
|
||||||
noButton.addEventListener('click', handleNoClick);
|
|
||||||
|
|
||||||
// Add some CSS animation
|
|
||||||
document.head.insertAdjacentHTML('beforeend', `
|
|
||||||
<style>
|
|
||||||
@keyframes shake {
|
|
||||||
0% { transform: translateX(0); }
|
|
||||||
25% { transform: translateX(-5px); }
|
|
||||||
50% { transform: translateX(5px); }
|
|
||||||
75% { transform: translateX(-5px); }
|
|
||||||
100% { transform: translateX(0); }
|
|
||||||
}
|
|
||||||
.shake {
|
|
||||||
animation: shake 0.5s ease-in-out;
|
|
||||||
border-color: #ef4444 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
`);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue