mirror of
https://github.com/vladmandic/automatic
synced 2026-09-19 17:24:32 +02:00
gallery video support
This commit is contained in:
+1
-1
@@ -11,7 +11,7 @@
|
||||
## Update for 2024-03-23
|
||||
|
||||
- **Features**:
|
||||
- **Gallery**:
|
||||
- **Gallery**: list, preview, search through all your images and videos!
|
||||
implemented as infinite-scroll with client-side-caching and lazy-loading while being fully async and non-blocking
|
||||
search or sort by path, name, size, width, height, mtime or any image metadata item, also with extended syntax like *width > 1000*
|
||||
*settings*: optional additional user-defined folders, thumbnails in fixed or variable aspect-ratio
|
||||
|
||||
@@ -100,8 +100,8 @@ async function delayFetchThumb(fn) {
|
||||
}
|
||||
const json = await res.json();
|
||||
outstanding--;
|
||||
if (!res || !json || json.error) {
|
||||
console.error(json.error);
|
||||
if (!res || !json || json.error || Object.keys(json).length === 0) {
|
||||
if (json.error) console.error(json.error);
|
||||
return undefined;
|
||||
}
|
||||
return json;
|
||||
@@ -125,7 +125,7 @@ class GalleryFile extends HTMLElement {
|
||||
async connectedCallback() {
|
||||
if (this.shadow.children.length > 0) return;
|
||||
const ext = this.name.split('.').pop().toLowerCase();
|
||||
if (!['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) return;
|
||||
if (!['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'mp4'].includes(ext)) return;
|
||||
this.hash = await getHash(`${this.folder}/${this.name}/${this.size}/${this.mtime}`); // eslint-disable-line no-use-before-define
|
||||
const style = document.createElement('style');
|
||||
const width = opts.browser_fixed_width ? `${opts.extra_networks_card_size}px` : 'unset';
|
||||
@@ -158,7 +158,7 @@ class GalleryFile extends HTMLElement {
|
||||
}
|
||||
};
|
||||
let ok = true;
|
||||
if (cache) {
|
||||
if (cache && cache.img) {
|
||||
img.src = cache.img;
|
||||
this.exif = cache.exif;
|
||||
this.width = cache.width;
|
||||
|
||||
+47
-20
@@ -21,7 +21,7 @@ OPTS_FOLDERS = [
|
||||
"outdir_img2img_samples",
|
||||
"outdir_control_samples",
|
||||
"outdir_extras_samples",
|
||||
"outdir_save"
|
||||
"outdir_save",
|
||||
"outdir_video",
|
||||
"outdir_init_images",
|
||||
"outdir_grids",
|
||||
@@ -73,6 +73,29 @@ class ConnectionManager:
|
||||
def register_api(app: FastAPI): # register api
|
||||
manager = ConnectionManager()
|
||||
|
||||
def get_video_thumbnail(filepath):
|
||||
from modules.ui_control_helpers import get_video_params
|
||||
try:
|
||||
frames, fps, duration, width, height, codec, frame = get_video_params(filepath, capture=True)
|
||||
h = shared.opts.extra_networks_card_size
|
||||
w = shared.opts.extra_networks_card_size if shared.opts.browser_fixed_width else width * h // height
|
||||
frame = frame.convert('RGB')
|
||||
frame.thumbnail((w, h), Image.Resampling.HAMMING)
|
||||
buffered = io.BytesIO()
|
||||
frame.save(buffered, format='jpeg')
|
||||
data_url = f'data:image/jpeg;base64,{base64.b64encode(buffered.getvalue()).decode("ascii")}'
|
||||
frame.close()
|
||||
content = {
|
||||
'exif': f'Codec: {codec}, Frames: {frames}, Duration: {duration:.2f} sec, FPS: {fps:.2f}',
|
||||
'data': data_url,
|
||||
'width': width,
|
||||
'height': height,
|
||||
}
|
||||
return content
|
||||
except Exception as e:
|
||||
shared.log.error(f'Gallery video: file="{filepath}" {e}')
|
||||
return {}
|
||||
|
||||
@app.get('/sdapi/v1/browser/folders', response_model=List[str])
|
||||
def get_folders():
|
||||
folders = [shared.opts.data.get(f, '') for f in OPTS_FOLDERS]
|
||||
@@ -92,24 +115,28 @@ def register_api(app: FastAPI): # register api
|
||||
async def get_thumb(file: str):
|
||||
try:
|
||||
decoded = unquote(file)
|
||||
image = Image.open(decoded)
|
||||
geninfo, _items = images.read_info_from_image(image)
|
||||
h = shared.opts.extra_networks_card_size
|
||||
w = shared.opts.extra_networks_card_size if shared.opts.browser_fixed_width else image.width * h // image.height
|
||||
width, height = image.width, image.height
|
||||
image = image.convert('RGB')
|
||||
image.thumbnail((w, h), Image.Resampling.HAMMING)
|
||||
buffered = io.BytesIO()
|
||||
image.save(buffered, format='jpeg')
|
||||
data_url = f'data:image/jpeg;base64,{base64.b64encode(buffered.getvalue()).decode("ascii")}'
|
||||
image.close()
|
||||
content = {
|
||||
'exif': geninfo,
|
||||
'data': data_url,
|
||||
'width': width,
|
||||
'height': height,
|
||||
}
|
||||
return JSONResponse(content=content)
|
||||
if decoded.lower().endswith('.mp4'):
|
||||
content = get_video_thumbnail(decoded)
|
||||
return JSONResponse(content=content)
|
||||
else:
|
||||
image = Image.open(decoded)
|
||||
geninfo, _items = images.read_info_from_image(image)
|
||||
h = shared.opts.extra_networks_card_size
|
||||
w = shared.opts.extra_networks_card_size if shared.opts.browser_fixed_width else image.width * h // image.height
|
||||
width, height = image.width, image.height
|
||||
image = image.convert('RGB')
|
||||
image.thumbnail((w, h), Image.Resampling.HAMMING)
|
||||
buffered = io.BytesIO()
|
||||
image.save(buffered, format='jpeg')
|
||||
data_url = f'data:image/jpeg;base64,{base64.b64encode(buffered.getvalue()).decode("ascii")}'
|
||||
image.close()
|
||||
content = {
|
||||
'exif': geninfo,
|
||||
'data': data_url,
|
||||
'width': width,
|
||||
'height': height,
|
||||
}
|
||||
return JSONResponse(content=content)
|
||||
except Exception as e:
|
||||
shared.log.error(f'Gallery: {file} {e}')
|
||||
content = { 'error': str(e) }
|
||||
@@ -135,7 +162,7 @@ def register_api(app: FastAPI): # register api
|
||||
await manager.send(ws, dct)
|
||||
await manager.send(ws, '#END#')
|
||||
t1 = time.time()
|
||||
shared.log.debug(f'Gallery: folder={folder} files={numFiles} time={t1-t0:.3f}')
|
||||
shared.log.debug(f'Gallery: folder="{folder}" files={numFiles} time={t1-t0:.3f}')
|
||||
except WebSocketDisconnect:
|
||||
debug('Browser WS unexpected disconnect')
|
||||
manager.disconnect(ws)
|
||||
|
||||
@@ -778,6 +778,14 @@ class Options:
|
||||
return
|
||||
return super(Options, self).__setattr__(key, value) # pylint: disable=super-with-arguments
|
||||
|
||||
def get(self, item):
|
||||
if self.data is not None:
|
||||
if item in self.data:
|
||||
return self.data[item]
|
||||
if item in self.data_labels:
|
||||
return self.data_labels[item].default
|
||||
return super(Options, self).__getattribute__(item) # pylint: disable=super-with-arguments
|
||||
|
||||
def __getattr__(self, item):
|
||||
if self.data is not None:
|
||||
if item in self.data:
|
||||
|
||||
@@ -71,21 +71,31 @@ def display_units(num_units):
|
||||
return (num_units * [gr.update(visible=True)]) + ((max_units - num_units) * [gr.update(visible=False)])
|
||||
|
||||
|
||||
def get_video_params(filepath: str, capture: bool = False):
|
||||
import cv2
|
||||
from modules.control.util import decode_fourcc
|
||||
video = cv2.VideoCapture(filepath)
|
||||
if not video.isOpened():
|
||||
msg = f'Control: video open failed: path="{filepath}"'
|
||||
shared.log.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
fps = video.get(cv2.CAP_PROP_FPS)
|
||||
duration = float(frames) / fps
|
||||
w, h = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)), int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
codec = decode_fourcc(video.get(cv2.CAP_PROP_FOURCC))
|
||||
frame = None
|
||||
if capture:
|
||||
_status, frame = video.read()
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
frame = Image.fromarray(frame)
|
||||
video.release()
|
||||
return frames, fps, duration, w, h, codec, frame
|
||||
|
||||
|
||||
def get_video(filepath: str):
|
||||
try:
|
||||
import cv2
|
||||
from modules.control.util import decode_fourcc
|
||||
video = cv2.VideoCapture(filepath)
|
||||
if not video.isOpened():
|
||||
msg = f'Control: video open failed: path="{filepath}"'
|
||||
shared.log.error(msg)
|
||||
return msg
|
||||
frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
fps = video.get(cv2.CAP_PROP_FPS)
|
||||
duration = float(frames) / fps
|
||||
w, h = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)), int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
codec = decode_fourcc(video.get(cv2.CAP_PROP_FOURCC))
|
||||
video.release()
|
||||
frames, fps, duration, w, h, codec, _cap = get_video_params(filepath)
|
||||
shared.log.debug(f'Control: input video: path={filepath} frames={frames} fps={fps} size={w}x{h} codec={codec}')
|
||||
msg = f'Control input | Video | Size {w}x{h} | Frames {frames} | FPS {fps:.2f} | Duration {duration:.2f} | Codec {codec}'
|
||||
return msg
|
||||
|
||||
+30
-15
@@ -2,25 +2,39 @@ import os
|
||||
from datetime import datetime
|
||||
import gradio as gr
|
||||
from PIL import Image
|
||||
from modules import ui_symbols, ui_common, images
|
||||
from modules import ui_symbols, ui_common, images, ui_control_helpers
|
||||
from modules.ui_components import ToolButton
|
||||
|
||||
|
||||
def read_image(fn):
|
||||
def read_media(fn):
|
||||
if not os.path.isfile(fn):
|
||||
return [[], '', f'Image not found: {fn}']
|
||||
return [[], None, '', f'Image not found: {fn}']
|
||||
stat = os.stat(fn)
|
||||
image = Image.open(fn)
|
||||
image.already_saved_as = fn
|
||||
geninfo, _items = images.read_info_from_image(image)
|
||||
log = f'''
|
||||
<p>Image <b>{image.width} x {image.height}</b>
|
||||
| Format <b>{image.format}</b>
|
||||
| Mode <b>{image.mode}</b>
|
||||
| Size <b>{stat.st_size:,}</b>
|
||||
| Modified <b>{datetime.fromtimestamp(stat.st_mtime)}</b></p><br>
|
||||
'''
|
||||
return [[image], geninfo, geninfo, log]
|
||||
if fn.lower().endswith('.mp4'):
|
||||
frames, fps, duration, w, h, codec, _frame = ui_control_helpers.get_video_params(fn)
|
||||
geninfo = ''
|
||||
log = f'''
|
||||
<p>Video <b>{w} x {h}</b>
|
||||
| Codec <b>{codec}</b>
|
||||
| Frames <b>{frames:,}</b>
|
||||
| FPS <b>{fps}</b>
|
||||
| Duration <b>{duration:,}</b>
|
||||
| Size <b>{stat.st_size:,}</b>
|
||||
| Modified <b>{datetime.fromtimestamp(stat.st_mtime)}</b></p><br>
|
||||
'''
|
||||
return [gr.update(visible=False, value=[]), gr.update(visible=True, value=fn), geninfo, geninfo, log]
|
||||
else:
|
||||
image = Image.open(fn)
|
||||
image.already_saved_as = fn
|
||||
geninfo, _items = images.read_info_from_image(image)
|
||||
log = f'''
|
||||
<p>Image <b>{image.width} x {image.height}</b>
|
||||
| Format <b>{image.format}</b>
|
||||
| Mode <b>{image.mode}</b>
|
||||
| Size <b>{stat.st_size:,}</b>
|
||||
| Modified <b>{datetime.fromtimestamp(stat.st_mtime)}</b></p><br>
|
||||
'''
|
||||
return [gr.update(visible=True, value=[image]), gr.update(visible=False), geninfo, geninfo, log]
|
||||
|
||||
|
||||
def create_ui():
|
||||
@@ -46,6 +60,7 @@ def create_ui():
|
||||
gr.HTML('', elem_id='tab-gallery-files')
|
||||
with gr.Column():
|
||||
btn_gallery_image = gr.Button('', elem_id='tab-gallery-send-image', visible=False, interactive=True)
|
||||
gallery_video = gr.Video(None, elem_id='tab-gallery-video', show_label=False, visible=False)
|
||||
gallery_images, gen_info, html_info, _html_info_formatted, html_log = ui_common.create_output_panel("gallery")
|
||||
btn_gallery_image.click(fn=read_image, _js='gallerySendImage', inputs=[html_info], outputs=[gallery_images, html_info, gen_info, html_log])
|
||||
btn_gallery_image.click(fn=read_media, _js='gallerySendImage', inputs=[html_info], outputs=[gallery_images, gallery_video, html_info, gen_info, html_log])
|
||||
return [(tab, 'Gallery', 'tab-gallery')]
|
||||
|
||||
Reference in New Issue
Block a user