Build Descript

KINDAreplaces $24/mosaves $288/yrback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A tool that transcribes media with word-level timestamps, presents the transcript as the editing surface, turns text deletions into a cut list, plays back the edit without rendering, and only renders through ffmpeg when you export.

estimated effort multi-daythe files for this build are in the project pack

Transcriptionwhisper.cpp with word-level timestampsRuntimeTypeScript with a desktop shellPreviewPlayback that skips cut ranges rather than renderingRenderffmpeg with a filter graph built from the cut listStorageThe source media untouched, plus a project file

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the build and the desktop shell.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    Why Conversion for transcription and the final render both go through it.

    Get it brew install ffmpeg, your package manager, or a static build. open ↗

    Verify ffmpeg -encoders lists libx264

  3. installfree, but transcription takes real time on your machine

    Why Word-level timestamps are the mechanism. Without them there is no product here.

    Get it Build whisper.cpp and download a model. Confirm the word timestamp output before building anything on it. open ↗

    Verify Output contains per-word start and end times

  4. have readyfree

    Why Sync drift, caption timing and speaker handling only reveal themselves at length and with more than one voice.

    Get it Use your own recordings. One over ten minutes and one conversation.

    Verify Both are available and their durations recorded

  5. have readyfree

    Why Every export writes a new file and every source needs a converted audio copy for transcription.

    Get it Estimate several times your source size and check free space.

    Verify Free space comfortably exceeds the estimate

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- media(id, path, kind, duration_ms, sample_rate, has_video, content_hash)
- words(id, media_id, ordinal, text, start_ms, end_ms, confidence, speaker) · the editing surface
- cuts(id, media_id, start_ms, end_ms, reason) · reason is manual, filler or silence
- speakers(media_id, label, display_name)
- projects(id, name, media_ids_json, created_at)
- A cut is a range, never a modification. The source file is read-only for the life of the project.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
WHISPER_MODEL_PATHrequired./models/ggml-base.en.binThe transcription model. Larger is more accurate and slower.
OUTPUT_DIRrequired./exportsWhere renders are written. Never the source directory.
CUT_PAD_MSrequired40How far a cut extends into surrounding silence, to avoid clicks at the edit.
SILENCE_THRESHOLD_DBrequired-35Level below which audio counts as silence for detection.
SILENCE_MIN_MSrequired600Minimum silence length before it is proposed as a cut. A pause is not a silence to remove.
FILLER_WORDSrequiredum,uh,like,you knowThe filler list, editable because it is language and speaker specific.

The build, in order

  1. Word-level timestamps

    Every word in the transcript knows exactly when it was said.

    1. Mono 16 kHz 16 bit is what whisper.cpp expects. Convert from the source rather than feeding it the original.

      terminal
      ffmpeg -i source.mp4 -ar 16000 -ac 1 -c:a pcm_s16le audio.wav
    2. Explicitly. Segment level is the default and it cannot support this product.

      Files src/transcribe.ts

      terminal
      whisper-cli -m models/ggml-base.en.bin -f audio.wav --output-json-full
    3. Pick a word from the middle of a sentence, play its range, and listen. This is the test that matters.

    done when · tick each as it passes
    watch out
    • Segment-level timestamps are the default in most transcription output and they are useless here, because a segment is a sentence and you need to cut a word. Enable word-level output explicitly and check the result, since a segment timestamp will look plausible in the data and fail the moment you cut.
  2. Deleting text, deleting time

    Selecting words and pressing delete produces a cut range, and the transcript reflows.

    1. Words as spans with their timestamps attached. Selection maps to a time range.

      Files src/editor.ts

    2. A deletion becomes a range. Adjacent or overlapping ranges merge, or the cut list grows without bound.

      Files src/cuts.ts

    3. Extend slightly into the gap between words. Flag cuts with no gap available.

      Files src/cuts.ts

    done when · tick each as it passes
    watch out
    • Cutting exactly at word boundaries clips the natural gap between words and produces an audible click at every edit. Extend each cut slightly into the surrounding silence, and where two words are contiguous with no gap, accept the tighter cut and flag it as likely audible.
  3. Preview without rendering

    You hear the edit immediately, without waiting for anything to encode.

    1. On approaching a cut, seek to its end. Keep a small lookahead so the seek is issued before the boundary.

      Files src/preview.ts

    2. Highlight the current word. This is also how you notice alignment problems.

      Files src/preview.ts

    3. Several short cuts in a row must not produce several audible seeks. Merge the playback plan rather than seeking repeatedly.

      Files src/preview.ts

    done when · tick each as it passes
    watch out
    • Rendering a preview after every edit makes the tool useless, because editing is a hundred small changes and each render is seconds. Skip ranges during playback instead: seeking is instant and the result is close enough to judge the edit.
  4. Filler words and silences

    The obvious cleanup is one action, and you can see exactly what it will remove.

    1. A configurable list. Language and speaker specific, so make the list editable rather than fixed.

      Files src/fillers.ts

    2. Threshold and minimum duration, both settings. A pause is not a silence to remove.

      Files src/silence.ts

      terminal
      ffmpeg -i audio.wav -af silencedetect=n=-35dB:d=0.6 -f null -
    3. Review with counts, bulk accept as one undoable action, and remembered rejections.

      Files src/proposals.ts

    done when · tick each as it passes
    watch out
    • Removing every detected filler makes speech sound unnaturally clipped, because some hesitations carry meaning and rhythm. Propose rather than apply, show the count, and let a bulk accept be a deliberate action rather than a default.
  5. Rendering the export

    The exported file matches the preview, including the video.

    1. Trim and concatenate kept ranges in one ffmpeg invocation for both streams.

      Files src/render.ts

    2. Twenty cuts in a ten minute file, checked at the end. Drift accumulates and only shows at length.

    3. Output alongside, checksum the source before and after as a test.

      Files src/render.test.ts

    done when · tick each as it passes
    watch out
    • Trimming video at a non-keyframe produces either a frozen frame or a re-encode of the whole file, and doing this per cut with many cuts is very slow. Concatenate trimmed segments through a filter graph in one pass rather than cutting and joining files, and accept that a full re-encode is the honest cost of frame-accurate cuts.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more audio & video.