Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

8 changed files with 293 additions and 530 deletions

@ -1,38 +1,31 @@
# Image-Generator
A CLI to generate comparison image sets utilizing the power of VapourSynth
A CLI to generate comparison image sets with
## Usage
```
usage: FrameForge [-h] [-v] [--source SOURCE] [--encode ENCODE] [--frames FRAMES] [--image-dir IMAGE_DIR]
[--indexer {lsmash,ffms2}] [--img-lib {imwri,fpng}] [--source-index-path SOURCE_INDEX_PATH]
[--encode-index-path ENCODE_INDEX_PATH] [--sub-size SUB_SIZE] [--sub-alignment SUB_ALIGNMENT]
[--left-crop LEFT_CROP] [--right-crop RIGHT_CROP] [--top-crop TOP_CROP] [--bottom-crop BOTTOM_CROP]
[--adv-resize-left ADV_RESIZE_LEFT] [--adv-resize-right ADV_RESIZE_RIGHT]
[--adv-resize-top ADV_RESIZE_TOP] [--adv-resize-bottom ADV_RESIZE_BOTTOM] [--tone-map]
[--re-sync RE_SYNC] [--comparison-count COMPARISON_COUNT] [--subtitle-color SUBTITLE_COLOR]
[--release-sub-title RELEASE_SUB_TITLE]
usage: Comparison Image Generator [-h] [-v] [--source SOURCE] [--encode ENCODE] [--image-dir IMAGE_DIR]
[--indexer {lsmash,ffms2}] [--index-dir INDEX_DIR] [--sub-size SUB_SIZE]
[--left-crop LEFT_CROP] [--right-crop RIGHT_CROP] [--top-crop TOP_CROP]
[--bottom-crop BOTTOM_CROP] [--adv-resize-left ADV_RESIZE_LEFT]
[--adv-resize-right ADV_RESIZE_RIGHT] [--adv-resize-top ADV_RESIZE_TOP]
[--adv-resize-bottom ADV_RESIZE_BOTTOM] [--tone-map] [--re-sync RE_SYNC]
[--comparison-count COMPARISON_COUNT] [--subtitle-color SUBTITLE_COLOR]
[--release-sub-title RELEASE_SUB_TITLE]
options:
-h, --help show this help message and exit
-v, --version show program's version number and exit
--source SOURCE Path to source file
--encode ENCODE Path to encode file
--frames FRAMES Only use this if you want to specify the frames to generate, this disables sync frames
--image-dir IMAGE_DIR
Path to base image folder
--indexer {lsmash,ffms2}
Indexer choice
--img-lib {imwri,fpng}
Image library to use
--source-index-path SOURCE_INDEX_PATH
Path to look/create indexes for source
--encode-index-path ENCODE_INDEX_PATH
Path to look/create indexes for encode
--index-dir INDEX_DIR
Path to look/create indexes
--sub-size SUB_SIZE Size of subtitles
--sub-alignment SUB_ALIGNMENT
Alignment of subtitles (.ass)
--left-crop LEFT_CROP
Left crop
--right-crop RIGHT_CROP
@ -53,62 +46,16 @@ options:
--comparison-count COMPARISON_COUNT
Amount of comparisons to generate
--subtitle-color SUBTITLE_COLOR
Hex color code for subtitle color (i.e. --subtitle-color "#fff000")
Hex color code for subtitle color
--release-sub-title RELEASE_SUB_TITLE
Release group subtitle name (this will show on the encode images)
```
## Supports
Windows 8 and up (x64).
Technically could support Linux/MacOS but binaries will only be compiled for Windows for now.
Windows 8 and up
## Requirements
You will need lsmash, ffms2, libfpng, libimwri, and SubText vapoursynth plugins. For windows
I have compiled an executable with the needed runtime libraries.
## Example Usage
```
FrameForge.exe --source "path/Ant-Man.2015.mkv" --encode "path/Ant-Man.2015.Encoded.mkv" --sub-size 12 --indexer lsmash --top-crop 22 --bottom-crop 22 --subtitle-color #00FF00
Indexing source
...
Source index completed
Indexing encode
...
Encode index completed
Checking if encode has been de-interlaced
No de-interlacing detected
Generating 20 'B' frames for comparison images
Finished generating 20 'B' frames
Creating folders for images
Folder creation completed
Generating screenshots, please wait
Writing file: 01a_source__%d.png, frame: 25270
Writing file: 02a_source__%d.png, frame: 30590
...
Generating a few sync frames
Writing file: 01b_encode__%d.png, frame: 35910
Writing file: 02b_encode__%d.png, frame: 25270
...
Screen generation completed
Output: path/Ant-Man.2015_images/
```
[![04a_source__41230.png](https://thumbs2.imgbox.com/8d/47/X3l54wjy_t.png)](https://imgbox.com/X3l54wjy)
[![04b_encode__41230.png](https://thumbs2.imgbox.com/07/65/WvJ55AZp_t.png)](https://imgbox.com/WvJ55AZp)
[![11a_source__41230.png](https://thumbs2.imgbox.com/59/bd/woQpbay2_t.png)](https://imgbox.com/woQpbay2)
[![11b_encode__41230.png](https://thumbs2.imgbox.com/96/24/WzCtO3dY_t.png)](https://imgbox.com/WzCtO3dY)
[![16a_source__41230.png](https://thumbs2.imgbox.com/a8/5b/GftN1tCw_t.png)](https://imgbox.com/GftN1tCw)
[![16b_encode__41230.png](https://thumbs2.imgbox.com/c2/94/tFhRztHT_t.png)](https://imgbox.com/tFhRztHT)
You will need lsmash, ffms2, libfpng, libimwri, and SubText vapoursynth plugins.
Place all of these in a folder 'img_plugins' beside the script/executable.

@ -1,5 +1,5 @@
from pathlib import Path
from subprocess import run, PIPE
from subprocess import run
import os
import shutil
import sys
@ -31,23 +31,11 @@ def build_app():
icon_path = project_root / "images" / "icon.ico"
additional_hooks_path = Path(Path.cwd() / "hooks")
# get poetry venv path
poetry_venv_path = run(
["cmd", "/c", "poetry", "env", "info", "--path"],
stdout=PIPE,
stderr=PIPE,
text=True,
check=True,
# paths to needed vapoursynth files
vapoursynth_64 = project_root / ".venv" / "Lib" / "site-packages" / "vapoursynth64"
vapoursynth_64_portable = (
project_root / ".venv" / "Lib" / "site-packages" / "portable.vs"
)
if poetry_venv_path.returncode == 0 and poetry_venv_path.stdout:
poetry_venv_path = Path(poetry_venv_path.stdout.strip())
site_packages = poetry_venv_path / "Lib" / "site-packages"
# get paths to needed vapoursynth files in poetry venv
vapoursynth_64 = site_packages / "vapoursynth64"
vapoursynth_64_portable = site_packages / "portable.vs"
else:
raise FileNotFoundError("Cannot find path to poetry venv")
# Change directory so PyInstaller outputs all of its files in its own folder
os.chdir(pyinstaller_folder)

@ -3,11 +3,10 @@ from argparse import ArgumentParser
from frame_forge import GenerateImages
from frame_forge.exceptions import FrameForgeError
from frame_forge.utils import exit_application
from frame_forge.cli_utils import frame_list
program_name = "FrameForge"
__version__ = "1.3.4"
__version__ = "1.0.0"
if __name__ == "__main__":
@ -19,37 +18,15 @@ if __name__ == "__main__":
parser.add_argument("--source", type=str, help="Path to source file")
parser.add_argument("--encode", type=str, help="Path to encode file")
parser.add_argument(
"--frames",
type=frame_list,
help="Only use this if you want to specify the "
"frames to generate, this disables sync frames",
)
parser.add_argument("--image-dir", type=str, help="Path to base image folder")
parser.add_argument(
"--indexer",
type=str,
choices=["lsmash", "ffms2"],
default="lsmash",
help="Indexer choice",
)
parser.add_argument(
"--img-lib",
type=str,
choices=["imwri", "fpng"],
default="fpng",
help="Image library to use",
)
parser.add_argument(
"--source-index-path", type=str, help="Path to look/create indexes for source"
)
parser.add_argument(
"--encode-index-path", type=str, help="Path to look/create indexes for encode"
)
parser.add_argument("--sub-size", type=int, default=20, help="Size of subtitles")
parser.add_argument(
"--sub-alignment", type=int, default=7, help="Alignment of subtitles (.ass)"
)
parser.add_argument("--index-dir", type=str, help="Path to look/create indexes")
parser.add_argument("--sub-size", type=int, help="Size of subtitles")
parser.add_argument("--left-crop", type=int, help="Left crop")
parser.add_argument("--right-crop", type=int, help="Right crop")
parser.add_argument("--top-crop", type=int, help="Top crop")
@ -70,9 +47,7 @@ if __name__ == "__main__":
"--comparison-count", type=int, help="Amount of comparisons to generate"
)
parser.add_argument(
"--subtitle-color",
type=str,
help='Hex color code for subtitle color (i.e. --subtitle-color "#fff000")',
"--subtitle-color", type=str, help="Hex color code for subtitle color"
)
parser.add_argument(
"--release-sub-title",
@ -98,16 +73,6 @@ if __name__ == "__main__":
1,
)
index_suffix = ".lwi" if args.indexer == "lsmash" else ".ffindex"
for index_input in [args.source_index_path, args.encode_index_path]:
if index_input:
if Path(index_input).suffix != index_suffix:
exit_application(
f"When using {args.indexer} indexer you must use '{index_suffix}' "
"for your source/encode index path suffix",
1,
)
if args.image_dir:
image_dir = Path(args.image_dir)
else:
@ -118,14 +83,10 @@ if __name__ == "__main__":
img_generator = GenerateImages(
source_file=Path(args.source),
encode_file=Path(args.encode),
frames=args.frames,
image_dir=image_dir,
indexer=args.indexer,
img_lib=args.img_lib,
source_index_path=args.source_index_path,
encode_index_path=args.encode_index_path,
index_directory=args.index_dir,
sub_size=args.sub_size,
sub_alignment=args.sub_alignment,
left_crop=args.left_crop,
right_crop=args.right_crop,
top_crop=args.top_crop,
@ -142,18 +103,8 @@ if __name__ == "__main__":
subtitle_color=args.subtitle_color,
release_sub_title=args.release_sub_title,
)
except Exception as init_error:
exit_application(f"Initiation Error: {init_error}", 1)
try:
img_gen = img_generator.process_images()
if img_gen:
exit_application(f"\nOutput: {img_gen}", 0)
except FrameForgeError as ff_error:
img_generator.clean_temp(False)
exit_application(str(ff_error), 1)
except Exception as except_error:
img_generator.clean_temp(False)
exit_application(f"Unhandled Exception: {except_error}", 1)
finally:
img_generator.clean_temp(False)
exit_application(f"Output: {img_gen}", 0)
except FrameForgeError as error:
exit_application(error, 1)

@ -1,13 +1,11 @@
import re
import shutil
import tempfile
from random import choice
from pathlib import Path
from typing import Tuple
from numpy import linspace
from unidecode import unidecode
import awsmfunc
import vapoursynth as vs
from awsmfunc import ScreenGenEncoder, ScreenGen, FrameInfo, DynamicTonemap
from frame_forge.exceptions import FrameForgeError
from frame_forge.utils import get_working_dir, hex_to_bgr
@ -17,14 +15,10 @@ class GenerateImages:
self,
source_file: Path,
encode_file: Path,
frames: str,
image_dir: Path,
indexer: str,
img_lib: str,
source_index_path: None | str,
encode_index_path: None | str,
index_directory: None | str,
sub_size: int,
sub_alignment: int,
left_crop: int,
right_crop: int,
top_crop: int,
@ -43,15 +37,11 @@ class GenerateImages:
self.source_node = None
self.reference_source_file = None
self.encode_file = encode_file
self.frames = frames
self.encode_node = None
self.image_dir = image_dir
self.indexer = indexer
self.img_lib = ScreenGenEncoder(img_lib)
self.source_index_path = source_index_path
self.encode_index_path = encode_index_path
self.index_dir = index_directory
self.sub_size = sub_size
self.sub_alignment = sub_alignment
self.left_crop = left_crop
self.right_crop = right_crop
self.top_crop = top_crop
@ -69,11 +59,7 @@ class GenerateImages:
self.core = vs.core
self.load_plugins()
self.temp_dir: Path = None
def process_images(self) -> Path:
self.check_index_paths()
def process_images(self):
if self.indexer == "lsmash":
self.index_lsmash()
@ -83,42 +69,25 @@ class GenerateImages:
num_source_frames = len(self.source_node)
num_encode_frames = len(self.encode_node)
# ASS subtitle styles
# Font Name, Font Size, Primary Color, Secondary Color, Outline Color, Back Color, Bold,
# Italic, Underline, Strikeout, Scale X, Scale Y, Spacing, Angle, Border Style, Outline Width,
# Shadow Depth, Alignment, Left Margin, Right Margin, Vertical Margin, Encoding
# Format: Name, Fontname, Font-size, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic,
# Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL,
# MarginR, MarginV,
# bgr color
color = "&H14FF39"
color = "&H000ac7f5"
if self.subtitle_color:
color = hex_to_bgr(self.subtitle_color)
selected_sub_style = (
f"Segoe UI,{self.sub_size},{color},&H00000000,&H00000000,&H00000000,"
f"1,0,0,0,100,100,0,0,1,1,0,{self.sub_alignment},10,10,10,1"
)
sync_sub_base = (
"Segoe UI,{size},&H31FF31&,&H00000000,&H00000000,&H00000000,"
"1,0,0,0,100,100,0,0,1,1,0,{pos},10,10,10,1"
)
selected_sub_style_ref = sync_sub_base.format(
size=str(self.sub_size + 5), pos="7"
)
selected_sub_style_sync = sync_sub_base.format(
size=str(self.sub_size + 5), pos="9"
"1,0,0,0,100,100,0,0,1,1,0,7,5,0,0,1"
)
self.check_de_interlaced(num_source_frames, num_encode_frames)
b_frames = None
if not self.frames:
b_frames = self.get_b_frames(num_source_frames)
b_frames = self.get_b_frames(num_source_frames)
(
temp_screenshot_comparison_dir,
temp_selected_dir,
temp_screenshot_sync_dir,
) = self.generate_temp_folders()
screenshot_comparison_dir, screenshot_sync_dir = self.generate_folders()
self.handle_crop()
@ -128,28 +97,16 @@ class GenerateImages:
vs_source_info, vs_encode_info = self.handle_subtitles(selected_sub_style)
if not self.frames:
self.generate_screens(
b_frames,
vs_source_info,
vs_encode_info,
temp_screenshot_comparison_dir,
temp_screenshot_sync_dir,
selected_sub_style_ref,
selected_sub_style_sync,
)
else:
self.generate_exact_screens(
vs_source_info,
vs_encode_info,
temp_screenshot_comparison_dir,
)
img_job = self.generate_screens(
b_frames,
vs_source_info,
vs_encode_info,
screenshot_comparison_dir,
screenshot_sync_dir,
selected_sub_style,
)
final_folder = self.generate_final_folder()
self.move_images(temp_screenshot_comparison_dir.parent, final_folder)
self.clean_temp()
return final_folder
return img_job
@staticmethod
def screen_gen_callback(sg_call_back):
@ -158,83 +115,6 @@ class GenerateImages:
flush=True,
)
def generate_ref_screens(
self, selected_sub_style_ref, frames: list, screenshot_sync_dir
):
"""Generates reference frames"""
for ref_frame in frames:
vs_encode_ref_info = self.core.sub.Subtitle(
clip=self.encode_node,
text=f"Reference\nFrame: {ref_frame}",
style=selected_sub_style_ref,
)
ScreenGen(
vs_encode_ref_info,
frame_numbers=[ref_frame],
fpng_compression=1,
folder=screenshot_sync_dir,
suffix="b_encode__%d",
callback=self.screen_gen_callback,
encoder=self.img_lib,
)
def generate_sync_screens(
self, frame_list, selected_sub_style_sync, screenshot_sync_dir
):
"""Generates sync frames"""
for sync_frame in frame_list:
vs_sync_info = self.core.sub.Subtitle(
clip=self.source_node,
text=f"Sync\nFrame: {sync_frame}",
style=selected_sub_style_sync,
)
ScreenGen(
vs_sync_info,
frame_numbers=[sync_frame],
fpng_compression=1,
folder=Path(screenshot_sync_dir),
suffix="a_source__%d",
callback=self.screen_gen_callback,
encoder=self.img_lib,
)
def generate_exact_screens(
self,
vs_source_info,
vs_encode_info,
screenshot_comparison_dir,
) -> Path:
print("\nGenerating screenshots, please wait", flush=True)
# generate source images
ScreenGen(
vs_source_info,
frame_numbers=[
self.frames[i] for i in range(len(self.frames)) if i % 2 == 0
],
fpng_compression=1,
folder=screenshot_comparison_dir,
suffix="a_source__%d",
callback=self.screen_gen_callback,
encoder=self.img_lib,
)
# generate encode images
ScreenGen(
vs_encode_info,
frame_numbers=[
self.frames[i] for i in range(len(self.frames)) if i % 2 != 0
],
fpng_compression=1,
folder=screenshot_comparison_dir,
suffix="b_encode__%d",
callback=self.screen_gen_callback,
encoder=self.img_lib,
)
print("Screen generation completed", flush=True)
return screenshot_comparison_dir
def generate_screens(
self,
b_frames,
@ -242,9 +122,8 @@ class GenerateImages:
vs_encode_info,
screenshot_comparison_dir,
screenshot_sync_dir,
selected_sub_style_ref,
selected_sub_style_sync,
) -> Path:
selected_sub_style,
):
print("\nGenerating screenshots, please wait", flush=True)
# handle re_sync if needed
@ -261,70 +140,99 @@ class GenerateImages:
sync_frames = b_frames
# generate source images
ScreenGen(
awsmfunc.ScreenGen(
vs_source_info,
frame_numbers=sync_frames,
fpng_compression=1,
folder=screenshot_comparison_dir,
suffix="a_source__%d",
callback=self.screen_gen_callback,
encoder=self.img_lib,
)
# generate encode images
ScreenGen(
awsmfunc.ScreenGen(
vs_encode_info,
frame_numbers=b_frames,
fpng_compression=1,
folder=screenshot_comparison_dir,
suffix="b_encode__%d",
callback=self.screen_gen_callback,
encoder=self.img_lib,
)
# generate some sync frames
print("\nGenerating a few sync frames", flush=True)
# select two frames randomly from list
get_sync_1 = choice(b_frames)
sync_1 = choice(b_frames)
remove_sync1 = b_frames.copy()
remove_sync1.remove(get_sync_1)
get_sync_2 = choice(remove_sync1)
# sync list
ref_sync_list = sorted([get_sync_1, get_sync_2])
remove_sync1.remove(sync_1)
sync_2 = choice(remove_sync1)
# reference subs
self.generate_ref_screens(
selected_sub_style_ref, ref_sync_list, screenshot_sync_dir
vs_source_ref_info = self.core.sub.Subtitle(
clip=self.source_node, text="Sync", style=selected_sub_style
)
vs_encode_ref_info = self.core.sub.Subtitle(
clip=self.encode_node, text="Reference", style=selected_sub_style
)
# sync subs 1
sync_subs_1 = [ref_sync_list[0] + i for i in range(-5, 6)]
self.generate_sync_screens(
sync_subs_1,
selected_sub_style_sync,
Path(Path(screenshot_sync_dir) / "sync1"),
# generate screens for the two reference frames
awsmfunc.ScreenGen(
vs_encode_ref_info,
frame_numbers=[sync_1, sync_2],
fpng_compression=1,
folder=screenshot_sync_dir,
suffix="b_encode__%d",
callback=self.screen_gen_callback,
)
# sync subs 2
sync_subs_2 = [ref_sync_list[1] + i for i in range(-5, 6)]
# generate 10 source frames around those reference frames
awsmfunc.ScreenGen(
vs_source_ref_info,
frame_numbers=[
sync_1 - 4,
sync_1 - 3,
sync_1 - 2,
sync_1 - 1,
sync_1,
sync_1 + 1,
sync_1 + 2,
sync_1 + 3,
sync_1 + 4,
],
fpng_compression=1,
folder=Path(Path(screenshot_sync_dir) / "sync1"),
suffix="a_source__%d",
callback=self.screen_gen_callback,
)
self.generate_sync_screens(
sync_subs_2,
selected_sub_style_sync,
Path(Path(screenshot_sync_dir) / "sync2"),
awsmfunc.ScreenGen(
vs_source_ref_info,
frame_numbers=[
sync_2 - 4,
sync_2 - 3,
sync_2 - 2,
sync_2 - 1,
sync_2,
sync_2 + 1,
sync_2 + 2,
sync_2 + 3,
sync_2 + 4,
],
fpng_compression=1,
folder=Path(Path(screenshot_sync_dir) / "sync2"),
suffix="a_source__%d",
callback=self.screen_gen_callback,
)
print("Screen generation completed", flush=True)
return screenshot_comparison_dir
return str(screenshot_comparison_dir)
def handle_subtitles(self, selected_sub_style):
vs_source_info = self.core.sub.Subtitle(
clip=self.source_node, text="Source", style=selected_sub_style
)
vs_encode_info = FrameInfo(
vs_encode_info = awsmfunc.FrameInfo(
clip=self.encode_node,
title=self.release_sub_title if self.release_sub_title else "",
style=selected_sub_style,
@ -334,8 +242,10 @@ class GenerateImages:
def handle_hdr(self):
if self.tone_map:
self.source_node = DynamicTonemap(clip=self.source_node, libplacebo=False)
self.encode_node = DynamicTonemap(
self.source_node = awsmfunc.DynamicTonemap(
clip=self.source_node, libplacebo=False
)
self.encode_node = awsmfunc.DynamicTonemap(
clip=self.encode_node,
reference=self.reference_source_file,
libplacebo=False,
@ -392,8 +302,8 @@ class GenerateImages:
bottom=self.bottom_crop if self.bottom_crop else 0,
)
def generate_final_folder(self) -> Path:
print("\nCreating final output folder", flush=True)
def generate_folders(self):
print("\nCreating folders for images", flush=True)
if self.image_dir:
image_output_dir = Path(self.image_dir)
else:
@ -401,61 +311,34 @@ class GenerateImages:
Path(self.encode_file).parent / f"{Path(self.encode_file).stem}_images"
)
if image_output_dir.exists():
for folder in ("img_comparison", "img_selected", "img_sync"):
rm_path = image_output_dir / folder
if rm_path.is_dir() and rm_path.exists():
shutil.rmtree(rm_path, ignore_errors=True)
# remove any accent characters from path
image_output_dir = Path(unidecode(str(image_output_dir)))
# check if temp image dir exists, if so delete it!
if image_output_dir.exists():
shutil.rmtree(image_output_dir, ignore_errors=True)
# create main image dir
image_output_dir.mkdir(exist_ok=True, parents=True)
print("Folder creation completed", flush=True)
# create comparison image directory and define it as variable
Path(Path(image_output_dir) / "img_comparison").mkdir(exist_ok=True)
screenshot_comparison_dir = str(Path(Path(image_output_dir) / "img_comparison"))
return image_output_dir
# create selected image directory and define it as variable
Path(Path(image_output_dir) / "img_selected").mkdir(exist_ok=True)
def generate_temp_folders(self) -> Tuple[Path, Path, Path]:
print("\nCreating temporary folders for images", flush=True)
self.temp_dir = Path(tempfile.mkdtemp(prefix="ff_"))
# create sync image directory and define it as variable
Path(Path(image_output_dir) / "img_sync").mkdir(exist_ok=True)
screenshot_sync_dir = str(Path(Path(image_output_dir) / "img_sync"))
screenshot_comparison_dir = Path(Path(self.temp_dir) / "img_comparison")
screenshot_comparison_dir.mkdir(exist_ok=True)
selected_dir = Path(Path(self.temp_dir) / "img_selected")
selected_dir.mkdir(exist_ok=True)
screenshot_sync_dir = Path(Path(self.temp_dir) / "img_sync")
screenshot_sync_dir.mkdir(exist_ok=True)
Path(screenshot_sync_dir / "sync1").mkdir(exist_ok=True)
Path(screenshot_sync_dir / "sync2").mkdir(exist_ok=True)
# create sub directories
Path(Path(image_output_dir) / "img_sync/sync1").mkdir(exist_ok=True)
Path(Path(image_output_dir) / "img_sync/sync2").mkdir(exist_ok=True)
print("Folder creation completed", flush=True)
return screenshot_comparison_dir, selected_dir, screenshot_sync_dir
def move_images(self, temp_folder: Path, output_folder: Path) -> None:
print("\nMoving generated images")
for sub_folder in temp_folder.iterdir():
if sub_folder.is_dir():
target_sub_folder = output_folder / sub_folder.name
target_sub_folder.mkdir(parents=True, exist_ok=True)
for item in sub_folder.iterdir():
target_item = target_sub_folder / item.name
if item.is_dir():
shutil.move(item, target_item)
else:
shutil.move(item, target_sub_folder)
print("Image move completed", flush=True)
def clean_temp(self, status: bool = True) -> None:
if status:
print("\nRemoving temp folder")
shutil.rmtree(self.temp_dir, ignore_errors=True)
if status:
print("Temp folder removal completed")
return screenshot_comparison_dir, screenshot_sync_dir
def get_b_frames(self, num_source_frames):
print(
@ -472,11 +355,9 @@ class GenerateImages:
)
try:
pict_types = ("B", b"B")
for i, frame in enumerate(b_frames):
while (
self.encode_node.get_frame(frame).props["_PictType"]
not in pict_types
self.encode_node.get_frame(frame).props["_PictType"].decode() != "B"
):
frame += 1
b_frames[i] = frame
@ -535,9 +416,10 @@ class GenerateImages:
flush=True,
)
def _index_source_lsmash(self):
def index_lsmash(self):
print("Indexing source", flush=True)
# index source file
# if index is found in the StaxRip temp working directory, attempt to use it
if (
Path(str(Path(self.source_file).with_suffix("")) + "_temp/").is_dir()
@ -547,134 +429,152 @@ class GenerateImages:
):
print("Index found in StaxRip temp, attempting to use", flush=True)
# define cache path
lwi_cache_path = Path(
str(Path(self.source_file).with_suffix("")) + "_temp/temp.lwi"
)
elif self.source_index_path.exists():
print("Index found, attempting to use", flush=True)
lwi_cache_path = self.source_index_path
# try to use index on source file with the cache path
try:
self.source_node = self.core.lsmas.LWLibavSource(
source=self.source_file, cachefile=lwi_cache_path
)
self.reference_source_file = self.core.lsmas.LWLibavSource(
source=self.source_file, cachefile=lwi_cache_path
)
print("Using existing index", flush=True)
# if index cannot be used
except vs.Error:
print("L-Smash version miss-match, indexing source again", flush=True)
# index source file
self.source_node = self.core.lsmas.LWLibavSource(self.source_file)
self.reference_source_file = self.core.lsmas.LWLibavSource(
self.source_file
)
# if no existing index is found index source file
else:
lwi_cache_path = Path(Path(self.source_file).with_suffix(".lwi"))
cache_path = Path(Path(self.source_file).with_suffix(".lwi"))
try:
# create index
self.source_node = self.core.lsmas.LWLibavSource(
self.source_file, cachefile=cache_path
)
self.reference_source_file = self.core.lsmas.LWLibavSource(
self.source_file, cachefile=cache_path
)
except vs.Error:
# delete index
Path(self.source_file).with_suffix(".lwi").unlink(missing_ok=True)
# create index
self.source_node = self.core.lsmas.LWLibavSource(
self.source_file, cachefile=cache_path
)
self.reference_source_file = self.core.lsmas.LWLibavSource(
self.source_file, cachefile=cache_path
)
try:
self.source_node = self.core.lsmas.LWLibavSource(
source=self.source_file, cachefile=lwi_cache_path
)
self.reference_source_file = self.core.lsmas.LWLibavSource(
source=self.source_file, cachefile=lwi_cache_path
)
print("Using existing index", flush=True)
except vs.Error:
print("L-Smash version miss-match, indexing source again", flush=True)
self.source_node = self.core.lsmas.LWLibavSource(self.source_file)
self.reference_source_file = self.core.lsmas.LWLibavSource(self.source_file)
print("Source index completed\n\nIndexing encode", flush=True)
print("Source index completed", flush=True)
def _index_encode_lsmash(self):
print("\nIndexing encode", flush=True)
if self.encode_index_path:
cache_path_enc = self.encode_index_path
# define a path for encode index to go
if self.index_dir:
index_base_path = Path(self.index_dir) / Path(self.encode_file).name
cache_path_enc = index_base_path.with_suffix(".lwi")
else:
cache_path_enc = Path(Path(self.encode_file).with_suffix(".lwi"))
try:
# create index
self.encode_node = self.core.lsmas.LWLibavSource(
self.encode_file, cachefile=cache_path_enc
)
except vs.Error:
# delete index
cache_path_enc.unlink(missing_ok=True)
# create index
self.encode_node = self.core.lsmas.LWLibavSource(
self.encode_file, cachefile=cache_path_enc
)
print("Encode index completed", flush=True)
def index_lsmash(self):
"""Index source/encode with lsmash"""
self._index_source_lsmash()
self._index_encode_lsmash()
def _index_source_ffms2(self):
print("Indexing source", flush=True)
# if index is found in the StaxRip temp working directory, attempt to use it
if (
Path(str(Path(self.source_file).with_suffix("")) + "_temp/").is_dir()
and Path(
str(Path(self.source_file).with_suffix("")) + "_temp/temp.ffindex"
).is_file()
):
print("Index found in StaxRip temp, attempting to use", flush=True)
ffindex_cache_path = Path(
str(Path(self.source_file).with_suffix("")) + "_temp/temp.ffindex"
)
elif self.source_index_path.exists():
print("Index found, attempting to use", flush=True)
ffindex_cache_path = self.source_index_path
# if no existing index is found index source file
else:
ffindex_cache_path = Path(Path(self.source_file).with_suffix(".ffindex"))
print(
"FFMS2 library doesn't allow progress, please wait while the index is completed",
flush=True,
)
try:
self.source_node = self.core.ffms2.Source(
self.source_file, cachefile=ffindex_cache_path
)
self.reference_source_file = self.core.ffms2.Source(
self.source_file, cachefile=ffindex_cache_path
)
except vs.Error:
Path(self.source_file).with_suffix(".ffindex").unlink(missing_ok=True)
print(
"FFMS2 library doesn't allow progress, please wait while the index is completed",
flush=True,
)
self.source_node = self.core.ffms2.Source(
self.source_file, cachefile=ffindex_cache_path
)
self.reference_source_file = self.core.ffms2.Source(
self.source_file, cachefile=ffindex_cache_path
)
print("Source index completed", flush=True)
def _index_encode_ffms2(self):
print("\nIndexing encode", flush=True)
if self.encode_index_path:
cache_path_enc = self.encode_index_path
else:
cache_path_enc = Path(str(self.encode_file) + ".ffindex")
try:
self.encode_node = self.core.ffms2.Source(
self.encode_file, cachefile=cache_path_enc
)
except vs.Error:
cache_path_enc.unlink(missing_ok=True)
self.encode_node = self.core.ffms2.Source(
self.encode_file, cachefile=cache_path_enc
)
print("Encode index completed", flush=True)
def index_ffms2(self):
"""Index source/encode with ffms2"""
print("Indexing source", flush=True)
self._index_source_ffms2()
self._index_encode_ffms2()
# index source file
# if index is found in the StaxRip temp working directory, attempt to use it
if (
Path(str(Path(self.source_file).with_suffix("")) + "_temp/").is_dir()
and Path(
str(Path(self.source_file).with_suffix("")) + "_temp/temp.ffindex"
).is_file()
):
print("Index found in StaxRip temp, attempting to use", flush=True)
# define cache path
ffindex_cache_path = Path(
str(Path(self.source_file).with_suffix("")) + "_temp/temp.ffindex"
)
# try to use index on source file with the cache path
try:
self.source_node = self.core.ffms2.Source(
source=self.source_file, cachefile=ffindex_cache_path
)
self.reference_source_file = self.core.ffms2.Source(
source=self.source_file, cachefile=ffindex_cache_path
)
print("Using existing index", flush=True)
# if index cannot be used
except vs.Error:
print("FFMS2 version miss-match, indexing source again", flush=True)
# index source file
self.source_node = self.core.ffms2.Source(self.source_file)
self.reference_source_file = self.core.ffms2.Source(self.source_file)
# if no existing index is found index source file
else:
try:
# create index
print(
"FFMS2 library doesn't allow progress, please wait while the index is completed",
flush=True,
)
self.source_node = self.core.ffms2.Source(self.source_file)
self.reference_source_file = self.core.ffms2.Source(self.source_file)
except vs.Error:
# delete index
Path(self.source_file).with_suffix(".ffindex").unlink(missing_ok=True)
# create index
print(
"FFMS2 library doesn't allow progress, please wait while the index is completed",
flush=True,
)
self.source_node = self.core.ffms2.Source(self.source_file)
self.reference_source_file = self.core.ffms2.Source(self.source_file)
print("Source index completed\n\nIndexing encode", flush=True)
# define a path for encode index to go
if self.index_dir:
index_base_path = Path(self.index_dir) / Path(self.encode_file).name
cache_path_enc = Path(str(index_base_path) + ".ffindex")
else:
cache_path_enc = Path(self.encode_file + ".ffindex")
try:
self.encode_node = self.core.ffms2.Source(
self.encode_file, cachefile=cache_path_enc
)
except vs.Error:
cache_path_enc.unlink(missing_ok=True)
self.encode_node = self.core.ffms2.Source(
self.encode_file, cachefile=cache_path_enc
)
print("Encode index completed", flush=True)
def load_plugins(self):
plugin_path = get_working_dir() / "img_plugins"
@ -683,21 +583,3 @@ class GenerateImages:
else:
for plugin in plugin_path.glob("*.dll"):
self.core.std.LoadPlugin(Path(plugin).resolve())
def check_index_paths(self):
indexer_ext = ".lwi" if self.indexer == "lsmash" else ".ffindex"
if not self.source_index_path or not Path(self.source_index_path).exists():
source_path_obj = Path(self.source_file)
self.source_index_path = source_path_obj.parent / Path(
f"{source_path_obj.stem}{indexer_ext}"
)
else:
self.source_index_path = Path(self.source_index_path)
if not self.encode_index_path or not Path(self.encode_index_path).exists():
encode_path_obj = Path(self.encode_file)
self.encode_index_path = encode_path_obj.parent / Path(
f"{encode_path_obj.stem}{indexer_ext}"
)
else:
self.encode_index_path = Path(self.encode_index_path)

@ -1,7 +0,0 @@
import re
def frame_list(frames: str) -> list:
if not re.match(r"\d+(?::\d+)*$", frames):
raise ValueError("Input must be in the format of int:int i.e. 101:104")
return [int(x) for x in frames.split(":")]

@ -22,7 +22,6 @@ hiddenimports = [
"collections.namedtuple",
"collections.abc.Iterable",
"collections.abc.Mapping",
"concurrent.futures",
"concurrent.futures.Future",
"fractions",
]

63
poetry.lock generated

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]]
name = "altgraph"
@ -17,8 +17,10 @@ version = "1.3.4"
description = "awesome VapourSynth functions"
optional = false
python-versions = ">=3.9"
files = []
develop = false
files = [
{file = "awsmfunc-1.3.4-py3-none-any.whl", hash = "sha256:d9ce9cf90dfdb66b4561a5d3b011232e663ad0d879e2a276827bff9b8b3b37e1"},
{file = "awsmfunc-1.3.4.tar.gz", hash = "sha256:8330332f5c4818322b4090b24499b1dc4e4e371460de70c4bd62a112f4157255"},
]
[package.dependencies]
numpy = "*"
@ -27,13 +29,7 @@ vs-rekt = ">=1.0.0"
vsutil = ">=0.7.0"
[package.extras]
dev = ["ruff", "toml"]
[package.source]
type = "git"
url = "https://github.com/OpusGang/awsmfunc"
reference = "HEAD"
resolved_reference = "e1290f799162749fc627951290bfb4089f2f39cb"
dev = ["pylint", "toml", "yapf"]
[[package]]
name = "black"
@ -220,10 +216,10 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
[[package]]
name = "pyinstaller"
version = "6.10.0"
version = "6.3.0"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
optional = false
python-versions = "<3.14,>=3.8"
python-versions = "<3.13,>=3.8"
files = []
develop = true
@ -232,7 +228,7 @@ altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
packaging = ">=22.0"
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2024.8"
pyinstaller-hooks-contrib = ">=2021.4"
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0"
@ -246,19 +242,15 @@ url = "custom-pyinstaller"
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2024.8"
version = "2023.11"
description = "Community maintained hooks for PyInstaller"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.7"
files = [
{file = "pyinstaller_hooks_contrib-2024.8-py3-none-any.whl", hash = "sha256:0057fe9a5c398d3f580e73e58793a1d4a8315ca91c3df01efea1c14ed557825a"},
{file = "pyinstaller_hooks_contrib-2024.8.tar.gz", hash = "sha256:29b68d878ab739e967055b56a93eb9b58e529d5b054fbab7a2f2bacf80cef3e2"},
{file = "pyinstaller-hooks-contrib-2023.11.tar.gz", hash = "sha256:5dd7a8a054a65c19cdaa381cabcfbe76f44d5f88d18214b0c570a0cd139be77f"},
{file = "pyinstaller_hooks_contrib-2023.11-py2.py3-none-any.whl", hash = "sha256:f2a75dac2968ec81f92dcd3768906f654fa4204bc496126ae8483e87a5d89602"},
]
[package.dependencies]
packaging = ">=22.0"
setuptools = ">=42.0.0"
[[package]]
name = "pywin32-ctypes"
version = "0.2.2"
@ -286,30 +278,41 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "unidecode"
version = "1.3.7"
description = "ASCII transliterations of Unicode text"
optional = false
python-versions = ">=3.5"
files = [
{file = "Unidecode-1.3.7-py3-none-any.whl", hash = "sha256:663a537f506834ed836af26a81b210d90cbde044c47bfbdc0fbbc9f94c86a6e4"},
{file = "Unidecode-1.3.7.tar.gz", hash = "sha256:3c90b4662aa0de0cb591884b934ead8d2225f1800d8da675a7750cbc3bd94610"},
]
[[package]]
name = "vapoursynth"
version = "65"
version = "64"
description = "A frameserver for the 21st century"
optional = false
python-versions = "*"
files = [
{file = "VapourSynth-65-cp311-cp311-win_amd64.whl", hash = "sha256:9fab72bb6dbcdd8d0e6643da68908f85387bc764b836524e97a1e6e8989ca13e"},
{file = "VapourSynth-65-cp38-cp38-win_amd64.whl", hash = "sha256:d9b49b595dc929d63250bd82f05e75238b6dfc4a50ab08e7fc4fe4f8888f6b95"},
{file = "VapourSynth-65.zip", hash = "sha256:1d42d461ef9988a3477134e478a2291d79f3469635cde8af2c66b1e87c36f711"},
{file = "VapourSynth-64-cp311-cp311-win_amd64.whl", hash = "sha256:ad046a704537276f7ebb4132e1210e8922ec4b2403a4b44f5fe30fd9456a6412"},
{file = "VapourSynth-64-cp38-cp38-win_amd64.whl", hash = "sha256:794f93bcaa1ce79510c11962f677a7d18685a0c29aa36586cb307d1eb9d7f2e0"},
{file = "VapourSynth-64.zip", hash = "sha256:29425a135ca68cbb17b3dfcad0097375b75f94af1f499ba89bcd2b03b2651846"},
]
[[package]]
name = "vapoursynth-portable"
version = "65"
version = "64"
description = "A frameserver for the 21st century"
optional = false
python-versions = "*"
files = [
{file = "VapourSynth_portable-65-py2.py3-none-win_amd64.whl", hash = "sha256:6a66615d8a25a71766d035054f9980fd8be47dbd97332e9c068933d1b25061b3"},
{file = "VapourSynth_portable-64-py2.py3-none-win_amd64.whl", hash = "sha256:82b26b38774197a7ef53cbd3206bafb02d197100fad513d635bf83f9981dad6c"},
]
[package.dependencies]
vapoursynth = "65"
vapoursynth = "64"
[[package]]
name = "vs-rekt"
@ -342,5 +345,5 @@ vapoursynth = "*"
[metadata]
lock-version = "2.0"
python-versions = "3.11.9"
content-hash = "42cace5dbe92132d04512c3dbc5a60c1cb06fa883a7030eb74f8f0e782b48226"
python-versions = "3.11.5"
content-hash = "820bfb62c5fba1b17221bc1a02144932dbce64fa44c698f24f7205c2fc4a3d18"

@ -1,17 +1,17 @@
[tool.poetry]
name = "frame-forge"
version = "1.3.4"
version = "1.0.0"
description = "CLI to offload image generation to"
authors = ["jlw4049 <jlw_4049@hotmail.com>"]
license = "MIT"
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "3.11.9"
python = "3.11.5"
awsmfunc = "^1.3.4"
unidecode = "^1.3.7"
vapoursynth-portable = "64"
numpy = "^1.26.2"
vapoursynth-portable = "65"
awsmfunc = {git = "https://github.com/OpusGang/awsmfunc"}
[tool.poetry.group.dev.dependencies]