Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add pip install to readme.txt #210

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,81 @@
# jumpcutter
![image](https://user-images.githubusercontent.com/46163555/93026937-aef09e80-f5d7-11ea-9caf-e929fe461a07.png)


# jumpcutter + PySimpleGUI

This project is a mashup between jumpcutter, by carykh, and a front-end GUI using PySimpleGUI.

The original jumpcutter.py code has had a single line modification, not because of the GUI, but because of an error that's generated if a sample rate is chosen. This error is likely due to a change in one of the packages being used by jumpcutter. The argparse definition says it's a float, but it should have said int.

Code was changed to:

```python
parser.add_argument('--sample_rate', type=int, default=44100, help="sample rate of the input and output videos")
```
## GUI Integration

To start the GUI, run the file jumpcutter_gui.py. This GUI integrates with the jumpcutter.py file by "running" it as if it were a command line program. It's launched as a subprocess with the parameters collected using the GUI being passed as arguments to the program.

## GUI

When GUI is initially started it looks like this, with default parameters filled in. These are the same defaults as the jumpcutter.py file has specified, so it's safe for you to clear them all if you don't need to change any of them. A handy "Clear All" button is provided to do this.

![image](https://user-images.githubusercontent.com/46163555/93027127-e14ecb80-f5d8-11ea-839f-c3c2bfc0c446.png)

## PyCharm Edit Button

If you want to be able to launch PyCharm to edit the code using the "PyCharm Me" button, you will need to change the `PYCHARM` constant located at the top of the program. It's currently set to:

```python
PYCHARM = r"C:\Program Files\JetBrains\PyCharm Community Edition 2019.1.1\bin\pycharm.bat"
```

If you locate your PyCharm folder, you should be able to locate the batch file in the bin folder. If it's not an important feature to you, simply delete the button.

## Temp Limitiations

There is currently one limitation...

* The URL downloading doesn't appear to work, or it didn't the first time I tried it.

It's next on the list to deal with.

## Incredible Tool!!

OK, so the reason this tool has a GUI added is that it's a miraculous tool in my opinion. At least it was for my videos. They all feel "tighter" to me. The time I spend fumbling around typing, trying not to make mistakes, all the while not talking adds up to about 18% - 20% of my videos. That's the amount of time, on average, each video was compressed. Over the whole series, that's quite a bit of time. Not only do they sound better, but viewers actually save real time viewing them. It's a win win.

Hopefully your videos will get the same kind of benefits mine did.

## Side By Side Examples

I am so enamored by this tool that I processed my entire 18 part series of PySimpleGUI lessons.

Here is the original playlist:

https://www.youtube.com/playlist?list=PLl8dD0doyrvFfzzniWS7FXrZefWWExJ2e


And here is a new playlist with all of the videos modified:

https://www.youtube.com/playlist?list=PLl8dD0doyrvF1nLakJJ7sl8OX2YSHclqn


I don't have a total amount of time saved, but I can tell you that each video is 17-20% shorter. That's quite a bit of a difference! Not only are they shorter, but they "feel tighter". I appear to have my act together when in fact I'm fumbling around trying to type with as few errors as possible, with the keyboard being obscured by my microphone.

## Future Test

I'm going to make an experimental "typical how-to video" to show the potential power of this tool. The idea is to be as fumbling and lost as possible and see if the tool will correct things to be much more direct and to the point.

## Attention Video Creators...

If you're a video creator, it's really easy to use this tool. YouTube provides you a link to download your video. You can then run the video through this tool, using the GUI, and upload the new video as a new YouTube video.

If you have good results using the tool, I would love to hear from you. Please open an Issue on this GitHub and post somelinks. This will give visiblity to other users of your results.

The remainder of the readme is from Cary's original repo:
------------------------------------

## jumpcutter
Automatically edits videos. Explanation here: https://www.youtube.com/watch?v=DQ8orIurGxw

## Some heads-up:
Expand Down
22 changes: 14 additions & 8 deletions jumpcutter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import os
import argparse
from pytube import YouTube
import PySimpleGUI as sg



def downloadFile(url):
name = YouTube(url).streams.first().download()
Expand Down Expand Up @@ -39,19 +42,21 @@ def inputToOutputFilename(filename):

def createPath(s):
#assert (not os.path.exists(s)), "The filepath "+s+" already exists. Don't want to overwrite it. Aborting."

try:
deletePath(s)
try:
os.mkdir(s)
except OSError:
assert False, "Creation of the directory %s failed. (The TEMP folder may already exist. Delete or rename it, and try again.)"

def deletePath(s): # Dangerous! Watch out!
try:
try:
rmtree(s,ignore_errors=False)
except OSError:
print ("Deletion of the directory %s failed" % s)
print(OSError)



parser = argparse.ArgumentParser(description='Modifies a video file to play at different speeds when there is sound vs. silence.')
parser.add_argument('--input_file', type=str, help='the video file you want modified')
parser.add_argument('--url', type=str, help='A youtube url to download and process')
Expand All @@ -60,7 +65,7 @@ def deletePath(s): # Dangerous! Watch out!
parser.add_argument('--sounded_speed', type=float, default=1.00, help="the speed that sounded (spoken) frames should be played at. Typically 1.")
parser.add_argument('--silent_speed', type=float, default=5.00, help="the speed that silent frames should be played at. 999999 for jumpcutting.")
parser.add_argument('--frame_margin', type=float, default=1, help="some silent frames adjacent to sounded frames are included to provide context. How many frames on either the side of speech should be included? That's this variable.")
parser.add_argument('--sample_rate', type=float, default=44100, help="sample rate of the input and output videos")
parser.add_argument('--sample_rate', type=int, default=44100, help="sample rate of the input and output videos")
parser.add_argument('--frame_rate', type=float, default=30, help="frame rate of the input and output videos. optional... I try to find it out myself, but it doesn't always work.")
parser.add_argument('--frame_quality', type=int, default=3, help="quality of frames to be extracted from input video. 1 is highest, 31 is lowest, 3 is the default.")

Expand All @@ -77,6 +82,7 @@ def deletePath(s): # Dangerous! Watch out!
INPUT_FILE = downloadFile(args.url)
else:
INPUT_FILE = args.input_file

URL = args.url
FRAME_QUALITY = args.frame_quality

Expand All @@ -89,13 +95,13 @@ def deletePath(s): # Dangerous! Watch out!

TEMP_FOLDER = "TEMP"
AUDIO_FADE_ENVELOPE_SIZE = 400 # smooth out transitiion's audio by quickly fading in/out (arbitrary magic number whatever)

createPath(TEMP_FOLDER)

command = "ffmpeg -i "+INPUT_FILE+" -qscale:v "+str(FRAME_QUALITY)+" "+TEMP_FOLDER+"/frame%06d.jpg -hide_banner"
command = "ffmpeg -i \""+INPUT_FILE+"\" -qscale:v "+str(FRAME_QUALITY)+" "+TEMP_FOLDER+"/frame%06d.jpg -hide_banner"
subprocess.call(command, shell=True)

command = "ffmpeg -i "+INPUT_FILE+" -ab 160k -ac 2 -ar "+str(SAMPLE_RATE)+" -vn "+TEMP_FOLDER+"/audio.wav"
command = "ffmpeg -i \""+INPUT_FILE+"\" -ab 160k -ac 2 -ar "+str(SAMPLE_RATE)+" -vn "+TEMP_FOLDER+"/audio.wav"

subprocess.call(command, shell=True)

Expand Down Expand Up @@ -197,7 +203,7 @@ def deletePath(s): # Dangerous! Watch out!
copyFrame(int(audioSampleCount/samplesPerFrame)-1,endGap)
'''

command = "ffmpeg -framerate "+str(frameRate)+" -i "+TEMP_FOLDER+"/newFrame%06d.jpg -i "+TEMP_FOLDER+"/audioNew.wav -strict -2 "+OUTPUT_FILE
command = "ffmpeg -framerate "+str(frameRate)+" -i "+TEMP_FOLDER+"/newFrame%06d.jpg -i "+TEMP_FOLDER+"/audioNew.wav -strict -2 \""+OUTPUT_FILE+"\""
subprocess.call(command, shell=True)

deletePath(TEMP_FOLDER)
Expand Down
159 changes: 159 additions & 0 deletions jumpcutter_gui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import PySimpleGUI as sg
import subprocess
import sys
print(sg.version)

help_text = \
"""
Jumpcutter GUI

This is a front-end GUI for a command line tool named jumpcutter.

jumpcutter is a command line based tool written by Carykh. You'll find the repo here:
https://github.com/carykh/jumpcutter

The design of this GUI was made in a way that should not have required any changes to the
jumpcutter.py file. However, there appears to be a bug in the original code. The sample rate
argument was specified as a float, but this later causes a crash in the program, so a single
change was made to line 68, changing the parameter from a float to an int. You can get around
this change by not specifying a default value in this GUI. Rather than specifying 44100, leave it blank
which will cause the parameter to be skipped.

This kind of GUI can be applied to a large number of other commandline programs.

NOTE - it has not yet been tested on Linux. It's only been tested on Windows. Hoping to get it
tested out on Linux shortly.

KNOWN Problem - filenames with spaces. Working on it. For now, make a temp folder and make sure everything
has no spaces and you'll be fine. YouTube download wasn't working on the video I tried

Copyright 2020 PySimpleGUI.org
"""

# The path to the program that will edit code in PyCharm
PYCHARM = r"C:\Program Files\JetBrains\PyCharm Community Edition 2019.1.1\bin\pycharm.bat"

version = '14 Sept 2020'

def FText(text, in_key=None, default=None, tooltip=None, input_size=None, text_size=None):
"""
A "User Defined Element" - Fixed-sized Text Input. Returns a row with a Text and an Input element.
Modify to expose more or less parameters. Avoid **kwargs so that parameters are named.
"""
if input_size is None:
input_size = (20, 1)
if text_size is None:
text_size = (20, 1)
return [sg.Text(text, size=text_size, justification='r', tooltip=tooltip),
sg.Input(default_text=default, key=in_key, size=input_size, tooltip=tooltip)]


def main():

# This version of the GUI uses this large dictionary to drive 100% of the creation of the
# layout that collections the parameters for the command line call. It's really simplistic
# at the moment with a tuple containing information about each entry.
# The definition of the GUI. Defines:
# PSG Input Key
# Tuple of items needed to build a line in the layout
# 0 - The command line's parameter
# 1 - The text to display next to input
# 2 - The default value for the input
# 3 - Size of input field (None for default)
# 4 - Tooltip string
# 5 - List of additional elements to include on the same row

input_defintion = {
'-FILE-' : ('--input_file', 'Input File', '', (40,1),'the video file you want modified', [sg.FileBrowse()]),
'-URL-' : ('--url','URL (not yet working)', '', (40,1), 'A youtube url to download and process', []),
'-OUT FILE-' : ('--output_file', 'Output File', '', (40,1), "the output file. (optional. if not included, it'll just modify the input file name)", [sg.FileSaveAs()]),
'-SILENT THRESHOLD-' : ('--silent_threshold', 'Silent Threshold', 0.03, None, "the volume amount that frames' audio needs to surpass to be consider \"sounded\". It ranges from 0 (silence) to 1 (max volume)", []),
'-SOUNDED SPEED-' : ('--sounded_speed', 'Sounded Speed', 1.00, None, "the speed that sounded (spoken) frames should be played at. Typically 1.", []),
'-SILENT SPEED-' : ('--silent_speed', 'Silent Speed', 5.00, None, "the speed that silent frames should be played at. 999999 for jumpcutting.", []),
'-FRAME MARGIN-' : ('--frame_margin', 'Frame Margin', 1, None, "some silent frames adjacent to sounded frames are included to provide context. How many frames on either the side of speech should be included? That's this variable.", []),
'-SAMPLE RATE-' : ('--sample_rate', 'Sample Rate', 44100, None, "sample rate of the input and output videos", []),
'-FRAME RATE-' : ('--frame_rate', 'Frame Rate', 30, None, "frame rate of the input and output videos. optional... I try to find it out myself, but it doesn't always work.", []),
'-FRAME QUALITY-' : ('--frame_quality', 'Frame Quality', 3, None, "quality of frames to be extracted from input video. 1 is highest, 31 is lowest, 3 is the default.", [])
}

# the command that will be invoked with the parameters
command_to_run = r'python .\jumpcutter.py '

# Find longest input descrption which is index 1 in table
text_len = max([len(input_defintion[key][1]) for key in input_defintion])
# Top part of layout that's not table driven
layout = [[sg.Text('Jump Cutter - Comress Silence in a Video', font='Any 20')]]
# Computed part of layout that's based on the dictionary of attributes (the table driven part)
for key in input_defintion:
layout_def = input_defintion[key]
line = FText(layout_def[1], in_key=key, default=layout_def[2], tooltip=layout_def[4], input_size=layout_def[3], text_size=(text_len,1))
if layout_def[5] != []:
line += layout_def[5]
layout += [line]
# Bottom part of layout that's not table driven
layout += [[sg.Text('Constructed Command Line:')],
[sg.Text(size=(80,3), key='-COMMAND LINE-', text_color='yellow', font='Courier 8')],
[sg.Text('Command Line Output:')],
[sg.Multiline(size=(80,10), reroute_stdout=True, reroute_stderr=False, reroute_cprint=True, write_only=True, font='Courier 8', autoscroll=True, key='-ML-')],
[sg.Button('Start'), sg.Button('Clear All'), sg.Button('PyCharm Me'), sg.Button('Help'), sg.Button('Exit'), sg.Checkbox('Test Mode (Do not run command line)', key='-CBOX-')],
[sg.Text(f'Version = {version} PySimpleGUI Version {sg.version.split(" ")[0]}', font='Any 8', text_color='yellow')]]

window = sg.Window('Jump Cutter', layout, finalize=True) # adding finalize in case a print is added later before read

while True:
event, values = window.read()
if event in (sg.WIN_CLOSED, 'Exit'): # if window was closed
break
elif event == 'Start': # if start button
parms = ''
for key in values:
if key not in input_defintion:
continue
if values[key] != '':
if 'file' in input_defintion[key][0]:
parms += f'{input_defintion[key][0]} "{values[key]}" '
else:
parms += f"{input_defintion[key][0]} {values[key]} "

command = command_to_run + parms
window['-COMMAND LINE-'].update(command)
if not values['-CBOX-']:
sg.popup_quick_message('Beginning conversion... this will take a long time... your window may appear',
'like it is not responding, but it will continue to be ruuning.',
'Do not close the window. You will see a red colored "DONE" message in the',
'Command Line Output area once the conversion has completed', line_width=90, keep_on_top=True, background_color='red', text_color='white', auto_close_duration=4)
runCommand(cmd=command, window=window)
sg.cprint('*'*20+'DONE'+'*'*20, background_color='red', text_color='white')
sg.popup('*'*20+'DONE'+'*'*20, title='Completed Jumpcutting!', background_color='red', text_color='white', keep_on_top=True)
elif event == 'Clear All': # if clearing, erase all elements except buttons
# Will cause some heads to explode 👍🏻
_ = [window[elem].update('') for elem in values if window[elem].Type != sg.ELEM_TYPE_BUTTON]
elif event == 'PyCharm Me': # edit this file using PyCharm
subprocess.Popen([PYCHARM, __file__], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
elif event == 'Help': # display the "help text" (comment header at top of program)
sg.popup(help_text, line_width=len(max(help_text.split('\n'), key=len)))
window.close()


def runCommand(cmd, timeout=None, window=None):
""" run shell command
@param cmd: command to execute
@param timeout: timeout for command execution
@param window: the PySimpleGUI window that the output is going to (needed to do refresh on)
@return: (return code from command, command output)
"""
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = ''
for line in p.stdout:
line = line.decode(errors='replace' if (sys.version_info) < (3, 5) else 'backslashreplace').rstrip()
output += line
print(line)
window.refresh() if window else None # yes, a 1-line if, so shoot me

retval = p.wait(timeout)
return (retval, output)


if __name__ == '__main__':
sg.theme('Dark Grey 9')
main()
Loading