Update preview
This commit is contained in:
parent
0b24c74ef3
commit
178a646d70
10 changed files with 129 additions and 25 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
31
README.md
31
README.md
|
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
## Tiếng Việt
|
## Tiếng Việt
|
||||||
### Giới thiệu
|
### Giới thiệu
|
||||||
aPix Image Workspace là một ứng dụng Flask đơn giản giúp bạn tạo hình ảnh dựa trên prompt, cho phép upload tài liệu tham khảo và chọn tỷ lệ khung hình, độ phân giải.
|
aPix Image Workspace là một giao diện Flask nhẹ giúp bạn tạo hình ảnh bằng API Model Gemini Image 3 Pro (Nano Banana Pro). Bạn có thể gửi prompt, upload tài liệu tham khảo và điều chỉnh tỷ lệ khung hình/độ phân giải.
|
||||||
|
|
||||||
|

|
||||||
|
Ảnh preview được lưu ngay tại `preview.jpeg` nếu bạn cần xem offline hoặc nhúng lại trong tài liệu khác.
|
||||||
|
|
||||||
### Cài đặt
|
### Cài đặt
|
||||||
1. Tạo môi trường ảo (ví dụ `python -m venv .venv`) và kích hoạt nó.
|
1. Tạo môi trường ảo (ví dụ `python -m venv .venv`) và kích hoạt nó.
|
||||||
|
|
@ -11,6 +14,12 @@ aPix Image Workspace là một ứng dụng Flask đơn giản giúp bạn tạo
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Khởi chạy nhanh bằng `run_app`
|
||||||
|
1. Đặt `GOOGLE_API_KEY` qua biến môi trường hoặc giao diện.
|
||||||
|
2. Dùng `./run_app.sh` (macOS/Linux) hoặc `run_app.bat`/`run_app.command` (Windows) để tự động tìm Python, tạo `.venv`, cài `requirements.txt` và khởi động `app.py`.
|
||||||
|
3. Mở `http://127.0.0.1:8888`, nhập prompt/tùy chọn rồi nhấn Generate.
|
||||||
|
4. Hình ảnh mới nằm trong `static/generated/`; `/gallery` thể hiện lịch sử.
|
||||||
|
|
||||||
### Sử dụng
|
### Sử dụng
|
||||||
1. Đặt biến môi trường `GOOGLE_API_KEY` với API key của Google GenAI hoặc nhập trực tiếp trong giao diện.
|
1. Đặt biến môi trường `GOOGLE_API_KEY` với API key của Google GenAI hoặc nhập trực tiếp trong giao diện.
|
||||||
2. Chạy Flask:
|
2. Chạy Flask:
|
||||||
|
|
@ -22,7 +31,10 @@ aPix Image Workspace là một ứng dụng Flask đơn giản giúp bạn tạo
|
||||||
|
|
||||||
## English
|
## English
|
||||||
### Overview
|
### Overview
|
||||||
aPix Image Workspace is a lightweight Flask frontend to Google GenAI image generation. You can submit a prompt, optional reference files, and pick aspect ratio/resolution.
|
aPix Image Workspace is a lightweight Flask frontend for Google GenAI image generation powered by the Gemini Image 3 Pro (Nano Banana Pro) model. Submit a prompt, include reference files if needed, and choose aspect ratio/resolution.
|
||||||
|
|
||||||
|

