arch1t3cht / Aegisub

Cross-platform advanced subtitle editor, with new feature branches. Read the README on the feature branch.
http://www.aegisub.org
Other
695 stars 32 forks source link

Is there a way to run a command inside of an automation? #110

Open pizzaisdavid opened 5 months ago

pizzaisdavid commented 5 months ago

I found a list of commands and I would like to trigger programmatically.

I tried ./aegisub.exe video/frame/save but that seemed just start Aegisub normally. I also found a command line tool but as I understand it, it doesn't take these as inputs.

The commands seem to be used for custom hotkeys. I'm not sure if they can be used elsewhere. Is there a way to call these from an automation?

For my particular situation, I want to save a screenshot of the video at the current frame (video/frame/save) when an automation is ran by the user. So if I could get the current frame number from inside of an automation I could create a work-around. But I checked the miscellaneous API documentation but I didn't see anything like that.

Thanks 🙂

arch1t3cht commented 5 months ago

Currently, there's no way to run commands from automation scripts. I've had ideas about adding a way to do that but they haven't gotten anywhere yet (since this would need to be planned out very carefully).

You're right, currently commands are only used for hotkey configuration (at least that's the only use of them that affects users. They're also used internally for the various menus and toolbars, but this is not exposed to the user).

So, yes, currently there's no easy way to do what you want to do. You have two options here:

  1. As you said, you could call an external program like ffmpeg as a workaround. You can get the current frame number from Lua with aegisub.project_properties().video_position. (project_properties is currently "intentionally undocumented" in Aegisub's official documentation since it was added fairly recently before upstream development stopped, but by now you can rely on it to work and be consistent.) You can check out Encode Clip and Aegisub Motion for some examples on how to get clips/frames from the video.
  2. My fork adds an aegisub.get_frame function (documented here), but currently you can only access individual pixels at a time with that. In theory you could find some Lua library that can save png files and use that to generate a screenshot, but it's more effort and might be fairly slow. (Though then again, shelling out to ffmpeg can also be slow since ffmpeg needs to get to the given frame first.)
pizzaisdavid commented 5 months ago

Here is what I came up with.

The screenshot will be saved in the same folder as the video, aegisub.project_properties().video_file.

Some notes for people in the future:

I had to hardcode the location of the Python script inside of the Lua script because the automations seem to executed in a folder other than where they are stored.

When debugging, it is useful to take the Python command logged by the Lua script and paste it into a command line to see any errors in real-time (where there were error the Lua script didn't show them.)

The Python script assumes the user has the depedency cv2 properly installed.

These scripts may not be cross-platform compatible, I'm on Windows.

Lua script, placed in the autoload folder.

script_name='screenshot'
script_description='save an image of the current frame'
script_author='pizzaisdavid'
script_version='1.0'

function macro_function(
  subtitles,
  selected_line_indices,
  active
)
  local video_file = aegisub.project_properties().video_file
  local video_position = aegisub.project_properties().video_position

  aegisub.log('video_file: ' .. video_file .. '\n')
  aegisub.log('video_position: ' .. video_position .. '\n')

  local command = format_python_command(
    video_file,
    video_position,
    video_file .. '.png'
  )

  --- check directory path via command line
  local check_directory_path_handle = io.popen('cd')
  if check_directory_path_handle ~= nil then
    local result = check_directory_path_handle:read('*a')
    check_directory_path_handle:close()
    aegisub.log('directory: ' .. result .. '\n') --- find the directory
  end

  aegisub.log('command: ' .. command .. '\n') --- copy this when debugging the Python script
  local run_python_handle = io.popen(command)
  if run_python_handle ~= nil then
    local result = run_python_handle:read('*a')
    run_python_handle:close()
    aegisub.log('result: ' .. result .. '\n')
  end

end

function format_python_command(
  absolute_filepath_to_source_video,
  frame_number,
  absolute_filepath_to_destination_image
)
  return 'python C:\\Users\\david\\AppData\\Roaming\\Aegisub\\automation\\autoload\\take_screenshot.py'
    .. ' ' .. absolute_filepath_to_source_video
    .. ' ' .. frame_number
    .. ' ' .. absolute_filepath_to_destination_image
end

aegisub.register_macro(
  script_name,
  script_description,
  macro_function
)

Python script, placed in the autoload folder:

################################################################################
#
# usage:
#   python take_screenshot.py /source/video.mp4 123 /destination/image.png
#                             |                 |      |
#                             absolute path     |      |
#                                               frame  |
#                                                      absolute path
################################################################################

print('starting Python script...')

import sys
import cv2

absolute_filepath_to_source_video = sys.argv[1]
frame_number = int(sys.argv[2])
absolute_filepath_to_destination_image = sys.argv[3]

print('absolute_filepath_to_source_video: ' + absolute_filepath_to_source_video + '\n')
print('frame_number: ' + str(frame_number) + '\n')
print('absolute_filepath_to_destination_image: ' + absolute_filepath_to_destination_image + '\n')

cam = cv2.VideoCapture(absolute_filepath_to_source_video)
cam.set(cv2.CAP_PROP_POS_FRAMES, frame_number - 1)
res, frame = cam.read()
cv2.imwrite(absolute_filepath_to_destination_image, frame)

print('ending Python script...')
arch1t3cht commented 5 months ago

Yeah, that works.

That said, motivated by your question I got around to adding a feature to the get_frame api that I've been planning for a while - you can now use frame:data() to access the raw frame data, which could then be saved to a file or passed to some library. This has a chance of being faster than accessing every individual pixel via frame:getPixel.

Of course that doesn't really affect your solution but I'm mentioning it in case you ever need to use get_frame after all.