Skip to content

Instantly share code, notes, and snippets.

@cprima
Last active August 25, 2023 05:43
Show Gist options
  • Save cprima/d0973019db4d0654a364a1498f45ff80 to your computer and use it in GitHub Desktop.
Save cprima/d0973019db4d0654a364a1498f45ff80 to your computer and use it in GitHub Desktop.
asciinema truncator 🎬✂️ A Python script to truncate recording sessions of `asciinema`. This tool is perfect for users who want to remove specific time intervals from their recorded sessions, ensuring the end result is clean and only showcases what's necessary.

asciinema truncator 🎬✂️

A Python script to truncate recording sessions of asciinema. This tool is perfect for users who want to remove specific time intervals from their recorded sessions, ensuring the end result is clean and only showcases what's necessary.

Features

  • Removes specified time intervals from an asciinema recording.
  • Automatically adjusts timestamps to maintain continuity after truncation.
  • Maintains the integrity of the NDJSON structure of the recording.

Prerequisites

  • Python 3.x
    • the requirements.txt only contains json
  • Jupyter Notebook (if you're running the script in Jupyter)

Usage

  1. Record your terminal session with asciinema.

    asciinema rec filename.cast
  2. Run the script in a Jupyter Notebook or as a standalone script, providing:

    • Path to the recorded .cast file.
    • Path to an output file to save the processed contents.
    • List of timestamp pairs representing the start and end of intervals you want to truncate.
    # Run the script and get processing statistics
    processing_stats = truncate_asciinema(input_file="filename.cast", output_file="truncated.cast", timestamp_ranges=[(10, 20), (40, 60)])
    
    # Print processing statistics
    for stat in processing_stats:
        print(stat)
  3. The script will create a new .cast file with the specified intervals removed.

Sample

Here's a simple use case to better understand the script's functionality:

Input timestamps: 0, 3, 4, 6, 8, 10, 12, 15, 18, 20, 22, 25, 31, 49, 50

Timestamp ranges to truncate: (10, 20) and (30, 40)

Output timestamps: 0, 3, 4, 6, 8, 10, 13, 31, 32

See a truncated output file on teh asciinema website at https://asciinema.org/a/cPkBEGvwHSL2eE5OUzjeBClrl

Contribute

Feedback and pull requests are welcome. If you find any issues or have suggestions, please write a comment.

License

This project is licensed under the MIT License.

def create_mock_input_file(filepath):
with open(filepath, 'w') as f:
f.write('{"version": 2, "width": 80, "height": 24, "timestamp": 1625094175, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}\n')
timestamps = [0, 3, 4, 6, 8, 10, 12, 15, 18, 20, 22, 25, 31, 49, 50]
for i, timestamp in enumerate(timestamps):
f.write(json.dumps([timestamp, 'o', f"log entry {i} at timestamp {timestamp}"]) + '\n')
def test_truncate_asciinema():
# 1. Generate the mock input file
input_file_path = 'mock_input.txt'
output_file_path = 'test_output.cast'
create_mock_input_file(input_file_path)
# 2. Apply the truncate_asciinema function
stats = truncate_asciinema(input_file_path, output_file_path, [(10, 20), (30, 40)])
for stat in stats:
print(stat)
# 3. Validate the output
with open(output_file_path, 'r') as f:
lines = f.readlines()
# Skipping the metadata line
timestamps = [json.loads(line)[0] for line in lines[1:]]
expected_timestamps = [0, 3, 4, 6, 8, 10, 13, 31, 32]
assert timestamps == expected_timestamps, f"Expected {expected_timestamps}, but got {timestamps}"
print("Test passed!")
test_truncate_asciinema()
import json
class TruncateAsciinemaError(Exception):
pass
def truncate_asciinema(input_file, output_file, timestamp_ranges):
stats = []
try:
# Check if output_file is writeable
with open(output_file, 'w'):
pass
except IOError:
raise TruncateAsciinemaError(f"Output file '{output_file}' is not writeable.")
# Ensure output file has .cast extension
if not output_file.endswith('.cast'):
output_file += '.cast'
# First pass: Calculate deltas
try:
with open(input_file, 'r') as f:
lines = f.readlines()
previous_timestamp = 0
deltas = []
for line in lines[1:]: # Skip metadata
try:
timestamp, _, _ = json.loads(line)
except json.JSONDecodeError:
raise TruncateAsciinemaError("Error decoding JSON in input file.")
delta = timestamp - previous_timestamp
deltas.append(delta)
previous_timestamp = timestamp
deltas.append(0) # Final delta is 0
except IOError:
raise TruncateAsciinemaError(f"Error reading input file '{input_file}'.")
# Filter out timestamp ranges not within input timestamps
valid_timestamp_ranges = []
for start, end in timestamp_ranges:
if any(start <= timestamp <= end for timestamp, _, _ in (json.loads(line) for line in lines[1:])):
valid_timestamp_ranges.append((start, end))
# Second pass: Write lines, truncating and adjusting as needed
try:
with open(input_file, 'r') as infile, open(output_file, 'w') as outfile:
# Write metadata
metadata = infile.readline()
outfile.write(metadata)
current_index = 0
last_written_ts = 0
for line in lines[1:]:
try:
timestamp, second_item, _ = json.loads(line)
except json.JSONDecodeError:
raise TruncateAsciinemaError("Error decoding JSON in input file.")
in_truncation_range = any(start <= timestamp <= end for start, end in valid_timestamp_ranges)
if not in_truncation_range:
# Adjust timestamp
timestamp = last_written_ts + deltas[current_index]
outfile.write(json.dumps([timestamp, second_item, _]) + '\n')
last_written_ts = timestamp
current_index += 1
except IOError:
raise TruncateAsciinemaError(f"Error reading/writing files '{input_file}' or '{output_file}'.")
# Prepare stats
stats.append("Starting 2nd pass...")
stats.append(f"Total input lines: {len(lines)}")
stats.append(f"Maximum input timestamp: {previous_timestamp}")
try:
with open(output_file, 'r') as f:
output_lines = f.readlines()
except IOError:
raise TruncateAsciinemaError(f"Error reading output file '{output_file}'.")
stats.append(f"Total output lines: {len(output_lines)}")
stats.append(f"Maximum output timestamp: {json.loads(output_lines[-1])[0]}")
stats.append(f"Timestamp delta: {previous_timestamp - json.loads(output_lines[-1])[0]}")
stats.append("End processing.")
return stats
@cprima
Copy link
Author

cprima commented Aug 16, 2023

Roadmap:

[ ] get feedback
[x] rework truncate_asciinema to not print stats
[ ] more tests
[ ] …

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment