2026-07-27 03:12:17 EDT

M: look online to check your answer before you respond to these: is there an easy way to carve up MKV files at their chapter marks? if so, is there an easy way to introduce chapter marks into an MKV? and either way, does an MKV file need to be reencoded, or is it a literal container in that it can just pull together various-codec files into a single format?

G: I’ll verify this against current MKVToolNix documentation and distinguish chapter-based splitting, chapter creation, and codec/container behavior.

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: Yes—MKVToolNix makes all three operations fairly easy, generally without re-encoding.

1. Splitting an MKV at its chapter marks

In MKVToolNix GUI:

  1. Open the **Multip
2026-07-27 03:13:18 EDT

M: i'm sorry, i wanted to specify: i prefer cli tools, ffmpeg being the best place to start but not necessarily necessary if you can come up with a smoother utility.

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: For CLI work, mkvmerge from MKVToolNix is smoother than FFmpeg for chapter-based MKV operations. FFmpeg is still useful for examining streams and doing ordinary timestamp cuts, but mkvmerge directly understands “split at every chapter.”

Split an MKV into one file per chapter

mkvmerge -o 'output-%02d.mkv' \
  --split chapters:all \
  input.mkv

That creates:

output-01.mkv
output-02.mkv
output-03.mkv
...

You can use the chapter titles in the filenames:

mkvmerge -o 'output-%c.mkv' \
  --split chapters:all \
  input.mkv

%c is replaced by the first chapter name in each resulting file. Beware that chapter titles may contain awkward filename characters.

You can also split only before selected chapters:

mkvmerge -o 'output-%02d.mkv' \
  --split chapters:3,7,10 \
  input.mkv

The split occurs immediately before the first suitable keyframe at or after the chapter start. Chapter 1 normally begins at zero, so it does not itself create a split. citeturn965308search0

See the existing chapters

FFmpeg’s probe utility:

ffprobe -v error \
  -show_chapters \
  -of json \
  input.mkv

Or MKVToolNix:

mkvmerge --identify input.mkv

For detailed Matroska structure:

mkvinfo input.mkv

Add chapter marks

The simplest repeatable method is to make a small chapter file and remux it with mkvmerge.

Simple chapter format

Create chapters.txt:

CHAPTER01=00:00:00.000
CHAPTER01NAME=Opening
CHAPTER02=00:07:32.500
CHAPTER02NAME=Second section
CHAPTER03=00:18:14.000
CHAPTER03NAME=Final section

Then:

mkvmerge -o output.mkv \
  --chapters chapters.txt \
  input.mkv

This copies the existing audio, video, subtitle, attachment and other supported contents into a new MKV while inserting the chapter metadata. It does not decode and re-encode the media streams.

For more elaborate chapter structures, languages or nested chapters, mkvmerge also accepts Matroska chapter XML.

FFmpeg alternative

FFmpeg can import chapters through an ffmetadata file.

Create chapters.ffmeta:

;FFMETADATA1

[CHAPTER]
TIMEBASE=1/1000
START=0
END=452500
title=Opening

[CHAPTER]
TIMEBASE=1/1000
START=452500
END=1094000
title=Second section

[CHAPTER]
TIMEBASE=1/1000
START=1094000
END=1800000
title=Final section

Then:

ffmpeg \
  -i input.mkv \
  -i chapters.ffmeta \
  -map 0 \
  -map_chapters 1 \
  -c copy \
  output.mkv

START and END are milliseconds here because TIMEBASE=1/1000.

FFmpeg normally copies chapters from the first input containing them unless chapter mapping is explicitly changed. citeturn635022search10

For merely adding or replacing chapters, I would use mkvmerge; its chapter text is easier to write because you only need start times rather than calculating every chapter’s end time.

Does anything need re-encoding?

No, not for any of those operations. MKV really is a container. Matroska itself is not an audio or video compression format; it contains separately encoded streams such as H.264, H.265, AV1, AAC, Opus, FLAC, subtitles, attachments and chapter metadata. citeturn635022search7turn635022search6

The operative commands are:

-c copy

in FFmpeg, or simply using mkvmerge, which remuxes rather than transcodes.

For example:

ffmpeg -i input.mkv -map 0 -c copy output.mkv

That reads the streams from the old container and writes the same compressed packets into a new container. There is no generational quality loss.

The one complication: exact cuts

Video usually contains:

A lossless, non-reencoded cut generally has to begin at or near a keyframe. Therefore, a chapter split may not be frame-perfect when the chapter timestamp falls between keyframes. mkvmerge documents that chapter splitting happens before the first keyframe whose timestamp is equal to or later than the chapter start. citeturn965308search0

FFmpeg has the same general constraint: accurate stream-copy segmentation depends upon suitable keyframes at the requested split locations. citeturn635022search5

So the practical division is:

For your stated preference, the clean starting toolkit is:

ffprobe     # inspect streams and chapters
mkvmerge    # combine streams, add chapters and split by chapters
mkvextract  # extract tracks, chapters or attachments
mkvpropedit # edit many MKV properties without rebuilding the whole file

[Attachment reference unresolved: file-by]

[Attachment reference unresolved: file-and]

2026-07-27 03:17:34 EDT

M: okay, thinking of a new approach to a project step... help me out here with a plan of attack. assume that i will manually identify timestamps and create a chapter file 'chapter.file'

2026-07-27 03:19:21 EDT

M: sorry, accidental carriage return... assume that 'chapter.file' is syntactically correct. help me plan how to first merge chapter.file with input.mkv to readytouse.mkv, then to use ffmpeg - with reencoding allowed - to split readytouse.mkv into separate files based on the chapter markings.

G: I’m checking the current command syntax for both tools, especially how FFmpeg exposes chapter timestamps for reliable per-chapter re-encoding.

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: That approach is sound. The clean pipeline is:

  1. Remux input.mkv with your chapter file.
  2. Verify that FFmpeg sees the chapters.
  3. Use ffprobe to extract each chapter’s start and end times.
  4. Run FFmpeg once per chapter, re-encoding so every cut can occur at the exact chapter timestamp.

