Skip to content
Merged
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
Binary file modified .DS_Store
Binary file not shown.
174 changes: 0 additions & 174 deletions docs/README.md

This file was deleted.

112 changes: 112 additions & 0 deletions docs/assembly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
title: Assembly
layout: default
nav_order: 5
parent: Library
permalink: assembly
---

# Assembly
The assembly file contains the functions which act to combine the various files created during the GuideFrame pipeline. The following section will list each function contained within this file and provide some insight into its use and syntax.

### `assemble_audio_video()`
```python
def assemble_audio_video(video_file, audio_file, output_file):
# Check that both files exist
if os.path.exists(video_file) and os.path.exists(audio_file):
# Create video and audio in variables for us in combined output
video_in = ffmpeg.input(video_file)
audio_in = ffmpeg.input(audio_file)
combined_output = ffmpeg.output(video_in, audio_in, output_file, vcodec='copy', acodec='copy')
try:
(
combined_output.run()
)
print(f"Successfully created: {output_file}")
# Attempting to extract an error via the wrapper if one occurs
except ffmpeg.Error as e:
# Outputting the error
error_output = e.stderr.decode('utf-8') if e.stderr else "No error details available."
print(f"Error combining {video_file} and {audio_file}: {error_output}")
else:
print(f"Missing video or audio file: {video_file}, {audio_file}")
```
This function takes a `video_file`, `audio_file` and `output_file` as arguments. It checks for these files before using the `ffmpeg` python package to combine them into a single file, named by the passed argument. This file then contains the combined audio and video for a single `guide_step`.

### `combine_all_videos()`
```python
def combine_all_videos(output_files, final_output):
# Temp text file to iterate through
file_list = "file_list.txt"

# Write the list of video files to the same file
with open(file_list, "w") as f:
for video in output_files:
if os.path.exists(video):
f.write(f"file '{video}'\n")
else:
print(f"Error: {video} not found")

try:
# Run FFmpeg using the concat method and check for errors etc
ffmpeg.input(file_list, format="concat", safe=0).output(final_output, vcodec='libxvid', acodec='aac').run()
print(f"Successfully combined all videos into {final_output}")
except ffmpeg.Error as e:
error_output = e.stderr.decode('utf-8') if e.stderr else "No error details available."
print(f"Error combining videos: {error_output}")
finally:
# Removing the txt file
os.remove(file_list)
```
This function takes `output_files` and `final_output` as arguments. It takes the array of passed files and writes them to a newly created text file called `file_list`. The `concat` function from `ffmpeg` is used with the `file_list` passed. This is the `input` portion of the command before the `output` portion uses the `final_output` name for outputted file name.

### `assemble()`
```python
def assemble(number_of_steps):
# Combine individual video and audio for each step by iterating through files and passing to above functions
clip_number = number_of_steps + 1
for i in range(1, clip_number):
video_file = f"step{i}.mp4"
audio_file = f"step{i}.mp3"
output_file = f"output_step{i}.mp4"
# Call the function to combine video and audio
assemble_audio_video(video_file, audio_file, output_file)

# Now that all video/audio combinations are complete, combine the output videos into the final one
output_files = [f"output_step{i}.mp4" for i in range(1, clip_number)]
# Now using the extract_script_name function from guideframe_utils.py to get the script name
script_name = extract_script_name()
# Creating a unique filename for the final output file via uuid and the extracted script name
output_filename = f"{script_name}_{uuid.uuid4().hex[:6]}.mp4"
# Combining all the videos into the final output as a single video
combine_all_videos(output_files, output_filename)

# Check if final_output exists and if so, clean up temporary files (the various mp3 and mp4 files we created)
if os.path.exists(output_filename):
print("Final output created. Cleaning up temporary files...")
# Cleanup loop
for i in range(1, clip_number):
step_video = f"step{i}.mp4"
step_audio = f"step{i}.mp3"
output_step = f"output_step{i}.mp4"
if os.path.exists(step_video):
os.remove(step_video)
print(f"Removed {step_video}")
if os.path.exists(step_audio):
os.remove(step_audio)
if os.path.exists(output_step):
os.remove(output_step)
print(f"Removed {output_step}")
print("Cleanup complete.")
else:
print("Final output not found. No cleanup performed.")
```
This function uses the two others from this file to perform the overall assembly and cleanup of GuideFrame step files. It takes the `clip_number` argument and uses it to iterate through a loop of all audio and video files in addition to creating the appropriately named `output_file` during the loop.

