diff --git a/.gitignore b/.gitignore
index 229e98c..656557b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -418,4 +418,5 @@ tags
[._]*.un~
.vscode
.github
-generated_samples/
\ No newline at end of file
+generated_samples/
+huggingface
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..89dbf52
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,16 @@
+# Use the PyTorch base image
+FROM pytorch/pytorch:latest
+
+# Set the working directory inside the container
+WORKDIR /app
+
+# Copy the current directory into the container
+COPY . /app
+
+# Install necessary Python packages
+RUN pip install -e . && \
+ pip install fastapi python-multipart uvicorn
+
+# Set the entrypoint for the container to launch your FastAPI app
+CMD ["python", "demo/fastapi_app.py"]
+
diff --git a/demo/fastapi_app.py b/demo/fastapi_app.py
index c2e5710..0a0afd9 100644
--- a/demo/fastapi_app.py
+++ b/demo/fastapi_app.py
@@ -1,16 +1,23 @@
from fastapi import FastAPI, File, Form, UploadFile, HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
+from fastapi.staticfiles import StaticFiles
import torch
from transformers import AutoConfig, AutoModelForCausalLM
from janus.models import MultiModalityCausalLM, VLChatProcessor
from PIL import Image
import numpy as np
import io
+import os
+
+# Resolve absolute path based on the script's location
+BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # This gets "demo/"
+WEBUI_DIR = os.path.join(BASE_DIR, "webui") # Moves up to the project root
app = FastAPI()
+app.mount("/webui", StaticFiles(directory=WEBUI_DIR, html=True), name="webui")
# Load model and processor
-model_path = "deepseek-ai/Janus-1.3B"
+model_path = os.getenv("MODEL_NAME", "deepseek-ai/Janus-1.3B")
config = AutoConfig.from_pretrained(model_path)
language_config = config.language_config
language_config._attn_implementation = 'eager'
@@ -63,7 +70,7 @@ def multimodal_understanding(image_data, question, seed, top_p, temperature):
return answer
-@app.post("/understand_image_and_question/")
+@app.post("/understand_image_and_question")
async def understand_image_and_question(
file: UploadFile = File(...),
question: str = Form(...),
@@ -152,7 +159,7 @@ def generate_image(prompt, seed, guidance):
return [Image.fromarray(images[i]).resize((1024, 1024), Image.LANCZOS) for i in range(parallel_size)]
-@app.post("/generate_images/")
+@app.post("/generate_images")
async def generate_images(
prompt: str = Form(...),
seed: int = Form(None),
diff --git a/demo/webui/APIChat.js b/demo/webui/APIChat.js
new file mode 100644
index 0000000..0a53961
--- /dev/null
+++ b/demo/webui/APIChat.js
@@ -0,0 +1,164 @@
+document.addEventListener('DOMContentLoaded', function () {
+ const apiUrlInput = document.getElementById('apiUrlInput');
+ const dropdownButton = document.getElementById('dropdownButton');
+ const dropdownMenu = document.getElementById('dropdownMenu');
+ const responsesContainer = document.getElementById('responsesContainer');
+ const promptInput = document.getElementById('promptInput');
+ const sendButton = document.getElementById('sendButton');
+ const errorMessage = document.getElementById('errorMessage');
+
+ const defaultApiUrl = window.location.origin + '/generate_images';
+
+ let apiUrl = localStorage.getItem('lastUsedUrl') || defaultApiUrl;
+ let urlHistory = JSON.parse(localStorage.getItem('urlHistory')) || [];
+
+ // If urlHistory is empty, add the default localhost URL
+ if (urlHistory.length === 0) {
+ saveUrl(defaultApiUrl)
+ }
+
+ apiUrlInput.value = apiUrl;
+ updateDropdown();
+
+ function updateDropdown() {
+ dropdownMenu.innerHTML = urlHistory.length
+ ? urlHistory.map(url => `
+ ${url}
+
+
`).join('')
+ : 'No history
';
+ }
+
+ function addMessage(sender, body, avatarUrl) {
+ const messageDiv = document.createElement('div');
+ messageDiv.classList.add('chat-message');
+
+ const avatarDiv = document.createElement('div');
+ avatarDiv.classList.add('message-avatar');
+ avatarDiv.innerHTML = avatarUrl ? `
` : `${sender[0]}
`;
+
+ const messageContent = document.createElement('div');
+ messageContent.classList.add('message-content');
+
+ // Generate timestamp
+ const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+
+ messageContent.innerHTML = `
+
+
+ `;
+
+ const messageBody = messageContent.querySelector('.message-body');
+ if (typeof body === 'string') {
+ messageBody.textContent = body;
+ } else {
+ messageBody.appendChild(body);
+ }
+
+ messageDiv.appendChild(avatarDiv);
+ messageDiv.appendChild(messageContent);
+ responsesContainer.appendChild(messageDiv);
+ responsesContainer.scrollTop = responsesContainer.scrollHeight;
+ }
+
+
+ dropdownButton.addEventListener('click', () => {
+ dropdownMenu.style.display = dropdownMenu.style.display === 'none' ? 'block' : 'none';
+ });
+
+ dropdownMenu.addEventListener('click', (e) => {
+ if (e.target.classList.contains('select-url')) {
+ apiUrl = e.target.textContent;
+ apiUrlInput.value = apiUrl;
+ saveUrl(apiUrl);
+ dropdownMenu.style.display = 'none';
+ } else if (e.target.classList.contains('delete-button')) {
+ const parent = e.target.closest('.dropdown-item');
+ const urlToDelete = parent.querySelector('.select-url').textContent;
+ urlHistory = urlHistory.filter(url => url !== urlToDelete);
+ localStorage.setItem('urlHistory', JSON.stringify(urlHistory));
+ updateDropdown();
+ }
+ });
+
+ apiUrlInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ apiUrl = apiUrlInput.value.trim(); // Ensure the latest value is saved
+ saveUrl(apiUrl);
+ }
+ });
+
+ function saveUrl(url) {
+ if (!url.trim()) return;
+
+ // Set the last used URL
+ localStorage.setItem('lastUsedUrl', url);
+
+ // Add to history if it doesn't exist
+ if (!urlHistory.includes(url)) {
+ urlHistory.push(url);
+ localStorage.setItem('urlHistory', JSON.stringify(urlHistory));
+ updateDropdown();
+ }
+
+ addMessage('System', `API URL set to: ${url}`, './system.png');
+ }
+ async function generateImage() {
+ const prompt = promptInput.value.trim();
+ if (!prompt) return;
+
+ addMessage('You', prompt, './user.png');
+ promptInput.value = '';
+ sendButton.disabled = true;
+ sendButton.textContent = 'Generating...';
+ errorMessage.style.display = 'none';
+
+ try {
+ const params = new URLSearchParams();
+ params.set('prompt', prompt);
+ params.set('seed', Math.floor(Math.random() * 1000000).toString());
+ params.set('guidance', '5');
+
+ const response = await fetch(apiUrl, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: params.toString(),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`Failed to generate image. Status: ${response.status}, Response: ${errorText}`);
+ }
+
+ const blob = await response.blob();
+ const img = document.createElement('img');
+ img.src = URL.createObjectURL(blob);
+ img.style.maxWidth = '100%';
+ img.style.borderRadius = '8px';
+
+ addMessage('AI', img, './child.jpg');
+ } catch (err) {
+ addMessage('System', `Error generating image: ${err.message}`, './system.png');
+ errorMessage.textContent = `Error generating image: ${err.message}`;
+ errorMessage.style.display = 'block';
+ } finally {
+ sendButton.disabled = false;
+ sendButton.textContent = 'Send';
+ }
+ }
+
+ sendButton.addEventListener('click', generateImage);
+
+ promptInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ generateImage();
+ }
+ });
+});
\ No newline at end of file
diff --git a/demo/webui/child.jpg b/demo/webui/child.jpg
new file mode 100644
index 0000000..87867bd
Binary files /dev/null and b/demo/webui/child.jpg differ
diff --git a/demo/webui/favicon.ico b/demo/webui/favicon.ico
new file mode 100644
index 0000000..0865eee
Binary files /dev/null and b/demo/webui/favicon.ico differ
diff --git a/demo/webui/index.css b/demo/webui/index.css
new file mode 100644
index 0000000..fb72baa
--- /dev/null
+++ b/demo/webui/index.css
@@ -0,0 +1,302 @@
+/* Apply border-box globally */
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+/* Ensure full-screen, no scrolling */
+html, body {
+ height: 100%;
+ width: 100vw;
+ margin: 0;
+ padding: 0;
+ overflow: hidden; /* Prevent scrolling */
+ background-color: black; /* Match chat background */
+ font-family: Arial, sans-serif; /* Improve readability */
+}
+
+/* Chat container setup */
+.chat-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
+}
+
+/* Center chat box */
+.chat-box {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 40vw; /* 40% of viewport width */
+ background-color: #000;
+ overflow: hidden;
+ margin: 0 auto; /* Center horizontally */
+ padding: 20px; /* Add padding */
+}
+
+/* Response messages container */
+.responses-container {
+ overflow-y: auto;
+ height: calc(100% - 120px); /* Adjust height to account for input container */
+ width: 100%;
+ border-radius: 0;
+ border: 1px solid #ccc;
+ background-color: #1e1e1e;
+ margin-bottom: 10px; /* Space between responses and input */
+}
+
+/* Mobile responsiveness */
+@media (max-width: 768px) {
+ .chat-box {
+ width: 100vw; /* Full width on mobile */
+ padding: 10px;
+ }
+}
+
+/* Input container */
+.input-container {
+ display: flex;
+ padding: 10px;
+ width: 100%;
+ background-color: #242424;
+ flex-shrink: 0; /* Prevent shrinking */
+}
+
+/* Individual chat message */
+.chat-message {
+ display: flex;
+ align-items: flex-start; /* Align items to the top */
+ margin-bottom: 5px;
+ padding: 10px;
+ word-wrap: break-word;
+ position: relative; /* For positioning options popup */
+}
+
+/* Show message options on hover */
+.chat-message:hover .message-options {
+ display: flex;
+}
+
+/* Avatar styling */
+.message-avatar img {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ margin-right: 10px;
+}
+
+.message-avatar .default-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background-color: #ccc;
+ color: #fff;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: bold;
+ margin-right: 10px;
+}
+
+/* Message content */
+.message-content {
+ padding: 0;
+ width: auto;
+ max-width: 80%;
+ margin: 0;
+}
+
+/* Sender name */
+.message-sender {
+ font-weight: bold;
+ margin-bottom: 2px;
+ color: var(--arkavo-orange, red);
+}
+
+/* Timestamp */
+.message-timestamp {
+ font-size: 0.8em;
+ color: #888;
+ margin-left: 8px;
+}
+
+/* Message body */
+.message-body {
+ background-color: #333;
+ color: #eee;
+ padding: 8px 12px;
+ border-radius: 8px;
+ word-wrap: break-word;
+}
+
+/* Message options */
+.message-options {
+ display: none;
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ background-color: #444;
+ border-radius: 4px;
+ padding: 4px;
+ gap: 4px;
+}
+
+/* Option button */
+.option-button {
+ background-color: #555;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ padding: 4px 8px;
+ cursor: pointer;
+}
+
+.option-button:hover {
+ background-color: #666;
+}
+
+/* Chat input field */
+.chat-input {
+ flex: 1;
+ padding: 10px;
+ border: 1px solid #444;
+ border-radius: 0px;
+ resize: none;
+ font-size: 1.2rem;
+ margin-right: 10px;
+ background-color: #333;
+ color: #fff;
+}
+
+/* Send button */
+.send-button {
+ padding: 10px 15px;
+ background-color: var(--arkavo-dark-orange, red);
+ color: #fff;
+ border: none;
+ font-size: 1.2rem;
+ border-radius: 8px;
+ cursor: pointer;
+}
+
+.send-button:hover {
+ background-color: darkred;
+}
+
+/* Combined input field */
+.combined-input {
+ position: relative;
+ align-items: center;
+ font-size: 1.2rem;
+ width: 100%;
+}
+
+.combined-input .chat-input {
+ flex: 1;
+ padding-right: 40px;
+ border: 1px solid #ccc;
+ font-size: 1.2rem;
+ border-radius: 4px;
+ padding: 8px;
+}
+
+/* Dropdown button */
+.dropdown-button {
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 100%;
+ width: 40px;
+ color: #333;
+ font-size: 1.2rem;
+ background-color: #f0f0f0;
+ border: 1px solid #ccc;
+ border-left: none;
+ border-radius: 0 4px 4px 0;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.dropdown-button:hover {
+ background-color: #e0e0e0;
+}
+
+/* Dropdown menu */
+.dropdown-menu {
+ position: absolute;
+ background: black;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ max-height: 200px;
+ overflow-y: auto;
+ font-size: 1.2rem;
+ width: 100%;
+ z-index: 2000;
+ box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
+}
+
+/* Dropdown item */
+.dropdown-item {
+ display: flex;
+ font-size: 1.2rem;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px;
+ background: #333;
+ color: #f0f0f0;
+ cursor: pointer;
+ border-bottom: 1px solid #eee;
+}
+
+.dropdown-item:hover {
+ background: #f0f0f0;
+ color: #333;
+}
+
+/* Delete button */
+.delete-button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 1.2rem;
+ color: red;
+}
+
+.delete-button:hover {
+ color: darkred;
+}
+
+/* Scrollbar for Webkit browsers (Chrome, Edge, Safari) */
+::-webkit-scrollbar {
+ width: 8px; /* Adjust width */
+ height: 8px; /* Adjust height for horizontal scrollbars */
+}
+
+/* Scrollbar track (background) */
+::-webkit-scrollbar-track {
+ background: #1a1a1a; /* Dark background */
+ border-radius: 4px; /* Rounded corners */
+}
+
+/* Scrollbar thumb (draggable handle) */
+::-webkit-scrollbar-thumb {
+ background: #666; /* Greyish thumb */
+ border-radius: 4px; /* Rounded corners */
+ transition: background 0.3s ease-in-out;
+}
+
+/* Hover effect */
+::-webkit-scrollbar-thumb:hover {
+ background: #888; /* Lighter grey on hover */
+}
+
+/* Firefox Scrollbar */
+* {
+ scrollbar-width: thin; /* Thinner scrollbar */
+ scrollbar-color: #666 #1a1a1a; /* Thumb color, Track color */
+}
\ No newline at end of file
diff --git a/demo/webui/index.html b/demo/webui/index.html
new file mode 100644
index 0000000..52418f8
--- /dev/null
+++ b/demo/webui/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Chat Interface
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/webui/start_docker_environment.sh b/demo/webui/start_docker_environment.sh
new file mode 100644
index 0000000..58b5103
--- /dev/null
+++ b/demo/webui/start_docker_environment.sh
@@ -0,0 +1,12 @@
+# This command will load a Bash terminal with the same installations as the Docker file
+# Useful for debugging the WebUI
+docker run -it --rm \
+ -p 8000:8000 \
+ -v huggingface:/root/.cache/huggingface \
+ -v $(pwd)/../..:/app \
+ -w /app \
+ --gpus all \
+ --name deepseek_janus \
+ -e MODEL_NAME=deepseek-ai/Janus-1.3B \
+ --entrypoint bash \
+ julianfl0w/janus:latest
diff --git a/demo/webui/system.png b/demo/webui/system.png
new file mode 100755
index 0000000..a4d346a
Binary files /dev/null and b/demo/webui/system.png differ
diff --git a/demo/webui/user.png b/demo/webui/user.png
new file mode 100644
index 0000000..322cb32
Binary files /dev/null and b/demo/webui/user.png differ