Compare commits

..

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

7 changed files with 243 additions and 351 deletions

@ -1,17 +1,20 @@
# Image-Generator # Image-Generator
A CLI to generate comparison image sets utilizing the power of VapourSynth A CLI to generate comparison image sets with
## Usage ## Usage
``` ```
usage: FrameForge [-h] [-v] [--source SOURCE] [--encode ENCODE] [--frames FRAMES] [--image-dir IMAGE_DIR] usage: FrameForge [-h] [-v] [--source SOURCE] [--encode ENCODE] [--frames FRAMES]
[--indexer {lsmash,ffms2}] [--img-lib {imwri,fpng}] [--source-index-path SOURCE_INDEX_PATH] [--image-dir IMAGE_DIR] [--indexer {lsmash,ffms2}]
[--encode-index-path ENCODE_INDEX_PATH] [--sub-size SUB_SIZE] [--sub-alignment SUB_ALIGNMENT] [--index-dir INDEX_DIR] [--sub-size SUB_SIZE] [--left-crop LEFT_CROP]
[--left-crop LEFT_CROP] [--right-crop RIGHT_CROP] [--top-crop TOP_CROP] [--bottom-crop BOTTOM_CROP] [--right-crop RIGHT_CROP] [--top-crop TOP_CROP]
[--adv-resize-left ADV_RESIZE_LEFT] [--adv-resize-right ADV_RESIZE_RIGHT] [--bottom-crop BOTTOM_CROP] [--adv-resize-left ADV_RESIZE_LEFT]
[--adv-resize-top ADV_RESIZE_TOP] [--adv-resize-bottom ADV_RESIZE_BOTTOM] [--tone-map] [--adv-resize-right ADV_RESIZE_RIGHT]
[--re-sync RE_SYNC] [--comparison-count COMPARISON_COUNT] [--subtitle-color SUBTITLE_COLOR] [--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] [--release-sub-title RELEASE_SUB_TITLE]
options: options:
@ -19,20 +22,15 @@ options:
-v, --version show program's version number and exit -v, --version show program's version number and exit
--source SOURCE Path to source file --source SOURCE Path to source file
--encode ENCODE Path to encode 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 --frames FRAMES Only use this if you want to specify the frames to generate,
this disables sync frames
--image-dir IMAGE_DIR --image-dir IMAGE_DIR
Path to base image folder Path to base image folder
--indexer {lsmash,ffms2} --indexer {lsmash,ffms2}
Indexer choice Indexer choice
--img-lib {imwri,fpng} --index-dir INDEX_DIR
Image library to use Path to look/create indexes
--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
--sub-size SUB_SIZE Size of subtitles --sub-size SUB_SIZE Size of subtitles
--sub-alignment SUB_ALIGNMENT
Alignment of subtitles (.ass)
--left-crop LEFT_CROP --left-crop LEFT_CROP
Left crop Left crop
--right-crop RIGHT_CROP --right-crop RIGHT_CROP
@ -53,9 +51,10 @@ options:
--comparison-count COMPARISON_COUNT --comparison-count COMPARISON_COUNT
Amount of comparisons to generate Amount of comparisons to generate
--subtitle-color SUBTITLE_COLOR --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-sub-title RELEASE_SUB_TITLE
Release group subtitle name (this will show on the encode images) Release group subtitle name (this will show on the encode
images)
``` ```
## Supports ## Supports

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