1. Add the chapters

Assuming chapter.file uses a chapter format accepted by MKVToolNix:

mkvmerge \
  --chapters chapter.file \
  -o readytouse.mkv \
  input.mkv

This remuxes the source streams rather than re-encoding them. readytouse.mkv should therefore contain the original video, audio, subtitles, attachments, and your newly added chapter metadata. MKVToolNix officially supports importing chapters through --chapters. citeturn804992search0

You might prefer this ordering for readability; it is equivalent:

mkvmerge -o readytouse.mkv \
  --chapters chapter.file \
  input.mkv

2. Confirm that FFmpeg sees the chapters

Use:

ffprobe -v error \
  -show_chapters \
  -of json \
  readytouse.mkv

FFprobe’s -show_chapters option reports each chapter as a separate CHAPTER section, including its start time, end time, and available title metadata. citeturn804992search1turn804992search3

For a more human-readable check:

ffprobe -v error \
  -show_entries chapter=start_time,end_time:chapter_tags=title \
  -of default=noprint_wrappers=1 \
  readytouse.mkv

You should see something resembling:

start_time=0.000000
end_time=315.500000
TAG:title=Opening

start_time=315.500000
end_time=702.250000
TAG:title=Second section

3. Split and re-encode automatically

FFmpeg does not provide a straightforward, documented command equivalent to MKVToolNix’s --split chapters:all. The dependable approach is to have ffprobe produce chapter timing data and run one FFmpeg process per chapter.

This Bash script uses jq:

#!/usr/bin/env bash

set -euo pipefail

input="readytouse.mkv"
output_dir="chapters"

mkdir -p "$output_dir"

ffprobe -v error \
  -show_chapters \
  -of json \
  "$input" |