*Note: 1 is added to account for steps starting at 1. This means an iteration from 1 -> `clip_number` will have the intended range.*

Within each loop, these files are passed to the `assemble_audio_video()` function for combination.

An array of the files outputted by this process is then created using another loop and the `clip_number` variable. A `script_name` variable is intialised using `extract_script_name()` from `utils`. An output file is then created using the `script_name` and a randomly generated `uuid`. The array and filename are then passed to `combine_all_videos()` for final assembly.

Once this process is complete, a check that a matching file exists is performed. Provided this is successful, a cleanup loop occurs using the `clip_number` variable again to iterate through all created audio and video files with the exception of the final output.
75 changes: 75 additions & 0 deletions docs/audio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: Audio
layout: default
nav_order: 3
parent: Library
permalink: audio
---

# Audio
The audio file contains functions designed to provide the voiceover for each GuideFrame step. It interacts with both `gTTS` and markdown in order to create these mp3 files. The following section will list each function contained within this file and provide some insight into its use and syntax.

### `export_gtts()`
```python
def export_gtts(text, file_name):
tts = gTTS(text)
tts.save(file_name)
print("Exported", file_name)
```
This function uses the `gTTS` python package in order to generate audio based on the user-prescribed text. It takes the `text` argument and passes it, along with a `file_name` to the native `gTTS` functions. This then writes an audio file, with the passed name and featuring the prescribed text, to the local directory.

### `sleep_based_on_vo()`
```python
def sleep_based_on_vo(file_name):
audio = MP3(file_name)
print("Sleeping for", audio.info.length, "seconds")
time.sleep(audio.info.length)
```
This function is designed to prevent the main script's interactions from accelerating beyond the recorded voiceover. It achieves this by taking the `file_name` of the .mp3 file created during the above function. It then parses the length of this audio file before using the `sleep` function from the `time` package to sleep based on the length found in seconds. This ensures that an interaction cannot occur until the requisite voiceover clip has completed.

### `pull_vo_from_markdown()`
```python
def pull_vo_from_markdown(md_file, step_number):
# Open the markdown file and read
with open(md_file, "r", encoding="utf-8") as file:
md_content = file.read()

'''
Regex pattern breakdown:

## Step {step_number} -> The step heading to match
\s* -> Any whitespace characters before the content
(.*?) -> The content under the step heading
(?=\n##|\Z) -> A lookahead to match the next step heading (##) or the end of the file
'''

# Define the regex pattern for the step heading (explained above)
step_heading = rf"## Step {step_number}\s*(.*?)\s*(?=\n##|\Z)"

# Search the markdown content for the step heading
match = re.search(step_heading, md_content, re.DOTALL)

# Return the content under the step heading if found
return match.group(1).strip() if match else None
```
This function takes the `md_file` and `step_number` as arguments. It uses these to extract the text content of the markdown file by opening it and then using the `re` package to perform a regex parse (outlined in above code comments). This pattern ensures that the text must follow a `##` heading with text matching "Step n*". Provided a match is found, it is then returned.

### `generate_voiceover()`
```python
def generate_voicover(md_file, step_number):
# Extract voiceover text from the .md file
voiceover = pull_vo_from_markdown(md_file, step_number) # parsing the markdown

# Check if content was found
if not voiceover:
print(f"Warning: No content found for Step {step_number}")
return

# Export the voiceover to an MP3 file
export_gtts(voiceover, f"step{step_number}.mp3")
# Sleeping based on the length of the voiceover
sleep_based_on_vo(f"step{step_number}.mp3")
```
This function brings the above functions together. It takes `md_file` and `step_number` as arguments before passing these into a call to `pull_vo_from_markdown()`. It then checks for the presence of resulting voiceover.

It then uses the `export_gtts()` function where it passes the voiceover created above along with a file name created using the `step_number` variable. Finally, it calls the `sleep_based_on_vo()` function to complete the generation cycle.
Loading