@ -7,7 +7,7 @@ from frame_forge.cli_utils import frame_list
program_name = "FrameForge" program_name = "FrameForge"
__version__ = "1.3.4" __version__ = "1.1.0"
if __name__ == "__main__": if __name__ == "__main__":
@ -30,26 +30,10 @@ if __name__ == "__main__":
"--indexer", "--indexer",
type=str, type=str,
choices=["lsmash", "ffms2"], choices=["lsmash", "ffms2"],
default="lsmash",
help="Indexer choice", help="Indexer choice",
) )
parser.add_argument( parser.add_argument("--index-dir", type=str, help="Path to look/create indexes")
"--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-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("--left-crop", type=int, help="Left crop") parser.add_argument("--left-crop", type=int, help="Left crop")
parser.add_argument("--right-crop", type=int, help="Right crop") parser.add_argument("--right-crop", type=int, help="Right crop")
parser.add_argument("--top-crop", type=int, help="Top crop") parser.add_argument("--top-crop", type=int, help="Top crop")
@ -70,9 +54,7 @@ if __name__ == "__main__":
"--comparison-count", type=int, help="Amount of comparisons to generate" "--comparison-count", type=int, help="Amount of comparisons to generate"
) )
parser.add_argument( parser.add_argument(
"--subtitle-color", "--subtitle-color", type=str, help="Hex color code for subtitle color"
type=str,
help='Hex color code for subtitle color (i.e. --subtitle-color "#fff000")',
) )
parser.add_argument( parser.add_argument(
"--release-sub-title", "--release-sub-title",
@ -98,16 +80,6 @@ if __name__ == "__main__":
1, 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: if args.image_dir:
image_dir = Path(args.image_dir) image_dir = Path(args.image_dir)
else: else:
@ -121,11 +93,8 @@ if __name__ == "__main__":
frames=args.frames, frames=args.frames,
image_dir=image_dir, image_dir=image_dir,
indexer=args.indexer, indexer=args.indexer,
img_lib=args.img_lib, index_directory=args.index_dir,
source_index_path=args.source_index_path,
encode_index_path=args.encode_index_path,
sub_size=args.sub_size, sub_size=args.sub_size,
sub_alignment=args.sub_alignment,
left_crop=args.left_crop, left_crop=args.left_crop,
right_crop=args.right_crop, right_crop=args.right_crop,
top_crop=args.top_crop, top_crop=args.top_crop,
@ -142,18 +111,8 @@ if __name__ == "__main__":
subtitle_color=args.subtitle_color, subtitle_color=args.subtitle_color,
release_sub_title=args.release_sub_title, 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() img_gen = img_generator.process_images()
if img_gen: if img_gen:
exit_application(f"\nOutput: {img_gen}", 0) exit_application(f"Output: {img_gen}", 0)
except FrameForgeError as ff_error: except FrameForgeError as error:
img_generator.clean_temp(False) exit_application(error, 1)
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)

@ -1,13 +1,11 @@
import re import re
import shutil import shutil
import tempfile
from random import choice from random import choice
from pathlib import Path from pathlib import Path
from typing import Tuple
from numpy import linspace from numpy import linspace
from unidecode import unidecode
import awsmfunc
import vapoursynth as vs import vapoursynth as vs
from awsmfunc import ScreenGenEncoder, ScreenGen, FrameInfo, DynamicTonemap
from frame_forge.exceptions import FrameForgeError from frame_forge.exceptions import FrameForgeError
from frame_forge.utils import get_working_dir, hex_to_bgr from frame_forge.utils import get_working_dir, hex_to_bgr
@ -20,11 +18,8 @@ class GenerateImages:
frames: str, frames: str,
image_dir: Path, image_dir: Path,
indexer: str, indexer: str,
img_lib: str, index_directory: None | str,
source_index_path: None | str,
encode_index_path: None | str,
sub_size: int, sub_size: int,
sub_alignment: int,
left_crop: int, left_crop: int,
right_crop: int, right_crop: int,
top_crop: int, top_crop: int,
@ -47,11 +42,8 @@ class GenerateImages:
self.encode_node = None self.encode_node = None
self.image_dir = image_dir self.image_dir = image_dir
self.indexer = indexer self.indexer = indexer
self.img_lib = ScreenGenEncoder(img_lib) self.index_dir = index_directory
self.source_index_path = source_index_path
self.encode_index_path = encode_index_path
self.sub_size = sub_size self.sub_size = sub_size
self.sub_alignment = sub_alignment
self.left_crop = left_crop self.left_crop = left_crop
self.right_crop = right_crop self.right_crop = right_crop
self.top_crop = top_crop self.top_crop = top_crop
@ -69,11 +61,7 @@ class GenerateImages:
self.core = vs.core self.core = vs.core
self.load_plugins() self.load_plugins()
self.temp_dir: Path = None def process_images(self):
def process_images(self) -> Path:
self.check_index_paths()
if self.indexer == "lsmash": if self.indexer == "lsmash":
self.index_lsmash() self.index_lsmash()
@ -89,13 +77,13 @@ class GenerateImages:
# Shadow Depth, Alignment, Left Margin, Right Margin, Vertical Margin, Encoding # Shadow Depth, Alignment, Left Margin, Right Margin, Vertical Margin, Encoding
# bgr color # bgr color
color = "&H14FF39" color = "&H000ac7f5"
if self.subtitle_color: if self.subtitle_color:
color = hex_to_bgr(self.subtitle_color) color = hex_to_bgr(self.subtitle_color)
selected_sub_style = ( selected_sub_style = (
f"Segoe UI,{self.sub_size},{color},&H00000000,&H00000000,&H00000000," 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" "1,0,0,0,100,100,0,0,1,1,0,7,10,10,10,1"
) )
sync_sub_base = ( sync_sub_base = (
"Segoe UI,{size},&H31FF31&,&H00000000,&H00000000,&H00000000," "Segoe UI,{size},&H31FF31&,&H00000000,&H00000000,&H00000000,"
@ -114,11 +102,7 @@ class GenerateImages:
if not self.frames: if not self.frames:
b_frames = self.get_b_frames(num_source_frames) b_frames = self.get_b_frames(num_source_frames)
( screenshot_comparison_dir, screenshot_sync_dir = self.generate_folders()
temp_screenshot_comparison_dir,
temp_selected_dir,
temp_screenshot_sync_dir,
) = self.generate_temp_folders()
self.handle_crop() self.handle_crop()
@ -129,27 +113,23 @@ class GenerateImages:
vs_source_info, vs_encode_info = self.handle_subtitles(selected_sub_style) vs_source_info, vs_encode_info = self.handle_subtitles(selected_sub_style)
if not self.frames: if not self.frames:
self.generate_screens( img_job = self.generate_screens(
b_frames, b_frames,
vs_source_info, vs_source_info,
vs_encode_info, vs_encode_info,
temp_screenshot_comparison_dir, screenshot_comparison_dir,
temp_screenshot_sync_dir, screenshot_sync_dir,
selected_sub_style_ref, selected_sub_style_ref,
selected_sub_style_sync, selected_sub_style_sync,
) )
else: else:
self.generate_exact_screens( img_job = self.generate_exact_screens(
vs_source_info, vs_source_info,
vs_encode_info, vs_encode_info,
temp_screenshot_comparison_dir, screenshot_comparison_dir,
) )
final_folder = self.generate_final_folder() return img_job
self.move_images(temp_screenshot_comparison_dir.parent, final_folder)
self.clean_temp()
return final_folder
@staticmethod @staticmethod
def screen_gen_callback(sg_call_back): def screen_gen_callback(sg_call_back):
@ -168,14 +148,13 @@ class GenerateImages:
text=f"Reference\nFrame: {ref_frame}", text=f"Reference\nFrame: {ref_frame}",
style=selected_sub_style_ref, style=selected_sub_style_ref,
) )
ScreenGen( awsmfunc.ScreenGen(
vs_encode_ref_info, vs_encode_ref_info,
frame_numbers=[ref_frame], frame_numbers=[ref_frame],
fpng_compression=1, fpng_compression=1,
folder=screenshot_sync_dir, folder=screenshot_sync_dir,
suffix="b_encode__%d", suffix="b_encode__%d",
callback=self.screen_gen_callback, callback=self.screen_gen_callback,
encoder=self.img_lib,
) )
def generate_sync_screens( def generate_sync_screens(
@ -188,14 +167,13 @@ class GenerateImages:
text=f"Sync\nFrame: {sync_frame}", text=f"Sync\nFrame: {sync_frame}",
style=selected_sub_style_sync, style=selected_sub_style_sync,
) )
ScreenGen( awsmfunc.ScreenGen(
vs_sync_info, vs_sync_info,
frame_numbers=[sync_frame], frame_numbers=[sync_frame],
fpng_compression=1, fpng_compression=1,
folder=Path(screenshot_sync_dir), folder=Path(screenshot_sync_dir),
suffix="a_source__%d", suffix="a_source__%d",
callback=self.screen_gen_callback, callback=self.screen_gen_callback,
encoder=self.img_lib,
) )
def generate_exact_screens( def generate_exact_screens(
@ -203,37 +181,31 @@ class GenerateImages:
vs_source_info, vs_source_info,
vs_encode_info, vs_encode_info,
screenshot_comparison_dir, screenshot_comparison_dir,
) -> Path: ) -> str:
print("\nGenerating screenshots, please wait", flush=True) print("\nGenerating screenshots, please wait", flush=True)
# generate source images # generate source images
ScreenGen( awsmfunc.ScreenGen(
vs_source_info, vs_source_info,
frame_numbers=[ frame_numbers=self.frames,
self.frames[i] for i in range(len(self.frames)) if i % 2 == 0
],
fpng_compression=1, fpng_compression=1,
folder=screenshot_comparison_dir, folder=screenshot_comparison_dir,
suffix="a_source__%d", suffix="a_source__%d",
callback=self.screen_gen_callback, callback=self.screen_gen_callback,
encoder=self.img_lib,
) )
# generate encode images # generate encode images
ScreenGen( awsmfunc.ScreenGen(
vs_encode_info, vs_encode_info,
frame_numbers=[ frame_numbers=self.frames,
self.frames[i] for i in range(len(self.frames)) if i % 2 != 0
],
fpng_compression=1, fpng_compression=1,
folder=screenshot_comparison_dir, folder=screenshot_comparison_dir,
suffix="b_encode__%d", suffix="b_encode__%d",
callback=self.screen_gen_callback, callback=self.screen_gen_callback,
encoder=self.img_lib,
) )
print("Screen generation completed", flush=True) print("Screen generation completed", flush=True)
return screenshot_comparison_dir return str(screenshot_comparison_dir)
def generate_screens( def generate_screens(
self, self,
@ -244,7 +216,7 @@ class GenerateImages:
screenshot_sync_dir, screenshot_sync_dir,
selected_sub_style_ref, selected_sub_style_ref,
selected_sub_style_sync, selected_sub_style_sync,
) -> Path: ) -> str:
print("\nGenerating screenshots, please wait", flush=True) print("\nGenerating screenshots, please wait", flush=True)
# handle re_sync if needed # handle re_sync if needed
@ -261,25 +233,23 @@ class GenerateImages:
sync_frames = b_frames sync_frames = b_frames
# generate source images # generate source images
ScreenGen( awsmfunc.ScreenGen(
vs_source_info, vs_source_info,
frame_numbers=sync_frames, frame_numbers=sync_frames,
fpng_compression=1, fpng_compression=1,
folder=screenshot_comparison_dir, folder=screenshot_comparison_dir,
suffix="a_source__%d", suffix="a_source__%d",
callback=self.screen_gen_callback, callback=self.screen_gen_callback,
encoder=self.img_lib,
) )
# generate encode images # generate encode images
ScreenGen( awsmfunc.ScreenGen(
vs_encode_info, vs_encode_info,
frame_numbers=b_frames, frame_numbers=b_frames,
fpng_compression=1, fpng_compression=1,
folder=screenshot_comparison_dir, folder=screenshot_comparison_dir,
suffix="b_encode__%d", suffix="b_encode__%d",
callback=self.screen_gen_callback, callback=self.screen_gen_callback,
encoder=self.img_lib,
) )
# generate some sync frames # generate some sync frames
@ -318,13 +288,13 @@ class GenerateImages:
) )
print("Screen generation completed", flush=True) print("Screen generation completed", flush=True)
return screenshot_comparison_dir return str(screenshot_comparison_dir)
def handle_subtitles(self, selected_sub_style): def handle_subtitles(self, selected_sub_style):
vs_source_info = self.core.sub.Subtitle( vs_source_info = self.core.sub.Subtitle(
clip=self.source_node, text="Source", style=selected_sub_style clip=self.source_node, text="Source", style=selected_sub_style
) )
vs_encode_info = FrameInfo( vs_encode_info = awsmfunc.FrameInfo(
clip=self.encode_node, clip=self.encode_node,
title=self.release_sub_title if self.release_sub_title else "", title=self.release_sub_title if self.release_sub_title else "",
style=selected_sub_style, style=selected_sub_style,
@ -334,8 +304,10 @@ class GenerateImages:
def handle_hdr(self): def handle_hdr(self):
if self.tone_map: if self.tone_map:
self.source_node = DynamicTonemap(clip=self.source_node, libplacebo=False) self.source_node = awsmfunc.DynamicTonemap(
self.encode_node = DynamicTonemap( clip=self.source_node, libplacebo=False
)
self.encode_node = awsmfunc.DynamicTonemap(
clip=self.encode_node, clip=self.encode_node,
reference=self.reference_source_file, reference=self.reference_source_file,
libplacebo=False, libplacebo=False,
@ -392,8 +364,8 @@ class GenerateImages:
bottom=self.bottom_crop if self.bottom_crop else 0, bottom=self.bottom_crop if self.bottom_crop else 0,
) )
def generate_final_folder(self) -> Path: def generate_folders(self):
print("\nCreating final output folder", flush=True) print("\nCreating folders for images", flush=True)
if self.image_dir: if self.image_dir:
image_output_dir = Path(self.image_dir) image_output_dir = Path(self.image_dir)
else: else:
@ -401,61 +373,34 @@ class GenerateImages:
Path(self.encode_file).parent / f"{Path(self.encode_file).stem}_images" Path(self.encode_file).parent / f"{Path(self.encode_file).stem}_images"
) )
if image_output_dir.exists(): # remove any accent characters from path
for folder in ("img_comparison", "img_selected", "img_sync"): image_output_dir = Path(unidecode(str(image_output_dir)))
rm_path = image_output_dir / folder
if rm_path.is_dir() and rm_path.exists():
shutil.rmtree(rm_path, ignore_errors=True)
# 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) 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]: # create sync image directory and define it as variable
print("\nCreating temporary folders for images", flush=True) Path(Path(image_output_dir) / "img_sync").mkdir(exist_ok=True)
self.temp_dir = Path(tempfile.mkdtemp(prefix="ff_")) screenshot_sync_dir = str(Path(Path(image_output_dir) / "img_sync"))
screenshot_comparison_dir = Path(Path(self.temp_dir) / "img_comparison") # create sub directories
screenshot_comparison_dir.mkdir(exist_ok=True) Path(Path(image_output_dir) / "img_sync/sync1").mkdir(exist_ok=True)
Path(Path(image_output_dir) / "img_sync/sync2").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)
print("Folder creation completed", flush=True) print("Folder creation completed", flush=True)
return screenshot_comparison_dir, selected_dir, screenshot_sync_dir return screenshot_comparison_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")
def get_b_frames(self, num_source_frames): def get_b_frames(self, num_source_frames):
print( print(
@ -472,11 +417,9 @@ class GenerateImages:
) )
try: try:
pict_types = ("B", b"B")
for i, frame in enumerate(b_frames): for i, frame in enumerate(b_frames):
while ( while (
self.encode_node.get_frame(frame).props["_PictType"] self.encode_node.get_frame(frame).props["_PictType"].decode() != "B"
not in pict_types
): ):
frame += 1 frame += 1
b_frames[i] = frame b_frames[i] = frame
@ -535,9 +478,10 @@ class GenerateImages:
flush=True, flush=True,
) )
def _index_source_lsmash(self): def index_lsmash(self):
print("Indexing source", flush=True) print("Indexing source", flush=True)
# index source file
# if index is found in the StaxRip temp working directory, attempt to use it # if index is found in the StaxRip temp working directory, attempt to use it
if ( if (
Path(str(Path(self.source_file).with_suffix("")) + "_temp/").is_dir() Path(str(Path(self.source_file).with_suffix("")) + "_temp/").is_dir()
@ -547,18 +491,12 @@ class GenerateImages:
): ):
print("Index found in StaxRip temp, attempting to use", flush=True) print("Index found in StaxRip temp, attempting to use", flush=True)
# define cache path
lwi_cache_path = Path( lwi_cache_path = Path(
str(Path(self.source_file).with_suffix("")) + "_temp/temp.lwi" str(Path(self.source_file).with_suffix("")) + "_temp/temp.lwi"
) )
elif self.source_index_path.exists(): # try to use index on source file with the cache path
print("Index found, attempting to use", flush=True)
lwi_cache_path = self.source_index_path
# if no existing index is found index source file
else:
lwi_cache_path = Path(Path(self.source_file).with_suffix(".lwi"))
try: try:
self.source_node = self.core.lsmas.LWLibavSource( self.source_node = self.core.lsmas.LWLibavSource(
source=self.source_file, cachefile=lwi_cache_path source=self.source_file, cachefile=lwi_cache_path
@ -567,42 +505,66 @@ class GenerateImages:
source=self.source_file, cachefile=lwi_cache_path source=self.source_file, cachefile=lwi_cache_path
) )
print("Using existing index", flush=True) print("Using existing index", flush=True)
# if index cannot be used
except vs.Error: except vs.Error:
print("L-Smash version miss-match, indexing source again", flush=True) 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.source_node = self.core.lsmas.LWLibavSource(self.source_file)
self.reference_source_file = self.core.lsmas.LWLibavSource(self.source_file) self.reference_source_file = self.core.lsmas.LWLibavSource(
self.source_file
)
print("Source index completed", flush=True) # if no existing index is found index source file
else:
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
)
def _index_encode_lsmash(self): print("Source index completed\n\nIndexing encode", flush=True)
print("\nIndexing encode", flush=True)
if self.encode_index_path: # define a path for encode index to go
cache_path_enc = self.encode_index_path 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: else:
cache_path_enc = Path(Path(self.encode_file).with_suffix(".lwi")) cache_path_enc = Path(Path(self.encode_file).with_suffix(".lwi"))
try: try:
# create index
self.encode_node = self.core.lsmas.LWLibavSource( self.encode_node = self.core.lsmas.LWLibavSource(
self.encode_file, cachefile=cache_path_enc self.encode_file, cachefile=cache_path_enc
) )
except vs.Error: except vs.Error:
# delete index
cache_path_enc.unlink(missing_ok=True) cache_path_enc.unlink(missing_ok=True)
# create index
self.encode_node = self.core.lsmas.LWLibavSource( self.encode_node = self.core.lsmas.LWLibavSource(
self.encode_file, cachefile=cache_path_enc self.encode_file, cachefile=cache_path_enc
) )
print("Encode index completed", flush=True) print("Encode index completed", flush=True)
def index_lsmash(self): def index_ffms2(self):
"""Index source/encode with lsmash"""
self._index_source_lsmash()
self._index_encode_lsmash()
def _index_source_ffms2(self):
print("Indexing source", flush=True) print("Indexing source", flush=True)
# index source file
# if index is found in the StaxRip temp working directory, attempt to use it # if index is found in the StaxRip temp working directory, attempt to use it
if ( if (
Path(str(Path(self.source_file).with_suffix("")) + "_temp/").is_dir() Path(str(Path(self.source_file).with_suffix("")) + "_temp/").is_dir()
@ -612,51 +574,57 @@ class GenerateImages:
): ):
print("Index found in StaxRip temp, attempting to use", flush=True) print("Index found in StaxRip temp, attempting to use", flush=True)
# define cache path
ffindex_cache_path = Path( ffindex_cache_path = Path(
str(Path(self.source_file).with_suffix("")) + "_temp/temp.ffindex" str(Path(self.source_file).with_suffix("")) + "_temp/temp.ffindex"
) )
elif self.source_index_path.exists(): # try to use index on source file with the cache path
print("Index found, attempting to use", flush=True) try:
ffindex_cache_path = self.source_index_path 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 # if no existing index is found index source file
else: 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: try:
self.source_node = self.core.ffms2.Source( # create index
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( print(
"FFMS2 library doesn't allow progress, please wait while the index is completed", "FFMS2 library doesn't allow progress, please wait while the index is completed",
flush=True, flush=True,
) )
self.source_node = self.core.ffms2.Source( self.source_node = self.core.ffms2.Source(self.source_file)
self.source_file, cachefile=ffindex_cache_path self.reference_source_file = self.core.ffms2.Source(self.source_file)
) except vs.Error:
self.reference_source_file = self.core.ffms2.Source( # delete index
self.source_file, cachefile=ffindex_cache_path 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", flush=True) print("Source index completed\n\nIndexing encode", flush=True)
def _index_encode_ffms2(self): # define a path for encode index to go
print("\nIndexing encode", flush=True) if self.index_dir:
index_base_path = Path(self.index_dir) / Path(self.encode_file).name
if self.encode_index_path: cache_path_enc = Path(str(index_base_path) + ".ffindex")
cache_path_enc = self.encode_index_path
else: else:
cache_path_enc = Path(str(self.encode_file) + ".ffindex") cache_path_enc = Path(self.encode_file + ".ffindex")
try: try:
self.encode_node = self.core.ffms2.Source( self.encode_node = self.core.ffms2.Source(
@ -670,12 +638,6 @@ class GenerateImages:
print("Encode index completed", flush=True) print("Encode index completed", flush=True)
def index_ffms2(self):
"""Index source/encode with ffms2"""
self._index_source_ffms2()
self._index_encode_ffms2()
def load_plugins(self): def load_plugins(self):
plugin_path = get_working_dir() / "img_plugins" plugin_path = get_working_dir() / "img_plugins"
if not plugin_path.is_dir() and not plugin_path.exists(): if not plugin_path.is_dir() and not plugin_path.exists():
@ -683,21 +645,3 @@ class GenerateImages:
else: else:
for plugin in plugin_path.glob("*.dll"): for plugin in plugin_path.glob("*.dll"):
self.core.std.LoadPlugin(Path(plugin).resolve()) 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)

@ -22,7 +22,6 @@ hiddenimports = [
"collections.namedtuple", "collections.namedtuple",
"collections.abc.Iterable", "collections.abc.Iterable",
"collections.abc.Mapping", "collections.abc.Mapping",
"concurrent.futures",
"concurrent.futures.Future", "concurrent.futures.Future",
"fractions", "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]] [[package]]
name = "altgraph" name = "altgraph"
@ -17,8 +17,10 @@ version = "1.3.4"
description = "awesome VapourSynth functions" description = "awesome VapourSynth functions"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [] files = [
develop = false {file = "awsmfunc-1.3.4-py3-none-any.whl", hash = "sha256:d9ce9cf90dfdb66b4561a5d3b011232e663ad0d879e2a276827bff9b8b3b37e1"},
{file = "awsmfunc-1.3.4.tar.gz", hash = "sha256:8330332f5c4818322b4090b24499b1dc4e4e371460de70c4bd62a112f4157255"},
]
[package.dependencies] [package.dependencies]
numpy = "*" numpy = "*"
@ -27,13 +29,7 @@ vs-rekt = ">=1.0.0"
vsutil = ">=0.7.0" vsutil = ">=0.7.0"
[package.extras] [package.extras]
dev = ["ruff", "toml"] dev = ["pylint", "toml", "yapf"]
[package.source]
type = "git"
url = "https://github.com/OpusGang/awsmfunc"
reference = "HEAD"
resolved_reference = "e1290f799162749fc627951290bfb4089f2f39cb"
[[package]] [[package]]
name = "black" name = "black"
@ -220,10 +216,10 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
[[package]] [[package]]
name = "pyinstaller" name = "pyinstaller"
version = "6.10.0" version = "6.3.0"
description = "PyInstaller bundles a Python application and all its dependencies into a single package." description = "PyInstaller bundles a Python application and all its dependencies into a single package."
optional = false optional = false
python-versions = "<3.14,>=3.8" python-versions = "<3.13,>=3.8"
files = [] files = []
develop = true develop = true
@ -232,7 +228,7 @@ altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
packaging = ">=22.0" packaging = ">=22.0"
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} 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\""} pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0" setuptools = ">=42.0.0"
@ -246,19 +242,15 @@ url = "custom-pyinstaller"
[[package]] [[package]]
name = "pyinstaller-hooks-contrib" name = "pyinstaller-hooks-contrib"
version = "2024.8" version = "2023.11"
description = "Community maintained hooks for PyInstaller" description = "Community maintained hooks for PyInstaller"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.7"
files = [ files = [
{file = "pyinstaller_hooks_contrib-2024.8-py3-none-any.whl", hash = "sha256:0057fe9a5c398d3f580e73e58793a1d4a8315ca91c3df01efea1c14ed557825a"}, {file = "pyinstaller-hooks-contrib-2023.11.tar.gz", hash = "sha256:5dd7a8a054a65c19cdaa381cabcfbe76f44d5f88d18214b0c570a0cd139be77f"},
{file = "pyinstaller_hooks_contrib-2024.8.tar.gz", hash = "sha256:29b68d878ab739e967055b56a93eb9b58e529d5b054fbab7a2f2bacf80cef3e2"}, {file = "pyinstaller_hooks_contrib-2023.11-py2.py3-none-any.whl", hash = "sha256:f2a75dac2968ec81f92dcd3768906f654fa4204bc496126ae8483e87a5d89602"},
] ]
[package.dependencies]
packaging = ">=22.0"
setuptools = ">=42.0.0"
[[package]] [[package]]
name = "pywin32-ctypes" name = "pywin32-ctypes"
version = "0.2.2" 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 = ["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"] 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]] [[package]]
name = "vapoursynth" name = "vapoursynth"
version = "65" version = "64"
description = "A frameserver for the 21st century" description = "A frameserver for the 21st century"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "VapourSynth-65-cp311-cp311-win_amd64.whl", hash = "sha256:9fab72bb6dbcdd8d0e6643da68908f85387bc764b836524e97a1e6e8989ca13e"}, {file = "VapourSynth-64-cp311-cp311-win_amd64.whl", hash = "sha256:ad046a704537276f7ebb4132e1210e8922ec4b2403a4b44f5fe30fd9456a6412"},
{file = "VapourSynth-65-cp38-cp38-win_amd64.whl", hash = "sha256:d9b49b595dc929d63250bd82f05e75238b6dfc4a50ab08e7fc4fe4f8888f6b95"}, {file = "VapourSynth-64-cp38-cp38-win_amd64.whl", hash = "sha256:794f93bcaa1ce79510c11962f677a7d18685a0c29aa36586cb307d1eb9d7f2e0"},
{file = "VapourSynth-65.zip", hash = "sha256:1d42d461ef9988a3477134e478a2291d79f3469635cde8af2c66b1e87c36f711"}, {file = "VapourSynth-64.zip", hash = "sha256:29425a135ca68cbb17b3dfcad0097375b75f94af1f499ba89bcd2b03b2651846"},
] ]
[[package]] [[package]]
name = "vapoursynth-portable" name = "vapoursynth-portable"
version = "65" version = "64"
description = "A frameserver for the 21st century" description = "A frameserver for the 21st century"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ 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] [package.dependencies]
vapoursynth = "65" vapoursynth = "64"
[[package]] [[package]]
name = "vs-rekt" name = "vs-rekt"
@ -342,5 +345,5 @@ vapoursynth = "*"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "3.11.9" python-versions = "3.11.5"
content-hash = "42cace5dbe92132d04512c3dbc5a60c1cb06fa883a7030eb74f8f0e782b48226" content-hash = "820bfb62c5fba1b17221bc1a02144932dbce64fa44c698f24f7205c2fc4a3d18"

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