This commit is contained in:
Julian Coy 2025-02-01 17:12:09 +08:00 committed by GitHub
commit cb2544c5d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 541 additions and 4 deletions

1
.gitignore vendored
View File

@ -419,3 +419,4 @@ tags
.vscode
.github
generated_samples/
huggingface

16
Dockerfile Normal file
View File

@ -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"]

View File

@ -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),

164
demo/webui/APIChat.js Normal file
View File

@ -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 => `<div class="dropdown-item">
<span class="select-url">${url}</span>
<button class="delete-button">🗑</button>
</div>`).join('')
: '<div class="dropdown-item">No history</div>';
}
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 ? `<img src="${avatarUrl}" alt="Avatar">` : `<div class="default-avatar">${sender[0]}</div>`;
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 = `
<div class="message-header">
<span class="message-sender">${sender}</span>
<span class="message-timestamp">${timestamp}</span>
</div>
<div class="message-body"></div>
`;
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();
}
});
});

BIN
demo/webui/child.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
demo/webui/favicon.ico Normal file

Binary file not shown.

302
demo/webui/index.css Normal file
View File

@ -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 */
}

35
demo/webui/index.html Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat Interface</title>
<link rel="stylesheet" href="./index.css">
<link rel="icon" type="image/png" href="/webui/favicon.ico">
</head>
<body>
<main class="chat-container">
<div class="chat-box">
<div class="input-container">
<div class="combined-input">
<input id="apiUrlInput" class="chat-input" type="text" placeholder="Enter API URL" style="width: 100%;">
<button id="dropdownButton" class="dropdown-button"></button>
<div id="dropdownMenu" class="dropdown-menu" style="display: none;"></div>
</div>
</div>
<div id="responsesContainer" class="responses-container"></div>
<div class="input-container">
<textarea id="promptInput" class="chat-input" placeholder="Enter your prompt for image generation" rows="3"></textarea>
<button id="sendButton" class="send-button">Send</button>
</div>
<div id="errorMessage" class="error-message" style="display: none;"></div>
</div>
</main>
<script type="text/javascript" src="./APIChat.js"></script>
</body>
</html>

View File

@ -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

BIN
demo/webui/system.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
demo/webui/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB