update
This commit is contained in:
parent
ac7303588f
commit
9e0f3b80b2
7 changed files with 213 additions and 2 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
136
app.py
136
app.py
|
|
@ -11,6 +11,8 @@ from flask import Flask, render_template, request, jsonify, url_for
|
||||||
from google import genai
|
from google import genai
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
from PIL import Image, PngImagePlugin
|
from PIL import Image, PngImagePlugin
|
||||||
|
import threading, time, subprocess, re
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
@ -1157,10 +1159,144 @@ def refine_prompt():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
#Tun sever
|
||||||
|
|
||||||
|
@app.route('/download_image', methods=['POST'])
|
||||||
|
def download_image():
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
data = request.get_json() or {}
|
||||||
|
url = data.get('url')
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
return jsonify({'error': 'URL is required'}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
download_url = url
|
||||||
|
|
||||||
|
# Check if it's a URL (http/https)
|
||||||
|
if url.startswith('http://') or url.startswith('https://'):
|
||||||
|
# Try to use gallery-dl to extract the image URL
|
||||||
|
try:
|
||||||
|
# -g: get URLs, -q: quiet
|
||||||
|
cmd = ['gallery-dl', '-g', '-q', url]
|
||||||
|
# Timeout to prevent hanging on slow sites
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
urls = result.stdout.strip().split('\n')
|
||||||
|
if urls and urls[0] and urls[0].startswith('http'):
|
||||||
|
download_url = urls[0]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"gallery-dl extraction failed (using direct URL): {e}")
|
||||||
|
# Fallback to using the original URL directly
|
||||||
|
|
||||||
|
# Download logic (for both direct URL and extracted URL)
|
||||||
|
if download_url.startswith('http://') or download_url.startswith('https://'):
|
||||||
|
response = requests.get(download_url, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
content_type = response.headers.get('content-type', '')
|
||||||
|
ext = '.png'
|
||||||
|
if 'image/jpeg' in content_type: ext = '.jpg'
|
||||||
|
elif 'image/webp' in content_type: ext = '.webp'
|
||||||
|
elif 'image/gif' in content_type: ext = '.gif'
|
||||||
|
else:
|
||||||
|
parsed = urlparse(download_url)
|
||||||
|
ext = os.path.splitext(parsed.path)[1] or '.png'
|
||||||
|
|
||||||
|
filename = f"{uuid.uuid4()}{ext}"
|
||||||
|
filepath = os.path.join(UPLOADS_DIR, filename)
|
||||||
|
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
rel_path = f"uploads/{filename}"
|
||||||
|
final_url = url_for('static', filename=rel_path)
|
||||||
|
|
||||||
|
return jsonify({'path': final_url, 'local_path': filepath})
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Handle local file path
|
||||||
|
# Remove quotes if present
|
||||||
|
clean_path = url.strip('"\'')
|
||||||
|
|
||||||
|
if os.path.exists(clean_path):
|
||||||
|
ext = os.path.splitext(clean_path)[1] or '.png'
|
||||||
|
filename = f"{uuid.uuid4()}{ext}"
|
||||||
|
filepath = os.path.join(UPLOADS_DIR, filename)
|
||||||
|
shutil.copy2(clean_path, filepath)
|
||||||
|
|
||||||
|
rel_path = f"uploads/{filename}"
|
||||||
|
final_url = url_for('static', filename=rel_path)
|
||||||
|
return jsonify({'path': final_url, 'local_path': filepath})
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'File path not found on server'}), 404
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error downloading image: {e}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
def pinggy_thread(port,pinggy):
|
||||||
|
|
||||||
|
server = {
|
||||||
|
"Auto": "",
|
||||||
|
"USA": "us.",
|
||||||
|
"Europe": "eu.",
|
||||||
|
"Asia": "ap.",
|
||||||
|
"South America": "br.",
|
||||||
|
"Australia": "au."
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sv = server[Sever_Pinggy]
|
||||||
|
|
||||||
|
import socket
|
||||||
|
while True:
|
||||||
|
time.sleep(0.5)
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
result = sock.connect_ex(('127.0.0.1', port))
|
||||||
|
if result == 0:
|
||||||
|
break
|
||||||
|
sock.close()
|
||||||
|
try:
|
||||||
|
if pinggy != None:
|
||||||
|
if ":" in pinggy:
|
||||||
|
pinggy, ac, ps = pinggy.split(":")
|
||||||
|
cmd = ["ssh", "-p", "443", f"-R0:localhost:{port}", "-o", "StrictHostKeyChecking=no", "-o", "ServerAliveInterval=30", f"{pinggy}@{sv}pro.pinggy.io", f'\"b:{ac}:{ps}\"']
|
||||||
|
else:
|
||||||
|
cmd = ["ssh", "-p", "443", f"-R0:localhost:{port}", "-o", "StrictHostKeyChecking=no", "-o", "ServerAliveInterval=30", f"{pinggy}@{sv}pro.pinggy.io"]
|
||||||
|
else:
|
||||||
|
cmd = ["ssh", "-p", "443", "-L4300:localhost:4300", "-o", "StrictHostKeyChecking=no", "-o", "ServerAliveInterval=30", f"-R0:localhost:{port}", "free.pinggy.io"]
|
||||||
|
process = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
|
||||||
|
for line in iter(process.stdout.readline, ''):
|
||||||
|
match = re.search(r'(https?://[^\s]+)', line)
|
||||||
|
if match:
|
||||||
|
url = match.group(1)
|
||||||
|
# Bỏ qua các link dashboard
|
||||||
|
if "dashboard.pinggy.io" in url:
|
||||||
|
continue
|
||||||
|
print(f"\033[92m🔗 Link online để sử dụng:\033[0m {url}")
|
||||||
|
if pinggy == None:
|
||||||
|
html="<div><code style='color:yellow'>Link pinggy free hoạt động trong 60phút, khởi động lại hoặc đăng ký tại [dashboard.pinggy.io] để lấy token, nhập custom pinggy trong tệp Domain_sever.txt trên drive theo cú pháp 'pinggy-{token}'</code></div>"
|
||||||
|
display(HTML(html))
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Lỗi: {e}")
|
||||||
|
|
||||||
|
def sever_flare(port, pinggy = None):
|
||||||
|
threading.Thread(target=pinggy_thread, daemon=True, args=(port,pinggy,)).start()
|
||||||
|
|
||||||
|
|
||||||
port_sever = 8888
|
port_sever = 8888
|
||||||
|
Sever_Pinggy = "Auto"
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Use ANSI green text so the startup banner stands out in terminals
|
# Use ANSI green text so the startup banner stands out in terminals
|
||||||
print("\033[32m" + "aPix Image Workspace running at:" + "\033[0m", flush=True)
|
print("\033[32m" + "aPix Image Workspace running at:" + "\033[0m", flush=True)
|
||||||
print("\033[32m" + f"http://localhost:{port_sever}" + " " + "\033[0m", flush=True)
|
print("\033[32m" + f"http://localhost:{port_sever}" + " " + "\033[0m", flush=True)
|
||||||
print("\033[32m" + f"http://127.0.0.1:{port_sever}" + "\033[0m", flush=True)
|
print("\033[32m" + f"http://127.0.0.1:{port_sever}" + "\033[0m", flush=True)
|
||||||
|
|
||||||
|
# sever_flare(port_sever, "cXPggKvHuW:sdvn:1231")
|
||||||
app.run(debug=True, port=port_sever)
|
app.run(debug=True, port=port_sever)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@
|
||||||
"gemini-3-pro-image-preview_20251125_42.png",
|
"gemini-3-pro-image-preview_20251125_42.png",
|
||||||
"gemini-3-pro-image-preview_20251125_41.png",
|
"gemini-3-pro-image-preview_20251125_41.png",
|
||||||
"gemini-3-pro-image-preview_20251125_37.png",
|
"gemini-3-pro-image-preview_20251125_37.png",
|
||||||
"gemini-3-pro-image-preview_20251125_26.png",
|
|
||||||
"gemini-3-pro-image-preview_20251125_24.png",
|
"gemini-3-pro-image-preview_20251125_24.png",
|
||||||
"generated/gemini-3-pro-image-preview_20251124_10.png",
|
"generated/gemini-3-pro-image-preview_20251124_10.png",
|
||||||
"generated/gemini-3-pro-image-preview_20251124_9.png"
|
"generated/gemini-3-pro-image-preview_20251124_9.png",
|
||||||
|
"generated/gemini-3-pro-image-preview_20251125_26.png",
|
||||||
|
"generated/gemini-3-pro-image-preview_20251129_12.png",
|
||||||
|
"generated/gemini-3-pro-image-preview_20251220_1.png"
|
||||||
]
|
]
|
||||||
|
|
@ -2,3 +2,5 @@ flask
|
||||||
google-genai
|
google-genai
|
||||||
pillow
|
pillow
|
||||||
Send2Trash
|
Send2Trash
|
||||||
|
gallery-dl
|
||||||
|
requests
|
||||||
|
|
|
||||||
|
|
@ -340,11 +340,29 @@ export function createReferenceSlotManager(imageInputGrid, options = {}) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addReferenceFromUrl(url) {
|
||||||
|
// Find first empty slot
|
||||||
|
let emptyIndex = imageSlotState.findIndex(record => !record.data);
|
||||||
|
|
||||||
|
if (emptyIndex === -1) {
|
||||||
|
if (imageSlotState.length < MAX_IMAGE_SLOTS) {
|
||||||
|
addImageSlot();
|
||||||
|
emptyIndex = imageSlotState.length - 1;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleSlotDropFromHistory(emptyIndex, url);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialize,
|
initialize,
|
||||||
getReferenceFiles,
|
getReferenceFiles,
|
||||||
getReferencePaths,
|
getReferencePaths,
|
||||||
serializeReferenceImages,
|
serializeReferenceImages,
|
||||||
setReferenceImages,
|
setReferenceImages,
|
||||||
|
addReferenceFromUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
const downloadLink = document.getElementById('download-link');
|
const downloadLink = document.getElementById('download-link');
|
||||||
const galleryGrid = document.getElementById('gallery-grid');
|
const galleryGrid = document.getElementById('gallery-grid');
|
||||||
const imageInputGrid = document.getElementById('image-input-grid');
|
const imageInputGrid = document.getElementById('image-input-grid');
|
||||||
|
const referenceUrlInput = document.getElementById('reference-url-input');
|
||||||
const imageDisplayArea = document.querySelector('.image-display-area');
|
const imageDisplayArea = document.querySelector('.image-display-area');
|
||||||
const canvasToolbar = document.querySelector('.canvas-toolbar');
|
const canvasToolbar = document.querySelector('.canvas-toolbar');
|
||||||
const sidebar = document.querySelector('.sidebar');
|
const sidebar = document.querySelector('.sidebar');
|
||||||
|
|
@ -2121,4 +2122,52 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reference URL Input Logic
|
||||||
|
if (referenceUrlInput && typeof slotManager !== 'undefined') {
|
||||||
|
referenceUrlInput.addEventListener('keydown', async (event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
const url = referenceUrlInput.value.trim();
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
referenceUrlInput.disabled = true;
|
||||||
|
const originalPlaceholder = referenceUrlInput.getAttribute('placeholder');
|
||||||
|
referenceUrlInput.setAttribute('placeholder', 'Đang tải...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/download_image', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Failed to download image');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.path) {
|
||||||
|
const success = await slotManager.addReferenceFromUrl(data.path);
|
||||||
|
if (success) {
|
||||||
|
referenceUrlInput.value = '';
|
||||||
|
} else {
|
||||||
|
alert('Không còn slot trống cho ảnh tham chiếu.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('No image path returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download error:', error);
|
||||||
|
alert(`Lỗi tải ảnh: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
referenceUrlInput.disabled = false;
|
||||||
|
referenceUrlInput.setAttribute('placeholder', originalPlaceholder);
|
||||||
|
referenceUrlInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,10 @@
|
||||||
<label>Reference Images</label>
|
<label>Reference Images</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="image-input-grid" class="image-input-grid" aria-live="polite"></div>
|
<div id="image-input-grid" class="image-input-grid" aria-live="polite"></div>
|
||||||
|
<div class="image-url-input-wrapper" style="margin-top: 0.5rem;">
|
||||||
|
<input type="text" id="reference-url-input" placeholder="Nhập URL hoặc đường dẫn ảnh..."
|
||||||
|
style="width: 100%; padding: 0.5rem; border-radius: 4px; border: 1px solid var(--border-color); background: rgba(0,0,0,0.2); color: var(--text-primary); font-size: 0.85rem;">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue