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 => ``).join('') + : ''; + } + + 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 ? `Avatar` : `
${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 = ` +
+ ${sender} + ${timestamp} +
+
+ `; + + 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