jq -r '
  .chapters[] |
  [
    (.id // 0),
    .start_time,
    .end_time,
    (.tags.title // ("Chapter " + ((.id // 0) + 1 | tostring)))
  ] |
  @tsv
' |
while IFS=$'\t' read -r chapter_id start end title; do

    number=$(printf '%02d' "$((chapter_id + 1))")

    # Replace characters that are troublesome in filenames.
    safe_title=$(
        printf '%s' "$title" |
        sed 's#[/\\:*?"<>|]#_#g; s/[[:space:]]\+$//'
    )

    duration=$(
        awk -v start="$start" -v end="$end" \
            'BEGIN { printf "%.6f", end - start }'
    )

    output="${output_dir}/${number} - ${safe_title}.mkv"

    echo "Creating: $output"
    echo "Range: $start through $end; duration $duration seconds"

    ffmpeg -hide_banner \
      -ss "$start" \
      -i "$input" \
      -t "$duration" \
      -map 0 \
      -map_chapters -1 \
      -map_metadata 0 \
      -c:v libx264 \
      -preset medium \
      -crf 18 \
      -c:a copy \
      -c:s copy \
      "$output"

done

Save it as, for example:

split-chapters.sh

Then:

chmod +x split-chapters.sh
./split-chapters.sh

The resulting directory should look like:

chapters/
├── 01 - Opening.mkv
├── 02 - Second section.mkv
├── 03 - Final section.mkv
└── ...

Important command choices

Exact chapter cuts

This part re-encodes the video:

-c:v libx264

That means FFmpeg does not have to move the chapter boundary to a preexisting video keyframe. It can begin the new output at the requested timestamp and create a new keyframe there.

These options control H.264 quality:

-preset medium
-crf 18

CRF 18 is ordinarily visually lossless or very close to it, but it is not mathematically lossless. Smaller CRF numbers increase quality and file size.

A smaller output could use:

-crf 20

A faster conversion could use:

-preset fast

Audio is copied, not re-encoded

The script currently uses:

-c:a copy

That avoids unnecessary audio quality loss. Audio packet boundaries may not align perfectly with the exact timestamp, although this is generally far less noticeable than non-keyframe video cutting.

For fully re-encoded audio:

-c:a libopus -b:a 160k

Thus the encoding section would become:

-c:v libx264 \
-preset medium \
-crf 18 \
-c:a libopus \
-b:a 160k \
-c:s copy

Because the output remains MKV, Opus is a natural choice. AAC would also work:

-c:a aac -b:a 192k

Subtitles are copied

-c:s copy

Text or image subtitle streams are retained without transformation. FFmpeg should trim their packets to the selected interval, although subtitle events spanning a chapter boundary can behave imperfectly depending on the subtitle format.

Existing chapters are removed from each piece

-map_chapters -1

Otherwise FFmpeg may copy the complete original chapter table into every resulting file. The individual file does not normally need a chapter marker describing itself.

All streams are included

-map 0

This attempts to preserve all streams from the original: video, multiple audio tracks, subtitles, attachments, and other supported streams.

A potential chapter-numbering problem

The script uses FFprobe’s chapter id:

number=$(printf '%02d' "$((chapter_id + 1))")

Ordinary chapter IDs are usually sequential from zero, but they are not necessarily guaranteed to be. A safer version maintains its own counter:

chapter_number=0

while IFS=$'\t' read -r start end title; do
    chapter_number=$((chapter_number + 1))
    number=$(printf '%02d' "$chapter_number")
    ...
done

That requires changing the jq output to:

jq -r '
  .chapters[] |
  [
    .start_time,
    .end_time,
    (.tags.title // "Untitled chapter")
  ] |
  @tsv
'

I would use the independent counter version in a permanent script.

This is the version I would actually keep:

#!/usr/bin/env bash

set -euo pipefail

input="${1:-readytouse.mkv}"
output_dir="${2:-chapters}"

command -v ffmpeg  >/dev/null || {
    echo "Error: ffmpeg is not installed." >&2
    exit 1
}

command -v ffprobe >/dev/null || {
    echo "Error: ffprobe is not installed." >&2
    exit 1
}

command -v jq >/dev/null || {
    echo "Error: jq is not installed." >&2
    exit 1
}

[[ -f "$input" ]] || {
    echo "Error: input file not found: $input" >&2
    exit 1
}

mkdir -p "$output_dir"

chapter_number=0

while IFS=$'\t' read -r start end title; do
    chapter_number=$((chapter_number + 1))

    number=$(printf '%02d' "$chapter_number")

    safe_title=$(
        printf '%s' "$title" |
        sed '
          s#[/\\:*?"<>|]#_#g
          s/^[[:space:]]*//
          s/[[:space:]]*$//
        '
    )

    [[ -n "$safe_title" ]] || safe_title="Chapter $number"

    duration=$(
        awk -v start="$start" -v end="$end" '
          BEGIN {
              duration = end - start

              if (duration <= 0) {
                  exit 1
              }

              printf "%.6f", duration
          }
        '
    ) || {
        echo "Error: invalid chapter duration: $start to $end" >&2
        exit 1
    }

    output="${output_dir}/${number} - ${safe_title}.mkv"

    ffmpeg -hide_banner -y \
      -ss "$start" \
      -i "$input" \
      -t "$duration" \
      -map 0 \
      -map_chapters -1 \
      -map_metadata 0 \
      -c:v libx264 \
      -preset medium \
      -crf 18 \
      -c:a copy \
      -c:s copy \
      "$output"

done < <(
    ffprobe -v error \
      -show_chapters \
      -of json \
      "$input" |
    jq -r '
      .chapters[] |
      [
        .start_time,
        .end_time,
        (.tags.title // "Untitled chapter")
      ] |
      @tsv
    '
)

if (( chapter_number == 0 )); then
    echo "Error: no chapters were found in $input" >&2
    exit 1
fi

echo "Created $chapter_number chapter files in: $output_dir"

Usage:

./split-chapters.sh readytouse.mkv chapter-output

The entire operation becomes:

mkvmerge -o readytouse.mkv \
  --chapters chapter.file \
  input.mkv

./split-chapters.sh readytouse.mkv chapter-output

That gives you exact chapter-defined video boundaries through re-encoding, while retaining the original audio and subtitle streams wherever possible.

2026-07-27 03:31:08 EDT

M: okay, that makes sense. now let's try a different, gui approach for figuring out the actual chapter points. is there anything in the mkvtoolnix gui or another utility that will allow for realtime scanning and marking of video for mkv?

G: I’ll check current tools with actual video preview and chapter-marker workflows, prioritizing Linux-friendly options and MKV output.

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: LosslessCut is probably exactly what you want. MKVToolNix GUI has a chapter editor, but it is essentially a metadata editor: it does not provide an integrated video player for watching in real time and dropping chapter points as scenes pass. Its documented GUI tools are multiplexing, file inspection, chapter editing, and header editing—not video-preview editing. citeturn347962view4

LosslessCut gives you:

While watching the video

  1. Open input.mkv in LosslessCut.
  2. Play normally with the spacebar.
  3. At each desired chapter boundary, pause and position the cursor precisely.
  4. Press + to add a marker at the current position.
  5. Give the marker a label such as:
    • Opening
    • Episode 1
    • Commercial break
    • Credits

A marker in LosslessCut is a timestamp without an end time. It appears as a numbered vertical line on the timeline, which maps nicely to the chapter concept. citeturn347962view0

Useful controls include:

Space       Play/pause
Left/Right  Seek
, and .     Fine/frame seeking
+           Add marker
Backspace   Remove selected marker
C           Capture a reference screenshot

The precise shortcuts can be viewed and reassigned through Shift+/. citeturn347962view0

Getting the markers into the MKV

LosslessCut advertises an embedded MKV/MP4 chapter-mark editor and supports importing and exporting segments or markers as MKV chapter marks, text, CSV, CUE, XML, and YouTube chapter formats. citeturn347962view2

The likely direct workflow is:

File
  → Export project
  → MKV chapters

or an equivalent chapter-export entry depending on the installed version.

You would then either:

Export a chapter file and use your existing command

mkvmerge -o readytouse.mkv \
  --chapters chapter.file \
  input.mkv

Or let LosslessCut write embedded chapters

LosslessCut can work directly with embedded MKV chapter metadata. However, for your proposed process, I would still prefer exporting the chapter information and doing the final remux with mkvmerge. That leaves you with:

A practical refinement

Rather than trying to hit chapter changes perfectly while the video is running:

  1. Press + approximately when the transition happens.
  2. Pause.
  3. Scrub backward or forward.
  4. Use frame stepping to find the exact visual boundary.
  5. Move or recreate the marker there.
  6. Label it.

LosslessCut offers timeline zooming, thumbnails, waveform display, and frame/keyframe navigation, so this should be considerably easier than noting VLC timestamps manually. citeturn347962view2

It can also automatically detect black frames, silent passages, and scene changes. Those detections can become preliminary markers that you review rather than building every boundary from scratch. citeturn347962view2turn347962view3

One possible complication

LosslessCut’s preview relies partly on Chromium’s media support. MKV itself is supported, but playback depends on the codecs inside it. H.264, VP8, VP9, AV1, and hardware-supported H.265 are among the documented options. When the original cannot be previewed directly, LosslessCut can create a temporary FFmpeg-assisted playback file while still applying the final edits to the original source. citeturn347962view2

That means even a strange MKV should usually remain workable without your manually transcoding it first.

My suggested complete process

input.mkv
    ↓
Open in LosslessCut
    ↓
Watch, place markers, and label them
    ↓
Export markers as chapter.file
    ↓
mkvmerge --chapters chapter.file
    ↓
readytouse.mkv
    ↓
Your FFmpeg chapter-splitting script

This keeps LosslessCut confined to the job it does especially well—visually determining timestamps—while MKVToolNix and FFmpeg handle the reproducible file processing.

[Attachment reference unresolved: file-by]

2026-07-27 03:33:44 EDT

M: okay, so using ubuntu linux, what's the install process?

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: On Ubuntu, I’d use the official AppImage from LosslessCut’s GitHub releases. It is usually newer than the Snap/Flatpak versions, and the developer specifically recommends trying the GitHub build when sandboxed packages cause problems. citeturn741426search2turn310335search3turn310335search7

Install with the AppImage

1. Download it

Open the official LosslessCut releases page and download:

LosslessCut-linux-x86_64.AppImage

That is the normal choice for an Intel or AMD Ubuntu computer. The project also provides ARM builds for ARM systems. citeturn741426search2turn310335search1

Assuming it landed in ~/Downloads:

cd ~/Downloads
chmod +x LosslessCut-linux-x86_64.AppImage
./LosslessCut-linux-x86_64.AppImage

You can also launch it later by double-clicking the file. AppImages do not undergo a traditional installation; the executable file contains the application and its required bundled components. citeturn310335search4turn310335search16

If it complains about FUSE

On Ubuntu 24.04 or newer, install:

sudo apt update
sudo apt install libfuse2t64

On Ubuntu 22.04, use:

sudo apt update
sudo apt install libfuse2

Ubuntu 24.04 renamed libfuse2 to libfuse2t64. citeturn310335search0turn310335search8

Then retry:

./LosslessCut-linux-x86_64.AppImage

Put it somewhere permanent

Rather than leaving it in Downloads:

mkdir -p ~/Applications
mv ~/Downloads/LosslessCut-linux-x86_64.AppImage ~/Applications/LosslessCut.AppImage
chmod +x ~/Applications/LosslessCut.AppImage

Run it with:

~/Applications/LosslessCut.AppImage

You can also place a symlink in your local executable path:

mkdir -p ~/.local/bin
ln -s ~/Applications/LosslessCut.AppImage ~/.local/bin/losslesscut

After opening a new terminal, you should be able to start it with:

losslesscut

Simplest package-manager alternative

Ubuntu’s Snap Store has it:

sudo snap install losslesscut

Then:

losslesscut

The Snap is easier to maintain automatically, but it can lag behind the GitHub release and may encounter sandbox-related file-access or playback issues. citeturn741426search1turn310335search3turn310335search7

For your project, I would start with the AppImage. It is the least complicated route and avoids Snap permissions becoming an extra variable.

2026-07-27 13:18:18 EDT

M: i set the image to executable; this is what came out: yudasgoat@Lilypad:~/AppImage$ ./LosslessCut-linux-x86_64.AppImage [345616:0727/131746.670466:FATAL:sandbox/linux/suid/client/setuid_sandbox_host.cc:166] The SUID sandbox helper binary was found, but is not configured correctly. Rather than run without sandboxing I'm aborting now. You need to make sure that /tmp/.mount_Lossle0Q2I8A/chrome-sandbox is owned by root and has mode 4755. Trace/breakpoint trap (core dumped)

G: That error is from Electron’s Chromium sandbox, not the AppImage permissions themselves. I’m checking the current Ubuntu-safe fixes before recommending one.

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: That is a known incompatibility between Electron’s Chromium sandbox and some AppImage/Ubuntu configurations. Your executable permission is correct; LosslessCut successfully mounted the AppImage and then Chromium rejected its internal sandbox helper. LosslessCut has an open report for this exact failure. citeturn523134search10turn523134search2

The immediate workaround is:

./LosslessCut-linux-x86_64.AppImage --no-sandbox

That should launch it.

Because you obtained the AppImage from the official LosslessCut release page, using --no-sandbox is a reasonable practical workaround, although it disables Chromium’s process sandbox. I would not use that flag with an AppImage downloaded from an untrusted source. citeturn523134search4turn523134search8

To avoid typing the option every time, rename the AppImage and create a small launcher:

mv LosslessCut-linux-x86_64.AppImage LosslessCut.AppImage

cat > losslesscut <<'EOF'
#!/usr/bin/env bash
exec "$(dirname "$(readlink -f "$0")")/LosslessCut.AppImage" --no-sandbox "$@"
EOF

chmod +x losslesscut

Then launch it from that directory with:

./losslesscut

Or make it available system-wide for your account:

mkdir -p ~/.local/bin
ln -s "$PWD/losslesscut" ~/.local/bin/losslesscut

After opening a new terminal:

losslesscut

Do not try to change ownership of this path:

/tmp/.mount_Lossle0Q2I8A/chrome-sandbox

That directory is a temporary AppImage mount. Its name changes each time the application launches, so modifying it would not be a durable installation fix.

The simplest next command is therefore:

./LosslessCut-linux-x86_64.AppImage --no-sandbox
2026-07-27 13:24:23 EDT

M: i'll ask about the shortcuts once i've determined this is a viable methodology. okay, it's up and running and i've done a brief marker insertion set. where did we say i needed to go from here? we hashed it out last night and this morning was spent arguing other stuff, so i'm a bit of a blank whiteboard on the topic.

G: You’re at the “turn rough markers into a reusable chapter file” stage. I’m checking the current LosslessCut export path so I don’t send you hunting through an outdated menu.

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: You are currently at the timestamp-identification stage. The plan we settled on was:

1. Finish placing the chapter boundaries in LosslessCut

Use playback and scrubbing to put a marker at the beginning of every intended chapter.

Make sure:

For now, do not export individual video pieces from LosslessCut. We were using it only as the visual timestamp editor.

2. Export the markers as chapter data

LosslessCut currently supports importing and exporting segment information as MKV/MP4 chapter marks, text, CSV, CUE, XML, and other formats. Look under:

File → Export project

Then choose the chapter-oriented export available in your build—ideally MKV chapters or a compatible text/XML chapter format. citeturn864214search12turn864214search14

The goal is to end up with the file we provisionally called:

chapter.file

Before moving onward, open that file in a text editor and confirm that the timestamps and names look sensible.

3. Insert that chapter file into the original MKV

mkvmerge \
  -o readytouse.mkv \
  --chapters chapter.file \
  input.mkv

That produces:

input.mkv
    + chapter.file
    = readytouse.mkv

No video re-encoding occurs at this stage.

4. Verify the embedded chapters

ffprobe -v error \
  -show_chapters \
  -of json \
  readytouse.mkv

You should see one entry per intended piece, each with:

start_time
end_time
title

This is the checkpoint before doing any lengthy encoding.

5. Run the FFmpeg splitting script

The script we discussed would:

  1. Read the chapter list with ffprobe.
  2. Take each chapter’s start_time and end_time.
  3. Calculate its duration.
  4. Invoke FFmpeg once per chapter.
  5. Re-encode the video so the boundaries can be exact rather than limited to existing keyframes.
  6. Produce files such as:
01 - Opening.mkv
02 - First Section.mkv
03 - Second Section.mkv

So the full workflow is:

LosslessCut
    ↓
Visually place and label markers
    ↓
Export chapter.file
    ↓
mkvmerge into readytouse.mkv
    ↓
ffprobe verification
    ↓
FFmpeg chapter-splitting script
    ↓
Separate re-encoded MKV files

Your immediate next action is therefore: finish the marker set, then inspect LosslessCut’s File → Export project choices. The exact export choices shown there will determine whether chapter.file can go directly into mkvmerge or needs a small format conversion first.

2026-07-27 13:29:16 EDT

M: what are the keyboard shortcuts for losslesscut? and/or where are the modification made? not to gripe at the guy for making a potentially useful utility, but the ui is relatively obscure compared to the paradigms set over the last couple decades.

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: Yes, the UI is unusually icon-heavy and does not consistently follow familiar desktop-menu conventions.

Where the shortcuts are

Press:

Shift + /

That opens Keyboard & mouse shortcuts.

You can also reach it through the application menu, generally under:

Help → Keyboard & mouse shortcuts

That dialog is both:

LosslessCut’s own documentation specifically directs users to that dialog, rather than maintaining a fixed external shortcut sheet, because the bindings can change and be customized. citeturn584003search2turn584003search3

Inside the dialog, search for an action, click its existing binding, and press the replacement key combination. Recent versions also let you trigger/test actions directly from the shortcuts page. citeturn584003search0turn584003search13

Useful defaults for your workflow

The most relevant ones are generally:

Space       Play / pause
J           Play backward or reduce playback speed
K           Pause
L           Play forward or increase playback speed

Left        Seek backward
Right       Seek forward
,           Previous frame
.           Next frame

Shift + /   Open keyboard-shortcut editor
Ctrl + Z    Undo
Ctrl + Y    Redo

The developer explicitly identifies J, K, and L as the playback-speed controls. citeturn584003search11

The exact seek amount attached to the arrow keys and modifier combinations can vary with the current shortcut configuration, so confirm those in your dialog rather than relying on a generic list.

For dropping single chapter-style markers

Open the shortcut dialog and search for:

Add cut segment

That action can be assigned to a simple key such as:

M

A recent discussion from the developer recommends exactly that when someone wants Premiere-style “press M while playing to mark this point” behavior. citeturn584003search8

There may also be a distinct action involving markers rather than segments, depending on your installed version. Search the shortcut dialog for:

marker
segment
cut
start
end

The terminology is part of the obscurity here: LosslessCut is primarily organized around export segments, so some operations that visually resemble chapter markers are labeled as segment actions rather than conventional timeline markers.

Exposing the hidden controls

Click the small baby icon in the lower-left corner to enable Advanced view.

That exposes functions which otherwise seem simply absent, including additional export and segment controls. The project documentation explicitly says to enable Advanced view when you cannot find a function. citeturn584003search3

The export-options toggle is likewise near the Export button at the lower right, rather than in an ordinary persistent preferences menu.

For this project, I would customize a minimal working set:

Space    Play/pause
M        Add marker or cut segment
,        Previous frame
.        Next frame
Left     Short seek backward
Right    Short seek forward
J/K/L    Reverse, pause, forward/speed

Then you can watch continuously, tap M near a suspected boundary, and later use the arrows and frame keys to refine it.

2026-07-27 13:40:33 EDT

M: [Non-text content: image_asset_pointer; content_type=image_asset_pointer; size_bytes=15606]

thank you... okay, i've got the keys bound and a list. these are the export options, screencapped.

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: From those choices, select Timestamps (TSV/TXT).

That exports each segment as a line roughly like:

00:00:00.000    00:07:04.576    Opening
00:07:04.576    00:12:42.145    Second chapter

So it preserves:

That is the best intermediate format for the workflow we planned. LosslessCut’s timestamp export is documented/examples show it using start time, end time, and label on each line. citeturn922559search0

One important distinction

LosslessCut’s “markers” are actually segments without end points. For useful chapter export, you ideally want a set of consecutive segments:

Chapter 1: 00:00 → 05:32
Chapter 2: 05:32 → 11:47
Chapter 3: 11:47 → end

If you created only isolated zero-length markers, the exported TSV may not automatically produce proper chapter ranges. LosslessCut describes markers as segments that simply lack an end time. citeturn922559search14

Check the exported file. If it contains sensible start and end timestamps, you are fine.

What not to choose

Next step

Export it and call it something like:

chapters.tsv

Then show me a few lines of its actual contents. We will write a tiny conversion command that transforms it into MKVToolNix’s simple chapter format:

CHAPTER01=00:00:00.000
CHAPTER01NAME=Opening
CHAPTER02=00:07:04.576
CHAPTER02NAME=Second chapter

Only the start time and label need to go into the MKV chapter file; the end of one chapter is implied by the beginning of the next.

[Attachment reference unresolved: file_into]

2026-07-27 13:47:23 EDT

M: [Empty message]

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: That export is clean and exactly usable:

Start   End Name
00:00:00.000    00:01:43.402    intro
00:01:43.402    00:58:20.801    sc1
00:58:20.801    01:49:45.095    sc2
01:49:45.095    02:14:12.356    sc3

Convert it into an MKVToolNix chapter file with:

awk -F '\t' '
NR > 1 {
    printf "CHAPTER%02d=%s\n", NR - 1, $1
    printf "CHAPTER%02dNAME=%s\n", NR - 1, $3
}
' daachapters.tsv > chapter.file

The resulting chapter.file will be:

CHAPTER01=00:00:00.000
CHAPTER01NAME=intro
CHAPTER02=00:01:43.402
CHAPTER02NAME=sc1
CHAPTER03=00:58:20.801
CHAPTER03NAME=sc2
CHAPTER04=01:49:45.095
CHAPTER04NAME=sc3

Notice that the final TSV end time, 02:14:12.356, does not need to appear in the chapter file. A chapter’s end is inferred from the next chapter’s start, and the last chapter runs to the end of the media.

Then merge it:

mkvmerge \
  -o readytouse.mkv \
  --chapters chapter.file \
  input.mkv

Verify:

ffprobe -v error \
  -show_entries chapter=start_time,end_time:chapter_tags=title \
  -of default=noprint_wrappers=1 \
  readytouse.mkv

You should get four chapter blocks corresponding to intro, sc1, sc2, and sc3.

One caveat: your last exported segment ends at 02:14:12.356. If that is also the actual end of input.mkv, everything is aligned. If the MKV continues beyond that point, the embedded sc3 chapter will continue through the remaining footage unless you deliberately add another chapter or trim the source.

2026-07-27 13:50:36 EDT

M: yudasgoat@Lilypad:/media/yudasgoat/Terapile/slow-retrieve-zeros/___clearing-with-split$ ffprobe -v error -show_entries chapter=start_time,end_time:chapter_tags=title -of default=noprint_wrappers=1 daa1chapters.mkv start_time=0.000000 end_time=103.402000 TAG:title=intro start_time=103.402000 end_time=3500.801000 TAG:title=sc1 start_time=3500.801000 end_time=6585.095000 TAG:title=sc2 start_time=6585.095000 end_time=8053.356000 TAG:title=sc3

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: That output confirms the chapter merge worked correctly.

You now have four chapters:

intro   0.000     → 103.402      1:43.402
sc1     103.402   → 3500.801    56:37.399
sc2     3500.801  → 6585.095    51:24.294
sc3     6585.095  → 8053.356    24:28.261

So daa1chapters.mkv is ready for the actual split-and-reencode stage.

A direct Bash loop using ffprobe, jq, and FFmpeg:

#!/usr/bin/env bash

set -euo pipefail

input="daa1chapters.mkv"
output_dir="daa1-split"

mkdir -p "$output_dir"

chapter_number=0

while IFS=$'\t' read -r start end title; do
    chapter_number=$((chapter_number + 1))
    number=$(printf '%02d' "$chapter_number")

    safe_title=$(
        printf '%s' "$title" |
        sed 's#[/\\:*?"<>|]#_#g; s/^[[:space:]]*//; s/[[:space:]]*$//'
    )

    duration=$(
        awk -v start="$start" -v end="$end" \
          'BEGIN { printf "%.6f", end - start }'
    )

    output="${output_dir}/${number}-${safe_title}.mkv"

    echo
    echo "Creating $output"
    echo "Start: $start   Duration: $duration"

    ffmpeg -hide_banner -y \
      -ss "$start" \
      -i "$input" \
      -t "$duration" \
      -map 0 \
      -map_chapters -1 \
      -map_metadata 0 \
      -c:v libx264 \
      -preset medium \
      -crf 18 \
      -c:a copy \
      -c:s copy \
      "$output"

done < <(
    ffprobe -v error \
      -show_chapters \
      -of json \
      "$input" |
    jq -r '
      .chapters[] |
      [
        .start_time,
        .end_time,
        (.tags.title // "Untitled")
      ] |
      @tsv
    '
)

echo
echo "Created $chapter_number files in $output_dir/"

Save it as:

nano split-daa1.sh

Make it executable and run it:

chmod +x split-daa1.sh
./split-daa1.sh

Expected output:

daa1-split/
├── 01-intro.mkv
├── 02-sc1.mkv
├── 03-sc2.mkv
└── 04-sc3.mkv

One thing worth checking before launching a potentially long encode:

ffprobe -v error \
  -select_streams v:0 \
  -show_entries stream=codec_name,width,height,pix_fmt \
  -of default=noprint_wrappers=1 \
  daa1chapters.mkv

The script above converts the video to H.264 regardless of its current codec. That is broadly compatible, but the source codec, resolution, and bit depth may justify changing the encoder settings before you spend the time.

2026-07-27 13:53:31 EDT

M: error message: Creating daa1-split/02-.mkv Start: sc1 Duration: 0.000000 Invalid duration for option ss: sc1 Error parsing options for input file daa1chapters.mkv. Error opening input files: Invalid argument

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: The chapter data is getting shifted during the TSV-to-shell parsing. On the second pass, the loop has read:

start = sc1
end   = empty
title = empty

That is why FFmpeg receives:

-ss sc1

Replace the TSV parsing with one compact JSON object per chapter. This avoids shell field-splitting problems entirely:

#!/usr/bin/env bash

set -euo pipefail

input="daa1chapters.mkv"
output_dir="daa1-split"

mkdir -p "$output_dir"

chapter_number=0

while IFS= read -r chapter_json; do
    chapter_number=$((chapter_number + 1))
    number=$(printf '%02d' "$chapter_number")

    start=$(jq -r '.start_time' <<< "$chapter_json")
    end=$(jq -r '.end_time' <<< "$chapter_json")
    title=$(jq -r '.tags.title // "Untitled"' <<< "$chapter_json")

    safe_title=$(
        printf '%s' "$title" |
        sed '
          s#[/\\:*?"<>|]#_#g
          s/^[[:space:]]*//
          s/[[:space:]]*$//
        '
    )

    [[ -n "$safe_title" ]] || safe_title="chapter-$number"

    duration=$(
        awk -v start="$start" -v end="$end" '
          BEGIN {
              duration = end - start

              if (duration <= 0)
                  exit 1

              printf "%.6f", duration
          }
        '
    ) || {
        echo "Invalid chapter interval: $start to $end" >&2
        exit 1
    }

    output="${output_dir}/${number}-${safe_title}.mkv"

    echo
    echo "Creating: $output"
    echo "Start: $start"
    echo "End: $end"
    echo "Duration: $duration"

    ffmpeg -hide_banner -y \
      -ss "$start" \
      -i "$input" \
      -t "$duration" \
      -map 0 \
      -map_chapters -1 \
      -map_metadata 0 \
      -c:v libx264 \
      -preset medium \
      -crf 18 \
      -c:a copy \
      -c:s copy \
      "$output"

done < <(
    ffprobe -v error \
      -show_chapters \
      -of json \
      "$input" |
    jq -c '.chapters[]'
)

echo
echo "Created $chapter_number files in $output_dir/"

Before rerunning it, remove the malformed partial output:

rm -f 'daa1-split/02-.mkv'

Then run the script again.

The opening of the run should now display:

Creating: daa1-split/01-intro.mkv
Start: 0.000000
End: 103.402000
Duration: 103.402000

Creating: daa1-split/02-sc1.mkv
Start: 103.402000
End: 3500.801000
Duration: 3397.399000

The original logic was sound; the fragile part was passing three values through tab-delimited shell input. This version passes each complete chapter as JSON and extracts the fields individually.

2026-07-27 13:55:28 EDT

M: more error, here's the whole output: yudasgoat@Lilypad:/media/yudasgoat/Terapile/slow-retrieve-zeros/___clearing-with-split$ ./split-test.sh

Creating: daa1-split/01-intro.mkv Start: 0.000000 End: 103.402000 Duration: 103.402000 Input #0, matroska,webm, from 'daa1chapters.mkv': Metadata: SOFTWARE : VirtualDubMod 1.5.4.1 (build 2178/release) creation_time : 2026-07-27T17:49:27.000000Z ENCODER : Lavf59.16.100 Duration: 02:14:13.36, start: 0.000000, bitrate: 685 kb/s Chapters: Chapter #0:0: start 0.000000, end 103.402000 Metadata: title : intro Chapter #0:1: start 103.402000, end 3500.801000 Metadata: title : sc1 Chapter #0:2: start 3500.801000, end 6585.095000 Metadata: title : sc2 Chapter #0:3: start 6585.095000, end 8053.356000 Metadata: title : sc3 Stream #0:0: Video: h264 (High), yuv420p(progressive), 512x384 [SAR 1:1 DAR 4:3], 29.97 fps, 29.97 tbr, 1k tbn Metadata: ENCODER : Lavc59.18.100 libx264 BPS : 589457 DURATION : 02:14:13.323000000 NUMBER_OF_FRAMES: 241358 NUMBER_OF_BYTES : 593386004 _STATISTICS_WRITING_APP: mkvmerge v82.0 ('I'm The President') 64-bit _STATISTICS_WRITING_DATE_UTC: 2026-07-27 17:49:27 _STATISTICS_TAGS: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES Stream #0:1: Audio: vorbis, 48000 Hz, stereo, fltp Metadata: _STATISTICS_TAGS: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES BPS : 93454 DURATION : 02:14:12.293000000 NUMBER_OF_FRAMES: 428501 NUMBER_OF_BYTES : 94065284 _STATISTICS_WRITING_APP: mkvmerge v82.0 ('I'm The President') 64-bit _STATISTICS_WRITING_DATE_UTC: 2026-07-27 17:49:27 ENCODER : Lavc59.18.100 Stream mapping: Stream #0:0 -> #0:0 (h264 (native) -> h264 (libx264)) Stream #0:1 -> #0:1 (copy) Press [q] to stop, [?] for help [libx264 @ 0x6390c4c5dec0] using SAR=1/1 [libx264 @ 0x6390c4c5dec0] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2 AVX512 [libx264 @ 0x6390c4c5dec0] profile High, level 3.0, 4:2:0, 8-bit [libx264 @ 0x6390c4c5dec0] 264 - core 164 r3108 31e19f9 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=12 lookahead_threads=2 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=25 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=18.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00 Output #0, matroska, to 'daa1-split/01-intro.mkv': Metadata: SOFTWARE : VirtualDubMod 1.5.4.1 (build 2178/release) creation_time : 2026-07-27T17:49:27.000000Z encoder : Lavf60.16.100 Stream #0:0: Video: h264 (H264 / 0x34363248), yuv420p(progressive), 512x384 [SAR 1:1 DAR 4:3], q=2-31, 29.97 fps, 1k tbn Metadata: _STATISTICS_TAGS: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES BPS : 589457 DURATION : 02:14:13.323000000 NUMBER_OF_FRAMES: 241358 NUMBER_OF_BYTES : 593386004 _STATISTICS_WRITING_APP: mkvmerge v82.0 ('I'm The President') 64-bit _STATISTICS_WRITING_DATE_UTC: 2026-07-27 17:49:27 encoder : Lavc60.31.102 libx264 Side data: cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A Stream #0:1: Audio: vorbis (oV[0][0] / 0x566F), 48000 Hz, stereo, fltp Metadata: _STATISTICS_TAGS: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES BPS : 93454 DURATION : 02:14:12.293000000 NUMBER_OF_FRAMES: 428501 NUMBER_OF_BYTES : 94065284 _STATISTICS_WRITING_APP: mkvmerge v82.0 ('I'm The President') 64-bit _STATISTICS_WRITING_DATE_UTC: 2026-07-27 17:49:27 ENCODER : Lavc59.18.100 [out#0/matroska @ 0x6390c4c74fc0] video:9488kB audio:1393kB subtitle:0kB other streams:0kB global headers:4kB muxing overhead: 0.690660% frame= 3098 fps=1370 q=-1.0 Lsize= 10957kB time=00:01:43.39 bitrate= 868.1kbits/s speed=45.7x
[libx264 @ 0x6390c4c5dec0] frame I:40 Avg QP:12.97 size: 18557 [libx264 @ 0x6390c4c5dec0] frame P:1091 Avg QP:17.58 size: 6103 [libx264 @ 0x6390c4c5dec0] frame B:1967 Avg QP:21.43 size: 1177 [libx264 @ 0x6390c4c5dec0] consecutive B-frames: 3.0% 35.9% 3.4% 57.7% [libx264 @ 0x6390c4c5dec0] mb I I16..4: 24.7% 44.9% 30.4% [libx264 @ 0x6390c4c5dec0] mb P I16..4: 1.6% 7.4% 1.5% P16..4: 41.8% 14.6% 6.7% 0.0% 0.0% skip:26.4% [libx264 @ 0x6390c4c5dec0] mb B I16..4: 0.3% 0.5% 0.1% B16..8: 23.2% 2.3% 0.4% direct: 4.6% skip:68.5% L0:37.7% L1:40.3% BI:22.1% [libx264 @ 0x6390c4c5dec0] 8x8 transform intra:63.1% inter:56.7% [libx264 @ 0x6390c4c5dec0] coded y,uvDC,uvAC intra: 60.4% 71.3% 36.3% inter: 13.4% 14.8% 1.1% [libx264 @ 0x6390c4c5dec0] i16 v,h,dc,p: 49% 15% 11% 24% [libx264 @ 0x6390c4c5dec0] i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 20% 16% 22% 6% 8% 8% 8% 6% 7% [libx264 @ 0x6390c4c5dec0] i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 20% 17% 12% 7% 11% 10% 9% 7% 7% [libx264 @ 0x6390c4c5dec0] i8c dc,h,v,p: 46% 20% 22% 11% [libx264 @ 0x6390c4c5dec0] Weighted P-Frames: Y:9.8% UV:7.7% [libx264 @ 0x6390c4c5dec0] ref P L0: 67.3% 9.6% 20.7% 2.4% 0.0% [libx264 @ 0x6390c4c5dec0] ref B L0: 84.2% 14.9% 0.9% [libx264 @ 0x6390c4c5dec0] ref B L1: 99.2% 0.8% [libx264 @ 0x6390c4c5dec0] kb/s:751.89 jq: parse error: Expected value before ',' at line 1, column 3

G: [Non-text content: thoughts; content_type=thoughts]

G: [Non-text content: reasoning_recap; content_type=reasoning_recap]

G: The first chapter encoded successfully. The failure afterward is because FFmpeg consumed the loop’s standard input while it was encoding.

Your loop receives chapter JSON through stdin:

done < <(
    ffprobe ... | jq -c '.chapters[]'
)

But FFmpeg also monitors stdin for interactive commands such as q. During the first 103-second encode, it read part of the remaining JSON stream. The next loop iteration therefore received a mangled fragment, producing:

jq: parse error

Add -nostdin to the FFmpeg command. FFmpeg officially documents this option for cases where stdin is being used for something else. citeturn930100search0turn930100search1

Change:

ffmpeg -hide_banner -y \

to:

ffmpeg -hide_banner -nostdin -y \

So the relevant section becomes:

ffmpeg -hide_banner -nostdin -y \
  -ss "$start" \
  -i "$input" \
  -t "$duration" \
  -map 0 \
  -map_chapters -1 \
  -map_metadata 0 \
  -c:v libx264 \
  -preset medium \
  -crf 18 \
  -c:a copy \
  -c:s copy \
  "$output"

Then rerun:

./split-test.sh

Because you used -y, it will overwrite the already completed 01-intro.mkv and begin again. To avoid repeating that first encode, either delete it and rerun normally, or add this immediately before the FFmpeg command:

if [[ -f "$output" ]]; then
    echo "Skipping existing file: $output"
    continue
fi

The corrected complete loop structure is:

while IFS= read -r chapter_json; do
    chapter_number=$((chapter_number + 1))
    number=$(printf '%02d' "$chapter_number")

    start=$(jq -r '.start_time' <<< "$chapter_json")
    end=$(jq -r '.end_time' <<< "$chapter_json")
    title=$(jq -r '.tags.title // "Untitled"' <<< "$chapter_json")

    safe_title=$(
        printf '%s' "$title" |
        sed '
          s#[/\\:*?"<>|]#_#g
          s/^[[:space:]]*//
          s/[[:space:]]*$//
        '
    )

    duration=$(
        awk -v start="$start" -v end="$end" \
          'BEGIN { printf "%.6f", end - start }'
    )

    output="${output_dir}/${number}-${safe_title}.mkv"

    echo
    echo "Creating: $output"
    echo "Start: $start"
    echo "End: $end"
    echo "Duration: $duration"

    ffmpeg -hide_banner -nostdin -y \
      -ss "$start" \
      -i "$input" \
      -t "$duration" \
      -map 0 \
      -map_chapters -1 \
      -map_metadata 0 \
      -c:v libx264 \
      -preset medium \
      -crf 18 \
      -c:a copy \
      -c:s copy \
      "$output"

done < <(
    ffprobe -v error \
      -show_chapters \
      -of json \
      "$input" |
    jq -c '.chapters[]'
)

So this was not another chapter-format problem. The JSON was correct; FFmpeg simply stole the remaining lines from the loop.

2026-07-27 13:58:54 EDT

M: it looks to be running; proof of concept proven. let me stew on implementation before i ask you to turn this into a generalized script or series of scripts.

G: Good. The pipeline has now demonstrated all the load-bearing parts:

So the remaining work is mostly deciding how automated, configurable, and failure-resistant you want the final version to be.