|
||||||
|
The preview screenshot is also stored as `preview.jpeg` in the repo for offline use or documentation embeds.
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
1. Create & activate a virtual environment (e.g., `python -m venv .venv`).
|
1. Create & activate a virtual environment (e.g., `python -m venv .venv`).
|
||||||
|
|
@ -30,12 +42,11 @@ aPix Image Workspace is a lightweight Flask frontend to Google GenAI image gener
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
3. macOS/Linux users can run `./run_app.sh`; Windows users can use `run_app.bat` (or `run_app.command`) to auto-detect Python, create the `.venv`, install deps, and launch the Flask server on port 8888.
|
||||||
|
|
||||||
### Basic usage
|
### Quick start via `run_app`
|
||||||
1. Supply a `GOOGLE_API_KEY` via environment variable or the UI form.
|
1. Export `GOOGLE_API_KEY` via your environment or the UI form.
|
||||||
2. Launch the app:
|
2. On macOS/Linux run `./run_app.sh` (or `run_app.command`); on Windows use `run_app.bat` or `run_app.command`.
|
||||||
```bash
|
- The script detects `python3`/`python`, creates `.venv`, installs `requirements.txt`, activates the virtualenv, and then launches `app.py`.
|
||||||
python app.py
|
3. Open `http://127.0.0.1:8888`, submit prompts/options, and click Generate.
|
||||||
```
|
4. New images appear under `static/generated/`, and `/gallery` exposes the history.
|
||||||
3. Visit `http://127.0.0.1:8888`, fill in the prompt and optional settings, then click Generate.
|
|
||||||
4. Generated assets appear in `static/generated/`, and `/gallery` exposes the list of saved images.
|
|
||||||
|
|
|
||||||
27
app.py
27
app.py
|
|
@ -3,7 +3,9 @@ import base64
|
||||||
import uuid
|
import uuid
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from send2trash import send2trash
|
||||||
from flask import Flask, render_template, request, jsonify, url_for
|
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
|
||||||
|
|
@ -145,8 +147,9 @@ def generate_image():
|
||||||
print(f"Error processing uploaded file: {e}")
|
print(f"Error processing uploaded file: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
model_name = "gemini-3-pro-image-preview"
|
||||||
response = client.models.generate_content(
|
response = client.models.generate_content(
|
||||||
model="gemini-3-pro-image-preview",
|
model=model_name,
|
||||||
contents=contents,
|
contents=contents,
|
||||||
config=types.GenerateContentConfig(
|
config=types.GenerateContentConfig(
|
||||||
response_modalities=['IMAGE'],
|
response_modalities=['IMAGE'],
|
||||||
|
|
@ -161,7 +164,25 @@ def generate_image():
|
||||||
image = Image.open(BytesIO(image_bytes))
|
image = Image.open(BytesIO(image_bytes))
|
||||||
png_info = PngImagePlugin.PngInfo()
|
png_info = PngImagePlugin.PngInfo()
|
||||||
|
|
||||||
filename = f"{uuid.uuid4()}.png"
|
date_str = datetime.now().strftime("%Y%m%d")
|
||||||
|
|
||||||
|
# Find existing files to determine next ID
|
||||||
|
search_pattern = os.path.join(GENERATED_DIR, f"{model_name}_{date_str}_*.png")
|
||||||
|
existing_files = glob.glob(search_pattern)
|
||||||
|
max_id = 0
|
||||||
|
for f in existing_files:
|
||||||
|
try:
|
||||||
|
basename = os.path.basename(f)
|
||||||
|
name_without_ext = os.path.splitext(basename)[0]
|
||||||
|
id_part = name_without_ext.split('_')[-1]
|
||||||
|
id_num = int(id_part)
|
||||||
|
if id_num > max_id:
|
||||||
|
max_id = id_num
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
next_id = max_id + 1
|
||||||
|
filename = f"{model_name}_{date_str}_{next_id}.png"
|
||||||
filepath = os.path.join(GENERATED_DIR, filename)
|
filepath = os.path.join(GENERATED_DIR, filename)
|
||||||
rel_path = os.path.join('generated', filename)
|
rel_path = os.path.join('generated', filename)
|
||||||
image_url = url_for('static', filename=rel_path)
|
image_url = url_for('static', filename=rel_path)
|
||||||
|
|
@ -209,7 +230,7 @@ def delete_image():
|
||||||
|
|
||||||
if os.path.exists(filepath):
|
if os.path.exists(filepath):
|
||||||
try:
|
try:
|
||||||
os.remove(filepath)
|
send2trash(filepath)
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
|
||||||
BIN
preview.jpeg
Normal file
BIN
preview.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 358 KiB |
|
|
@ -1,3 +1,4 @@
|
||||||
flask
|
flask
|
||||||
google-genai
|
google-genai
|
||||||
pillow
|
pillow
|
||||||
|
Send2Trash
|
||||||
|
|
|
||||||
34
run_app.bat
Normal file
34
run_app.bat
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
if defined PYTHON_BIN goto :found_python
|
||||||
|
|
||||||
|
for /f "delims=" %%P in ('where python3 2^>nul') do (
|
||||||
|
set "PYTHON_BIN=%%~P"
|
||||||
|
goto :found_python
|
||||||
|
)
|
||||||
|
for /f "delims=" %%P in ('where python 2^>nul') do (
|
||||||
|
set "PYTHON_BIN=%%~P"
|
||||||
|
goto :found_python
|
||||||
|
)
|
||||||
|
for /f "delims=" %%P in ('py -3 -c "import sys; print(sys.executable)" 2^>nul') do (
|
||||||
|
set "PYTHON_BIN=%%~P"
|
||||||
|
goto :found_python
|
||||||
|
)
|
||||||
|
|
||||||
|
echo No python interpreter found in PATH.
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:found_python
|
||||||
|
if not exist ".venv" (
|
||||||
|
"%PYTHON_BIN%" -m venv .venv
|
||||||
|
)
|
||||||
|
|
||||||
|
call .venv\Scripts\activate.bat
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
call .venv\Scripts\python.exe app.py
|
||||||
|
endlocal
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
# /opt/miniconda3/bin/python is required by the user
|
|
||||||
#!/bin/zsh
|
#!/bin/zsh
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
PYTHON_BIN="/opt/miniconda3/bin/python"
|
# Prefer python3 but fall back to python; allow overriding via env
|
||||||
|
PYTHON_BIN="${PYTHON_BIN:-$(command -v python3 || command -v python)}"
|
||||||
|
if [[ -z "$PYTHON_BIN" ]]; then
|
||||||
|
echo "No python interpreter found in PATH."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Create a virtual environment if missing, then activate it
|
# Create a virtual environment if missing, then activate it
|
||||||
if [[ ! -d ".venv" ]]; then
|
if [[ ! -d ".venv" ]]; then
|
||||||
|
|
|
||||||
24
run_app.sh
Executable file
24
run_app.sh
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Prefer python3 but fall back to python; allow override via environment
|
||||||
|
PYTHON_BIN="${PYTHON_BIN:-$(command -v python3 || command -v python)}"
|
||||||
|
if [[ -z "$PYTHON_BIN" ]]; then
|
||||||
|
echo "No python interpreter found in PATH."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create a virtual environment if missing, then activate it
|
||||||
|
if [[ ! -d ".venv" ]]; then
|
||||||
|
"$PYTHON_BIN" -m venv .venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# Ensure dependencies are available
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Start the Flask app on port 8888
|
||||||
|
exec .venv/bin/python app.py
|
||||||
|
|
@ -61,7 +61,7 @@ export function createGallery({ galleryGrid, onSelect }) {
|
||||||
deleteBtn.title = 'Delete image';
|
deleteBtn.title = 'Delete image';
|
||||||
deleteBtn.addEventListener('click', async (e) => {
|
deleteBtn.addEventListener('click', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!confirm('Are you sure you want to delete this image?')) return;
|
|
||||||
|
|
||||||
const filename = imageUrl.split('/').pop().split('?')[0];
|
const filename = imageUrl.split('/').pop().split('?')[0];
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600;700&family=Playwrite+AU+SA&display=swap" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600;700&family=Playwrite+AU+SA&display=swap"
|
||||||
|
rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -59,8 +61,8 @@
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="resolution">Resolution</label>
|
<label for="resolution">Resolution</label>
|
||||||
<select id="resolution">
|
<select id="resolution">
|
||||||
<option value="2K" selected>2K</option>
|
<option value="1K" selected>1K</option>
|
||||||
<option value="1K">1K</option>
|
<option value="2K">2K</option>
|
||||||
<option value="4K">4K</option>
|
<option value="4K">4K</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -101,11 +103,18 @@
|
||||||
<div class="canvas-toolbar" role="toolbar">
|
<div class="canvas-toolbar" role="toolbar">
|
||||||
<button type="button" class="canvas-btn" data-action="zoom-out">−</button>
|
<button type="button" class="canvas-btn" data-action="zoom-out">−</button>
|
||||||
<button type="button" class="canvas-btn" data-action="zoom-in">+</button>
|
<button type="button" class="canvas-btn" data-action="zoom-in">+</button>
|
||||||
<button type="button" class="canvas-btn icon-btn" data-action="zoom-reset" aria-label="Reset view">↺</button>
|
<button type="button" class="canvas-btn icon-btn" data-action="zoom-reset"
|
||||||
<a id="download-link" href="#" download="gemini_image.png" class="canvas-btn icon-btn" aria-label="Download image">
|
aria-label="Reset view">↺</button>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<a id="download-link" href="#" download="gemini_image.png" class="canvas-btn icon-btn"
|
||||||
<path d="M12.5535 16.5061C12.4114 16.6615 12.2106 16.75 12 16.75C11.7894 16.75 11.5886 16.6615 11.4465 16.5061L7.44648 12.1311C7.16698 11.8254 7.18822 11.351 7.49392 11.0715C7.79963 10.792 8.27402 10.8132 8.55352 11.1189L11.25 14.0682V3C11.25 2.58579 11.5858 2.25 12 2.25C12.4142 2.25 12.75 2.58579 12.75 3V14.0682L15.4465 11.1189C15.726 10.8132 16.2004 10.792 16.5061 11.0715C16.8118 11.351 16.833 11.8254 16.5535 12.1311L12.5535 16.5061Z" fill="currentColor"/>
|
aria-label="Download image">
|
||||||
<path d="M3.75 15C3.75 14.5858 3.41422 14.25 3 14.25C2.58579 14.25 2.25 14.5858 2.25 15V15.0549C2.24998 16.4225 2.24996 17.5248 2.36652 18.3918C2.48754 19.2919 2.74643 20.0497 3.34835 20.6516C3.95027 21.2536 4.70814 21.5125 5.60825 21.6335C6.47522 21.75 7.57754 21.75 8.94513 21.75H15.0549C16.4225 21.75 17.5248 21.75 18.3918 21.6335C19.2919 21.5125 20.0497 21.2536 20.6517 20.6516C21.2536 20.0497 21.5125 19.2919 21.6335 18.3918C21.75 17.5248 21.75 16.4225 21.75 15.0549V15C21.75 14.5858 21.4142 14.25 21 14.25C20.5858 14.25 20.25 14.5858 20.25 15C20.25 16.4354 20.2484 17.4365 20.1469 18.1919C20.0482 18.9257 19.8678 19.3142 19.591 19.591C19.3142 19.8678 18.9257 20.0482 18.1919 20.1469C17.4365 20.2484 16.4354 20.25 15 20.25H9C7.56459 20.25 6.56347 20.2484 5.80812 20.1469C5.07435 20.0482 4.68577 19.8678 4.40901 19.591C4.13225 19.3142 3.9518 18.9257 3.85315 18.1919C3.75159 17.4365 3.75 16.4354 3.75 15Z" fill="currentColor"/>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M12.5535 16.5061C12.4114 16.6615 12.2106 16.75 12 16.75C11.7894 16.75 11.5886 16.6615 11.4465 16.5061L7.44648 12.1311C7.16698 11.8254 7.18822 11.351 7.49392 11.0715C7.79963 10.792 8.27402 10.8132 8.55352 11.1189L11.25 14.0682V3C11.25 2.58579 11.5858 2.25 12 2.25C12.4142 2.25 12.75 2.58579 12.75 3V14.0682L15.4465 11.1189C15.726 10.8132 16.2004 10.792 16.5061 11.0715C16.8118 11.351 16.833 11.8254 16.5535 12.1311L12.5535 16.5061Z"
|
||||||
|
fill="currentColor" />
|
||||||
|
<path
|
||||||
|
d="M3.75 15C3.75 14.5858 3.41422 14.25 3 14.25C2.58579 14.25 2.25 14.5858 2.25 15V15.0549C2.24998 16.4225 2.24996 17.5248 2.36652 18.3918C2.48754 19.2919 2.74643 20.0497 3.34835 20.6516C3.95027 21.2536 4.70814 21.5125 5.60825 21.6335C6.47522 21.75 7.57754 21.75 8.94513 21.75H15.0549C16.4225 21.75 17.5248 21.75 18.3918 21.6335C19.2919 21.5125 20.0497 21.2536 20.6517 20.6516C21.2536 20.0497 21.5125 19.2919 21.6335 18.3918C21.75 17.5248 21.75 16.4225 21.75 15.0549V15C21.75 14.5858 21.4142 14.25 21 14.25C20.5858 14.25 20.25 14.5858 20.25 15C20.25 16.4354 20.2484 17.4365 20.1469 18.1919C20.0482 18.9257 19.8678 19.3142 19.591 19.591C19.3142 19.8678 18.9257 20.0482 18.1919 20.1469C17.4365 20.2484 16.4354 20.25 15 20.25H9C7.56459 20.25 6.56347 20.2484 5.80812 20.1469C5.07435 20.0482 4.68577 19.8678 4.40901 19.591C4.13225 19.3142 3.9518 18.9257 3.85315 18.1919C3.75159 17.4365 3.75 16.4354 3.75 15Z"
|
||||||
|
fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -132,4 +141,4 @@
|
||||||
<script type="module" src="{{ url_for('static', filename='script.js') }}"></script>
|
<script type="module" src="{{ url_for('static', filename='script.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Loading…
Reference in a new issue