Vibecode diclip
track this build4 phases, 12 steps, beginner friendly0%The core loop is genuinely one-shottable: transcribe, rank the strongest moments, cut vertical clips with burned captions. The gaps are execution and infra. Face-aware reframing with seat tracking, a full in-browser timeline editor, and a container-scale render pipeline are not a one-session build. A competent agent can produce a usable personal clipper quickly, but matching diclip's reframe quality and editing depth is a multi-day project.
You are building a lean indie version of diclip. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # diclip · indie build A personal video clipper whose hard problems are producing word-level timings accurate enough to cut on, making the cut land exactly where the transcript said it would, and rendering captions and framing without pretending to do the face tracking that the paid product actually does. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small local web interface, bound to the loopback address | the media is large and local and nothing here wants to be uploaded | | Transcription | A local speech model producing word-level timings | the cut points come from the transcript, so the timings are the foundation | | Rendering | FFmpeg, invoked with explicit arguments you understand | every video tool is calling this underneath and the argument order changes the result | | Projects | JSON on disk beside the media, with renders in their own directory | a project should survive the application being deleted | | Ranking | A rented model if a key is present, a local heuristic if not | the ranking is a convenience and the tool has to work without it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the local application and the render queue. - Get it: python.org, or your version manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg with a recent build** · free - Why: Every cut, caption burn and framing operation is an FFmpeg invocation, and the seeking behaviour you depend on is documented there. - Get it: Your package manager, or a static build from the project. - Verify: ffmpeg -version prints a version and the probe tool is on the path - [ ] **A local speech model with word-level timestamps** · free, but the model download is large - Why: Word timings are what the cuts are computed from, so a model that only returns sentences is not usable here. - Get it: Install the library and download a model size your machine can run. - Verify: A one minute fixture returns words with start and end times - [ ] **Working disk space for sources, renders and intermediates** · free if you have the space - Why: A long recording plus its renders and intermediates is the constraint that ends most sessions, and it fails in a confusing way. - Get it: Point the project directory at a volume with room to spare and set the threshold below it. - Verify: The application reports free space and refuses below the threshold - [ ] **An API key for a text model** (optional) · pay per token; a long transcript is a few pence - Why: Only the ranking step uses it, and the build is designed to be useful without it. - Get it: Any provider, with a spend limit set in their console. - Verify: A ranked candidate list arrives with the source recorded as model ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Face-aware reframing and speaker tracking. - A timeline editor with tracks and effects. - Downloading source video from platforms. - Cloud rendering. - face-aware vertical reframing with seat tracking - full in-browser timeline editor with scenes, tracks, and effects - container-scale render pipeline and yt-dlp import - credit-based capacity model and priority processing - ready-to-post hooks and confidence scoring polished for clips If one of those is essential to you, that is the reason to keep paying for diclip, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · diclip The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local clipper that turns a long recording into vertical clips and never guesses where the cut actually landed to replace diclip. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Python with a small local web interface, bound to the loopback address · the media is large and local and nothing here wants to be uploaded - Transcription: A local speech model producing word-level timings · the cut points come from the transcript, so the timings are the foundation - Rendering: FFmpeg, invoked with explicit arguments you understand · every video tool is calling this underneath and the argument order changes the result - Projects: JSON on disk beside the media, with renders in their own directory · a project should survive the application being deleted - Ranking: A rented model if a key is present, a local heuristic if not · the ranking is a convenience and the tool has to work without it ### Data model (create this before Phase 1) - projects(id, source_path, source_sha256, duration_ms, imported_at, transcript_state, render_state) - words(project_id, ordinal, text, start_ms, end_ms, speaker, confidence) · the unit everything else is built from - segments(project_id, ordinal, start_ms, end_ms, speaker, text) · sentences assembled from words, never from the model's own punctuation guesses alone - candidates(id, project_id, start_ms, end_ms, hook_text, score, reason, source, state) · source is model or heuristic, state is pending, accepted or rejected - renders(id, candidate_id, aspect, out_path, started_at, finished_at, ffmpeg_args, actual_start_ms, actual_duration_ms, failure) - captions(render_id, ordinal, start_ms, end_ms, text, speaker) · grouped for reading, not one word per frame - actual_start_ms is measured from the produced file, not copied from the request. The gap between requested and actual is the number this build exists to keep at zero. ### Phase 1 · Import and transcribe, without touching the source Build: Import by path, hash the source, probe it for duration, streams and frame rate. Transcribe to word level timings and store every word with its start, end and confidence. Assemble sentences from word timings. Show progress with an honest estimate, because this step takes real time on a processor. Trap: Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. Done when: Words with start and end timings are stored for the whole file and sentences are derived from them, checked against a fixture with known speech. The source file's hash before and after import is identical, asserted in the suite. Re-importing the same path with an unchanged hash reuses the existing transcript rather than transcribing again. A file the probe cannot read fails with the reason and does not create a half-built project. Do not build yet: No ranking, no cutting, no captions, no rendering. ### Phase 2 · Candidates with a reason you can argue with Build: A heuristic ranker over the transcript using speaking pace, question marks, pause lengths and keyword density, producing candidates between a configured minimum and maximum length. If a model key is present, ask a model for candidates in the same shape and record which source produced each one. Snap every candidate to word boundaries from the word table. Trap: Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. Done when: With no model key configured the heuristic produces candidates and the interface says which ranker ran, tested with the key absent. Model-produced candidates are located by text span in the word table and their timings come from words, never from the model, asserted in the suite. Every candidate has a hook line, a score and a one-sentence reason, and a candidate missing any of them is not displayed. Candidate boundaries always align to word starts and ends, tested. Do not build yet: No cutting, no captions, no rendering, no export. ### Phase 3 · Cuts that land where you asked Build: Render with FFmpeg and read its own documentation on seeking before choosing argument order. As an input option before the input, a seek goes to the nearest seek point before the position; when transcoding with accurate seeking the surplus is decoded and discarded, but during stream copy or with accurate seeking disabled it is kept. Measure the produced file and compare against the request. Trap: Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. Done when: Every render probes the produced file and stores its actual start and duration, and a difference beyond one frame fails the render, tested against a fixture with a known keyframe interval. The exact FFmpeg arguments used are stored with the render and shown in the interface. A stream-copy mode, if offered, is labelled as keyframe aligned and the interface shows the boundary it will actually use. Rendering never writes over the source or an existing render. Do not build yet: No captions, no framing modes, no batch, no export. ### Phase 4 · Captions and framing, without pretending to track faces Build: Group words into caption lines by reading speed and pause length rather than fixed word counts, and burn them with a per-speaker style. Offer centre crop and blurred pad for vertical output and label each one plainly. Show a preview frame at the chosen framing before rendering the whole clip. Trap: Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. Done when: Caption lines break at pauses and respect a maximum character length, checked against a fast-speech fixture. Caption timings come from word timings and no line displays before its first word is spoken, tested. Centre crop and blurred pad are named in the interface and a preview frame is rendered before the clip. No claim of face or speaker tracking appears anywhere in the interface or the README. Do not build yet: No batch rendering, no exports, no retention. ### Phase 5 · Disk, batches and failures you can read Build: A render queue processing accepted candidates one at a time, resumable after a crash. A retention policy for renders and intermediate files with a configured ceiling, refusing to start a render when free space is below a threshold. Export a project as its JSON plus its renders. Surface the FFmpeg failure reason in the interface without exposing full paths. Trap: Letting a batch write intermediates until the disk is full produces a failure that looks like a video codec problem and takes an evening to diagnose, and by then the partial files are still there. Check free space before each render, refuse with a clear message, and delete intermediates in a finally block rather than on the success path. Done when: A render refuses to start when free space is below the configured threshold and says so, tested by pointing the output directory at a constrained volume. Intermediates are removed whether the render succeeded or failed, tested by forcing a failure mid-render. A batch killed halfway resumes and skips completed renders, tested. Every failure shows a readable reason in the interface and the full command in the project log, not in the interface. Do not build yet: Nothing after this. ### Out of scope (and why) - Face-aware reframing with speaker tracking, which needs a detection model, temporal smoothing and months of tuning, and is a genuine reason the subscription exists. - A timeline editor with tracks and effects, because building one that is worse than the free desktop editors you already have is not progress. - Downloading source video from platforms, which is a terms question rather than a technical one and is not needed when you own the recording. - Cloud rendering, since the cost model of a render farm is the actual product on the other side and there is nothing to learn from half of it. ### README must contain - That transcription and rendering are slow on a processor, with a measured figure from your own machine - That cut accuracy is measured on every render and what the one-frame tolerance means - That vertical framing is centre crop or blurred pad, with no face or speaker tracking of any kind - Where projects, renders and intermediates live, and what the disk space threshold does - That the ranking step is optional and the heuristic runs when no model key is present ===== AGENTS.md ===== # Agent instructions · diclip indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with a small local web interface, bound to the loopback address, A local speech model producing word-level timings, FFmpeg, invoked with explicit arguments you understand, JSON on disk beside the media, with renders in their own directory, A rented model if a key is present, a local heuristic if not. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. - Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. - Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. - Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. ===== BUILD_PLAN.md ===== # Build plan · diclip A personal video clipper whose hard problems are producing word-level timings accurate enough to cut on, making the cut land exactly where the transcript said it would, and rendering captions and framing without pretending to do the face tracking that the paid product actually does. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Import and transcribe, without touching the source A long file becomes words with timings, and the original is never modified or moved. ### Steps 1. Store word timings, derive sentences from them A sentence boundary is the wrong resolution for a cut point. Files: `src/transcribe/words.py`, `tests/test_segments.py` 2. Hash the source and never write to it The recording is the one file you cannot regenerate. Files: `src/project/import.py` 3. Probe before you transcribe An unreadable file should fail in a second, not in an hour. Files: `src/media/probe.py` ### Done when - [ ] Word-level timings cover the whole file - [ ] The source hash is unchanged after import - [ ] A repeat import reuses the transcript - [ ] An unreadable file fails cleanly ### Watch out - Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. ## Phase 2 · Candidates with a reason you can argue with Every proposed clip carries a hook line, a score and a stated reason, and the tool works with no model key. ### Steps 1. Make the heuristic the default, not the fallback The tool has to work before anyone pays for a token. Files: `src/rank/heuristic.py`, `tests/test_heuristic.py` 2. Locate model spans in the word table The model reads text; the word table knows the time. Files: `src/rank/locate.py` 3. Snap every boundary to a word A cut inside a word is audible and unfixable later. Files: `src/rank/snap.py` ### Done when - [ ] Candidates appear with no model key present - [ ] Model timings are derived from the word table - [ ] Every candidate has hook, score and reason - [ ] Boundaries align to word edges ### Watch out - Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. ## Phase 3 · Cuts that land where you asked A rendered clip starts and ends within a frame of the requested times, and the build proves it. ### Steps 1. Read the seeking documentation before writing the command Argument order is the difference between correct and nearly. Files: `docs/ffmpeg-seeking.md`, `src/render/cut.py` 2. Probe the output and compare with the request The only trustworthy timing is the one measured afterwards. Files: `src/render/verify.py`, `tests/test_cut_accuracy.py` 3. Store the arguments with the render Every strange clip is explained by its own command line. Files: `src/render/record.py` ### Done when - [ ] Actual start and duration are measured per render - [ ] A drift beyond one frame fails the render - [ ] The FFmpeg arguments are stored and visible - [ ] Nothing overwrites the source or a prior render ### Watch out - Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. ## Phase 4 · Captions and framing, without pretending to track faces Burned captions are readable and correctly timed, and the vertical framing is honest about what it is doing. ### Steps 1. Group captions by pause and reading speed A fixed word count breaks lines in the wrong places. Files: `src/captions/group.py`, `tests/test_caption_grouping.py` 2. Preview the framing on one frame first Discovering the crop after a five minute render is the annoyance. Files: `src/render/preview.py` 3. Say centre crop, not smart reframing The paid product does the other thing and this one does not. Files: `src/web/templates/render.html` ### Done when - [ ] Caption lines break at pauses within a length cap - [ ] No caption appears before its first word - [ ] A preview frame precedes every render - [ ] No tracking claim appears in the interface ### Watch out - Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. ## Not in this build - Face-aware reframing and speaker tracking. - A timeline editor with tracks and effects. - Downloading source video from platforms. - Cloud rendering. ## After v1, if you want it - A per-project accuracy report of requested against measured cut points, which is the only quality number this build has - Caption style presets stored per project, so a series of clips looks like a series - A second pass that trims leading silence using the word table, which costs nothing and improves most clips ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project JSON and transcripts. The part worth backing up. PROJECT_DIR=~/clips/projects # Required. Finished clips. Regenerable, so back it up last. RENDER_DIR=~/clips/exports # Required. Renders refuse to start below this. It is the cheapest error message in the build. MIN_FREE_GB=10 # Required. Lower bound for a candidate. CLIP_MIN_SECONDS=30 # Required. Upper bound for a candidate. CLIP_MAX_SECONDS=90 # Optional · secret. Optional. Absent means the heuristic ranker runs. RANKER_API_KEY=sk-...redacted # Required. A render whose measured start drifts beyond this fails rather than shipping. CUT_TOLERANCE_FRAMES=1
You are building a lean indie version of diclip. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # diclip · indie build A personal video clipper whose hard problems are producing word-level timings accurate enough to cut on, making the cut land exactly where the transcript said it would, and rendering captions and framing without pretending to do the face tracking that the paid product actually does. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small local web interface, bound to the loopback address | the media is large and local and nothing here wants to be uploaded | | Transcription | A local speech model producing word-level timings | the cut points come from the transcript, so the timings are the foundation | | Rendering | FFmpeg, invoked with explicit arguments you understand | every video tool is calling this underneath and the argument order changes the result | | Projects | JSON on disk beside the media, with renders in their own directory | a project should survive the application being deleted | | Ranking | A rented model if a key is present, a local heuristic if not | the ranking is a convenience and the tool has to work without it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the local application and the render queue. - Get it: python.org, or your version manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg with a recent build** · free - Why: Every cut, caption burn and framing operation is an FFmpeg invocation, and the seeking behaviour you depend on is documented there. - Get it: Your package manager, or a static build from the project. - Verify: ffmpeg -version prints a version and the probe tool is on the path - [ ] **A local speech model with word-level timestamps** · free, but the model download is large - Why: Word timings are what the cuts are computed from, so a model that only returns sentences is not usable here. - Get it: Install the library and download a model size your machine can run. - Verify: A one minute fixture returns words with start and end times - [ ] **Working disk space for sources, renders and intermediates** · free if you have the space - Why: A long recording plus its renders and intermediates is the constraint that ends most sessions, and it fails in a confusing way. - Get it: Point the project directory at a volume with room to spare and set the threshold below it. - Verify: The application reports free space and refuses below the threshold - [ ] **An API key for a text model** (optional) · pay per token; a long transcript is a few pence - Why: Only the ranking step uses it, and the build is designed to be useful without it. - Get it: Any provider, with a spend limit set in their console. - Verify: A ranked candidate list arrives with the source recorded as model ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Face-aware reframing and speaker tracking. - A timeline editor with tracks and effects. - Downloading source video from platforms. - Cloud rendering. - face-aware vertical reframing with seat tracking - full in-browser timeline editor with scenes, tracks, and effects - container-scale render pipeline and yt-dlp import - credit-based capacity model and priority processing - ready-to-post hooks and confidence scoring polished for clips If one of those is essential to you, that is the reason to keep paying for diclip, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · diclip The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local clipper that turns a long recording into vertical clips and never guesses where the cut actually landed to replace diclip. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Python with a small local web interface, bound to the loopback address · the media is large and local and nothing here wants to be uploaded - Transcription: A local speech model producing word-level timings · the cut points come from the transcript, so the timings are the foundation - Rendering: FFmpeg, invoked with explicit arguments you understand · every video tool is calling this underneath and the argument order changes the result - Projects: JSON on disk beside the media, with renders in their own directory · a project should survive the application being deleted - Ranking: A rented model if a key is present, a local heuristic if not · the ranking is a convenience and the tool has to work without it ### Data model (create this before Phase 1) - projects(id, source_path, source_sha256, duration_ms, imported_at, transcript_state, render_state) - words(project_id, ordinal, text, start_ms, end_ms, speaker, confidence) · the unit everything else is built from - segments(project_id, ordinal, start_ms, end_ms, speaker, text) · sentences assembled from words, never from the model's own punctuation guesses alone - candidates(id, project_id, start_ms, end_ms, hook_text, score, reason, source, state) · source is model or heuristic, state is pending, accepted or rejected - renders(id, candidate_id, aspect, out_path, started_at, finished_at, ffmpeg_args, actual_start_ms, actual_duration_ms, failure) - captions(render_id, ordinal, start_ms, end_ms, text, speaker) · grouped for reading, not one word per frame - actual_start_ms is measured from the produced file, not copied from the request. The gap between requested and actual is the number this build exists to keep at zero. ### Phase 1 · Import and transcribe, without touching the source Build: Import by path, hash the source, probe it for duration, streams and frame rate. Transcribe to word level timings and store every word with its start, end and confidence. Assemble sentences from word timings. Show progress with an honest estimate, because this step takes real time on a processor. Trap: Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. Done when: Words with start and end timings are stored for the whole file and sentences are derived from them, checked against a fixture with known speech. The source file's hash before and after import is identical, asserted in the suite. Re-importing the same path with an unchanged hash reuses the existing transcript rather than transcribing again. A file the probe cannot read fails with the reason and does not create a half-built project. Do not build yet: No ranking, no cutting, no captions, no rendering. ### Phase 2 · Candidates with a reason you can argue with Build: A heuristic ranker over the transcript using speaking pace, question marks, pause lengths and keyword density, producing candidates between a configured minimum and maximum length. If a model key is present, ask a model for candidates in the same shape and record which source produced each one. Snap every candidate to word boundaries from the word table. Trap: Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. Done when: With no model key configured the heuristic produces candidates and the interface says which ranker ran, tested with the key absent. Model-produced candidates are located by text span in the word table and their timings come from words, never from the model, asserted in the suite. Every candidate has a hook line, a score and a one-sentence reason, and a candidate missing any of them is not displayed. Candidate boundaries always align to word starts and ends, tested. Do not build yet: No cutting, no captions, no rendering, no export. ### Phase 3 · Cuts that land where you asked Build: Render with FFmpeg and read its own documentation on seeking before choosing argument order. As an input option before the input, a seek goes to the nearest seek point before the position; when transcoding with accurate seeking the surplus is decoded and discarded, but during stream copy or with accurate seeking disabled it is kept. Measure the produced file and compare against the request. Trap: Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. Done when: Every render probes the produced file and stores its actual start and duration, and a difference beyond one frame fails the render, tested against a fixture with a known keyframe interval. The exact FFmpeg arguments used are stored with the render and shown in the interface. A stream-copy mode, if offered, is labelled as keyframe aligned and the interface shows the boundary it will actually use. Rendering never writes over the source or an existing render. Do not build yet: No captions, no framing modes, no batch, no export. ### Phase 4 · Captions and framing, without pretending to track faces Build: Group words into caption lines by reading speed and pause length rather than fixed word counts, and burn them with a per-speaker style. Offer centre crop and blurred pad for vertical output and label each one plainly. Show a preview frame at the chosen framing before rendering the whole clip. Trap: Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. Done when: Caption lines break at pauses and respect a maximum character length, checked against a fast-speech fixture. Caption timings come from word timings and no line displays before its first word is spoken, tested. Centre crop and blurred pad are named in the interface and a preview frame is rendered before the clip. No claim of face or speaker tracking appears anywhere in the interface or the README. Do not build yet: No batch rendering, no exports, no retention. ### Phase 5 · Disk, batches and failures you can read Build: A render queue processing accepted candidates one at a time, resumable after a crash. A retention policy for renders and intermediate files with a configured ceiling, refusing to start a render when free space is below a threshold. Export a project as its JSON plus its renders. Surface the FFmpeg failure reason in the interface without exposing full paths. Trap: Letting a batch write intermediates until the disk is full produces a failure that looks like a video codec problem and takes an evening to diagnose, and by then the partial files are still there. Check free space before each render, refuse with a clear message, and delete intermediates in a finally block rather than on the success path. Done when: A render refuses to start when free space is below the configured threshold and says so, tested by pointing the output directory at a constrained volume. Intermediates are removed whether the render succeeded or failed, tested by forcing a failure mid-render. A batch killed halfway resumes and skips completed renders, tested. Every failure shows a readable reason in the interface and the full command in the project log, not in the interface. Do not build yet: Nothing after this. ### Out of scope (and why) - Face-aware reframing with speaker tracking, which needs a detection model, temporal smoothing and months of tuning, and is a genuine reason the subscription exists. - A timeline editor with tracks and effects, because building one that is worse than the free desktop editors you already have is not progress. - Downloading source video from platforms, which is a terms question rather than a technical one and is not needed when you own the recording. - Cloud rendering, since the cost model of a render farm is the actual product on the other side and there is nothing to learn from half of it. ### README must contain - That transcription and rendering are slow on a processor, with a measured figure from your own machine - That cut accuracy is measured on every render and what the one-frame tolerance means - That vertical framing is centre crop or blurred pad, with no face or speaker tracking of any kind - Where projects, renders and intermediates live, and what the disk space threshold does - That the ranking step is optional and the heuristic runs when no model key is present ===== AGENTS.md ===== # Agent instructions · diclip indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with a small local web interface, bound to the loopback address, A local speech model producing word-level timings, FFmpeg, invoked with explicit arguments you understand, JSON on disk beside the media, with renders in their own directory, A rented model if a key is present, a local heuristic if not. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. - Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. - Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. - Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. ===== BUILD_PLAN.md ===== # Build plan · diclip A personal video clipper whose hard problems are producing word-level timings accurate enough to cut on, making the cut land exactly where the transcript said it would, and rendering captions and framing without pretending to do the face tracking that the paid product actually does. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Import and transcribe, without touching the source A long file becomes words with timings, and the original is never modified or moved. ### Steps 1. Store word timings, derive sentences from them A sentence boundary is the wrong resolution for a cut point. Files: `src/transcribe/words.py`, `tests/test_segments.py` 2. Hash the source and never write to it The recording is the one file you cannot regenerate. Files: `src/project/import.py` 3. Probe before you transcribe An unreadable file should fail in a second, not in an hour. Files: `src/media/probe.py` ### Done when - [ ] Word-level timings cover the whole file - [ ] The source hash is unchanged after import - [ ] A repeat import reuses the transcript - [ ] An unreadable file fails cleanly ### Watch out - Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. ## Phase 2 · Candidates with a reason you can argue with Every proposed clip carries a hook line, a score and a stated reason, and the tool works with no model key. ### Steps 1. Make the heuristic the default, not the fallback The tool has to work before anyone pays for a token. Files: `src/rank/heuristic.py`, `tests/test_heuristic.py` 2. Locate model spans in the word table The model reads text; the word table knows the time. Files: `src/rank/locate.py` 3. Snap every boundary to a word A cut inside a word is audible and unfixable later. Files: `src/rank/snap.py` ### Done when - [ ] Candidates appear with no model key present - [ ] Model timings are derived from the word table - [ ] Every candidate has hook, score and reason - [ ] Boundaries align to word edges ### Watch out - Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. ## Phase 3 · Cuts that land where you asked A rendered clip starts and ends within a frame of the requested times, and the build proves it. ### Steps 1. Read the seeking documentation before writing the command Argument order is the difference between correct and nearly. Files: `docs/ffmpeg-seeking.md`, `src/render/cut.py` 2. Probe the output and compare with the request The only trustworthy timing is the one measured afterwards. Files: `src/render/verify.py`, `tests/test_cut_accuracy.py` 3. Store the arguments with the render Every strange clip is explained by its own command line. Files: `src/render/record.py` ### Done when - [ ] Actual start and duration are measured per render - [ ] A drift beyond one frame fails the render - [ ] The FFmpeg arguments are stored and visible - [ ] Nothing overwrites the source or a prior render ### Watch out - Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. ## Phase 4 · Captions and framing, without pretending to track faces Burned captions are readable and correctly timed, and the vertical framing is honest about what it is doing. ### Steps 1. Group captions by pause and reading speed A fixed word count breaks lines in the wrong places. Files: `src/captions/group.py`, `tests/test_caption_grouping.py` 2. Preview the framing on one frame first Discovering the crop after a five minute render is the annoyance. Files: `src/render/preview.py` 3. Say centre crop, not smart reframing The paid product does the other thing and this one does not. Files: `src/web/templates/render.html` ### Done when - [ ] Caption lines break at pauses within a length cap - [ ] No caption appears before its first word - [ ] A preview frame precedes every render - [ ] No tracking claim appears in the interface ### Watch out - Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. ## Not in this build - Face-aware reframing and speaker tracking. - A timeline editor with tracks and effects. - Downloading source video from platforms. - Cloud rendering. ## After v1, if you want it - A per-project accuracy report of requested against measured cut points, which is the only quality number this build has - Caption style presets stored per project, so a series of clips looks like a series - A second pass that trims leading silence using the word table, which costs nothing and improves most clips ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project JSON and transcripts. The part worth backing up. PROJECT_DIR=~/clips/projects # Required. Finished clips. Regenerable, so back it up last. RENDER_DIR=~/clips/exports # Required. Renders refuse to start below this. It is the cheapest error message in the build. MIN_FREE_GB=10 # Required. Lower bound for a candidate. CLIP_MIN_SECONDS=30 # Required. Upper bound for a candidate. CLIP_MAX_SECONDS=90 # Optional · secret. Optional. Absent means the heuristic ranker runs. RANKER_API_KEY=sk-...redacted # Required. A render whose measured start drifts beyond this fails rather than shipping. CUT_TOLERANCE_FRAMES=1
You are building a production product version of diclip. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # diclip · product brief ## Problem The core loop is genuinely one-shottable: transcribe, rank the strongest moments, cut vertical clips with burned captions. The gaps are execution and infra. Face-aware reframing with seat tracking, a full in-browser timeline editor, and a container-scale render pipeline are not a one-session build. A competent agent can produce a usable personal clipper quickly, but matching diclip's reframe quality and editing depth is a multi-day project. ## Product outcome A clipper that cuts where the transcript said, measures every render to prove it, burns captions that break at pauses, and declines to claim the reframing intelligence it does not have. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - FFmpeg - faster-whisper - LLM API key for moment ranking (optional, falls back to heuristics) - desktop with enough storage for source media and renders ## Explicit non-goals for v1 - Face-aware reframing and speaker tracking. - A timeline editor with tracks and effects. - Downloading source video from platforms. - Cloud rendering. - face-aware vertical reframing with seat tracking - full in-browser timeline editor with scenes, tracks, and effects - container-scale render pipeline and yt-dlp import - credit-based capacity model and priority processing - ready-to-post hooks and confidence scoring polished for clips ## Success criteria - Every render is confirmed to measure its actual start and duration - A drift beyond the tolerance is confirmed to fail the render - Candidates are confirmed to appear with no model key present - Caption lines are confirmed to break at pauses within a length cap - A low-space render is confirmed refused with a readable message ===== BRIEF.md ===== # Build brief · diclip The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local clipper that turns a long recording into vertical clips and never guesses where the cut actually landed to replace diclip. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Python with a small local web interface, bound to the loopback address · the media is large and local and nothing here wants to be uploaded - Transcription: A local speech model producing word-level timings · the cut points come from the transcript, so the timings are the foundation - Rendering: FFmpeg, invoked with explicit arguments you understand · every video tool is calling this underneath and the argument order changes the result - Projects: JSON on disk beside the media, with renders in their own directory · a project should survive the application being deleted - Ranking: A rented model if a key is present, a local heuristic if not · the ranking is a convenience and the tool has to work without it ### Data model (create this before Phase 1) - projects(id, source_path, source_sha256, duration_ms, imported_at, transcript_state, render_state) - words(project_id, ordinal, text, start_ms, end_ms, speaker, confidence) · the unit everything else is built from - segments(project_id, ordinal, start_ms, end_ms, speaker, text) · sentences assembled from words, never from the model's own punctuation guesses alone - candidates(id, project_id, start_ms, end_ms, hook_text, score, reason, source, state) · source is model or heuristic, state is pending, accepted or rejected - renders(id, candidate_id, aspect, out_path, started_at, finished_at, ffmpeg_args, actual_start_ms, actual_duration_ms, failure) - captions(render_id, ordinal, start_ms, end_ms, text, speaker) · grouped for reading, not one word per frame - actual_start_ms is measured from the produced file, not copied from the request. The gap between requested and actual is the number this build exists to keep at zero. ### Phase 1 · Import and transcribe, without touching the source Build: Import by path, hash the source, probe it for duration, streams and frame rate. Transcribe to word level timings and store every word with its start, end and confidence. Assemble sentences from word timings. Show progress with an honest estimate, because this step takes real time on a processor. Trap: Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. Done when: Words with start and end timings are stored for the whole file and sentences are derived from them, checked against a fixture with known speech. The source file's hash before and after import is identical, asserted in the suite. Re-importing the same path with an unchanged hash reuses the existing transcript rather than transcribing again. A file the probe cannot read fails with the reason and does not create a half-built project. Do not build yet: No ranking, no cutting, no captions, no rendering. ### Phase 2 · Candidates with a reason you can argue with Build: A heuristic ranker over the transcript using speaking pace, question marks, pause lengths and keyword density, producing candidates between a configured minimum and maximum length. If a model key is present, ask a model for candidates in the same shape and record which source produced each one. Snap every candidate to word boundaries from the word table. Trap: Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. Done when: With no model key configured the heuristic produces candidates and the interface says which ranker ran, tested with the key absent. Model-produced candidates are located by text span in the word table and their timings come from words, never from the model, asserted in the suite. Every candidate has a hook line, a score and a one-sentence reason, and a candidate missing any of them is not displayed. Candidate boundaries always align to word starts and ends, tested. Do not build yet: No cutting, no captions, no rendering, no export. ### Phase 3 · Cuts that land where you asked Build: Render with FFmpeg and read its own documentation on seeking before choosing argument order. As an input option before the input, a seek goes to the nearest seek point before the position; when transcoding with accurate seeking the surplus is decoded and discarded, but during stream copy or with accurate seeking disabled it is kept. Measure the produced file and compare against the request. Trap: Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. Done when: Every render probes the produced file and stores its actual start and duration, and a difference beyond one frame fails the render, tested against a fixture with a known keyframe interval. The exact FFmpeg arguments used are stored with the render and shown in the interface. A stream-copy mode, if offered, is labelled as keyframe aligned and the interface shows the boundary it will actually use. Rendering never writes over the source or an existing render. Do not build yet: No captions, no framing modes, no batch, no export. ### Phase 4 · Captions and framing, without pretending to track faces Build: Group words into caption lines by reading speed and pause length rather than fixed word counts, and burn them with a per-speaker style. Offer centre crop and blurred pad for vertical output and label each one plainly. Show a preview frame at the chosen framing before rendering the whole clip. Trap: Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. Done when: Caption lines break at pauses and respect a maximum character length, checked against a fast-speech fixture. Caption timings come from word timings and no line displays before its first word is spoken, tested. Centre crop and blurred pad are named in the interface and a preview frame is rendered before the clip. No claim of face or speaker tracking appears anywhere in the interface or the README. Do not build yet: No batch rendering, no exports, no retention. ### Phase 5 · Disk, batches and failures you can read Build: A render queue processing accepted candidates one at a time, resumable after a crash. A retention policy for renders and intermediate files with a configured ceiling, refusing to start a render when free space is below a threshold. Export a project as its JSON plus its renders. Surface the FFmpeg failure reason in the interface without exposing full paths. Trap: Letting a batch write intermediates until the disk is full produces a failure that looks like a video codec problem and takes an evening to diagnose, and by then the partial files are still there. Check free space before each render, refuse with a clear message, and delete intermediates in a finally block rather than on the success path. Done when: A render refuses to start when free space is below the configured threshold and says so, tested by pointing the output directory at a constrained volume. Intermediates are removed whether the render succeeded or failed, tested by forcing a failure mid-render. A batch killed halfway resumes and skips completed renders, tested. Every failure shows a readable reason in the interface and the full command in the project log, not in the interface. Do not build yet: Nothing after this. ### Out of scope (and why) - Face-aware reframing with speaker tracking, which needs a detection model, temporal smoothing and months of tuning, and is a genuine reason the subscription exists. - A timeline editor with tracks and effects, because building one that is worse than the free desktop editors you already have is not progress. - Downloading source video from platforms, which is a terms question rather than a technical one and is not needed when you own the recording. - Cloud rendering, since the cost model of a render farm is the actual product on the other side and there is nothing to learn from half of it. ### README must contain - That transcription and rendering are slow on a processor, with a measured figure from your own machine - That cut accuracy is measured on every render and what the one-frame tolerance means - That vertical framing is centre crop or blurred pad, with no face or speaker tracking of any kind - Where projects, renders and intermediates live, and what the disk space threshold does - That the ranking step is optional and the heuristic runs when no model key is present ===== ARCHITECTURE.md ===== # Architecture · diclip ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small local web interface, bound to the loopback address | the media is large and local and nothing here wants to be uploaded | | Transcription | A local speech model producing word-level timings | the cut points come from the transcript, so the timings are the foundation | | Rendering | FFmpeg, invoked with explicit arguments you understand | every video tool is calling this underneath and the argument order changes the result | | Projects | JSON on disk beside the media, with renders in their own directory | a project should survive the application being deleted | | Ranking | A rented model if a key is present, a local heuristic if not | the ranking is a convenience and the tool has to work without it | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Hashing, probing and leaving the source alone. | Any importer, provided the source is never written to. | | Transcript | Word timings and derived sentences. | Any speech model, provided it returns word-level timestamps. | | Rank | Heuristic and optional model candidates. | Any ranker, provided timings still come from the word table. | | Render | FFmpeg invocation, measurement and stored arguments. | Any wrapper, provided output is probed and compared with the request. | | Queue | Batches, disk checks and cleanup. | Any queue, provided intermediates are removed on failure too. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PROJECT_DIR` · required · Project JSON and transcripts. The part worth backing up. - `RENDER_DIR` · required · Finished clips. Regenerable, so back it up last. - `MIN_FREE_GB` · required · Renders refuse to start below this. It is the cheapest error message in the build. - `CLIP_MIN_SECONDS` · required · Lower bound for a candidate. - `CLIP_MAX_SECONDS` · required · Upper bound for a candidate. - `RANKER_API_KEY` · optional, secret · Optional. Absent means the heuristic ranker runs. - `CUT_TOLERANCE_FRAMES` · required · A render whose measured start drifts beyond this fails rather than shipping. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · diclip product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python with a small local web interface, bound to the loopback address, A local speech model producing word-level timings, FFmpeg, invoked with explicit arguments you understand, JSON on disk beside the media, with renders in their own directory, A rented model if a key is present, a local heuristic if not. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. - Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. - Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. - Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. - Letting a batch write intermediates until the disk is full produces a failure that looks like a video codec problem and takes an evening to diagnose, and by then the partial files are still there. Check free space before each render, refuse with a clear message, and delete intermediates in a finally block rather than on the success path. ===== MILESTONES.md ===== # Delivery milestones · diclip Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Import and transcribe, without touching the source A long file becomes words with timings, and the original is never modified or moved. ### Steps 1. Store word timings, derive sentences from them A sentence boundary is the wrong resolution for a cut point. Files: `src/transcribe/words.py`, `tests/test_segments.py` 2. Hash the source and never write to it The recording is the one file you cannot regenerate. Files: `src/project/import.py` 3. Probe before you transcribe An unreadable file should fail in a second, not in an hour. Files: `src/media/probe.py` ### Done when - [ ] Word-level timings cover the whole file - [ ] The source hash is unchanged after import - [ ] A repeat import reuses the transcript - [ ] An unreadable file fails cleanly ### Watch out - Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. ## M2 · Candidates with a reason you can argue with Every proposed clip carries a hook line, a score and a stated reason, and the tool works with no model key. ### Steps 1. Make the heuristic the default, not the fallback The tool has to work before anyone pays for a token. Files: `src/rank/heuristic.py`, `tests/test_heuristic.py` 2. Locate model spans in the word table The model reads text; the word table knows the time. Files: `src/rank/locate.py` 3. Snap every boundary to a word A cut inside a word is audible and unfixable later. Files: `src/rank/snap.py` ### Done when - [ ] Candidates appear with no model key present - [ ] Model timings are derived from the word table - [ ] Every candidate has hook, score and reason - [ ] Boundaries align to word edges ### Watch out - Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. ## M3 · Cuts that land where you asked A rendered clip starts and ends within a frame of the requested times, and the build proves it. ### Steps 1. Read the seeking documentation before writing the command Argument order is the difference between correct and nearly. Files: `docs/ffmpeg-seeking.md`, `src/render/cut.py` 2. Probe the output and compare with the request The only trustworthy timing is the one measured afterwards. Files: `src/render/verify.py`, `tests/test_cut_accuracy.py` 3. Store the arguments with the render Every strange clip is explained by its own command line. Files: `src/render/record.py` ### Done when - [ ] Actual start and duration are measured per render - [ ] A drift beyond one frame fails the render - [ ] The FFmpeg arguments are stored and visible - [ ] Nothing overwrites the source or a prior render ### Watch out - Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. ## M4 · Captions and framing, without pretending to track faces Burned captions are readable and correctly timed, and the vertical framing is honest about what it is doing. ### Steps 1. Group captions by pause and reading speed A fixed word count breaks lines in the wrong places. Files: `src/captions/group.py`, `tests/test_caption_grouping.py` 2. Preview the framing on one frame first Discovering the crop after a five minute render is the annoyance. Files: `src/render/preview.py` 3. Say centre crop, not smart reframing The paid product does the other thing and this one does not. Files: `src/web/templates/render.html` ### Done when - [ ] Caption lines break at pauses within a length cap - [ ] No caption appears before its first word - [ ] A preview frame precedes every render - [ ] No tracking claim appears in the interface ### Watch out - Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. ## M5 · Disk, batches and failures you can read (production only) A long session does not fill the disk, a batch that fails halfway is resumable, and every failure says what went wrong. ### Steps 1. Check free space before every render A full disk masquerades as a codec error. Files: `src/render/space.py` 2. Delete intermediates in a finally block The success path is not where the leftovers come from. Files: `src/render/queue.py`, `tests/test_cleanup.py` 3. Show the reason, log the command Users need the reason; you need the arguments. Files: `src/web/errors.py` ### Done when - [ ] Low disk space refuses the render with a message - [ ] Intermediates are removed on success and failure - [ ] A killed batch resumes and skips completed work - [ ] Failures show a reason without leaking paths ### Watch out - Letting a batch write intermediates until the disk is full produces a failure that looks like a video codec problem and takes an evening to diagnose, and by then the partial files are still there. Check free space before each render, refuse with a clear message, and delete intermediates in a finally block rather than on the success path. ===== OPERATIONS.md ===== # Operations · diclip ## Backup Project JSON and transcripts. The renders can be regenerated from them and the source, and the transcripts are the expensive part to recompute. ## Restore Restore projects, confirm the source hashes still match the files on disk, and re-render one clip to check that the measured start still matches. A source that has been re-encoded since will fail that check, which is the point. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool this lives in the suite and on the queue page: measured cut drift per render, free space, and the count of renders that failed with a reason. A rising drift usually means an input format whose seeking behaves differently. ## Incident checklist The failure that matters is a clip that shipped starting two seconds late, because nobody checks a render they already posted. The stored arguments and the measured start are what let you find which format or which code path caused it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Every render is confirmed to measure its actual start and duration - [ ] A drift beyond the tolerance is confirmed to fail the render - [ ] Candidates are confirmed to appear with no model key present - [ ] Caption lines are confirmed to break at pauses within a length cap - [ ] A low-space render is confirmed refused with a readable message ## Launch constraint Do not market omitted diclip capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project JSON and transcripts. The part worth backing up. PROJECT_DIR=~/clips/projects # Required. Finished clips. Regenerable, so back it up last. RENDER_DIR=~/clips/exports # Required. Renders refuse to start below this. It is the cheapest error message in the build. MIN_FREE_GB=10 # Required. Lower bound for a candidate. CLIP_MIN_SECONDS=30 # Required. Upper bound for a candidate. CLIP_MAX_SECONDS=90 # Optional · secret. Optional. Absent means the heuristic ranker runs. RANKER_API_KEY=sk-...redacted # Required. A render whose measured start drifts beyond this fails rather than shipping. CUT_TOLERANCE_FRAMES=1
# diclip · indie build A personal video clipper whose hard problems are producing word-level timings accurate enough to cut on, making the cut land exactly where the transcript said it would, and rendering captions and framing without pretending to do the face tracking that the paid product actually does. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small local web interface, bound to the loopback address | the media is large and local and nothing here wants to be uploaded | | Transcription | A local speech model producing word-level timings | the cut points come from the transcript, so the timings are the foundation | | Rendering | FFmpeg, invoked with explicit arguments you understand | every video tool is calling this underneath and the argument order changes the result | | Projects | JSON on disk beside the media, with renders in their own directory | a project should survive the application being deleted | | Ranking | A rented model if a key is present, a local heuristic if not | the ranking is a convenience and the tool has to work without it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the local application and the render queue. - Get it: python.org, or your version manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg with a recent build** · free - Why: Every cut, caption burn and framing operation is an FFmpeg invocation, and the seeking behaviour you depend on is documented there. - Get it: Your package manager, or a static build from the project. - Verify: ffmpeg -version prints a version and the probe tool is on the path - [ ] **A local speech model with word-level timestamps** · free, but the model download is large - Why: Word timings are what the cuts are computed from, so a model that only returns sentences is not usable here. - Get it: Install the library and download a model size your machine can run. - Verify: A one minute fixture returns words with start and end times - [ ] **Working disk space for sources, renders and intermediates** · free if you have the space - Why: A long recording plus its renders and intermediates is the constraint that ends most sessions, and it fails in a confusing way. - Get it: Point the project directory at a volume with room to spare and set the threshold below it. - Verify: The application reports free space and refuses below the threshold - [ ] **An API key for a text model** (optional) · pay per token; a long transcript is a few pence - Why: Only the ranking step uses it, and the build is designed to be useful without it. - Get it: Any provider, with a spend limit set in their console. - Verify: A ranked candidate list arrives with the source recorded as model ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Face-aware reframing and speaker tracking. - A timeline editor with tracks and effects. - Downloading source video from platforms. - Cloud rendering. - face-aware vertical reframing with seat tracking - full in-browser timeline editor with scenes, tracks, and effects - container-scale render pipeline and yt-dlp import - credit-based capacity model and priority processing - ready-to-post hooks and confidence scoring polished for clips If one of those is essential to you, that is the reason to keep paying for diclip, and the README should say so rather than pretend.
# Build brief · diclip The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local clipper that turns a long recording into vertical clips and never guesses where the cut actually landed to replace diclip. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Python with a small local web interface, bound to the loopback address · the media is large and local and nothing here wants to be uploaded - Transcription: A local speech model producing word-level timings · the cut points come from the transcript, so the timings are the foundation - Rendering: FFmpeg, invoked with explicit arguments you understand · every video tool is calling this underneath and the argument order changes the result - Projects: JSON on disk beside the media, with renders in their own directory · a project should survive the application being deleted - Ranking: A rented model if a key is present, a local heuristic if not · the ranking is a convenience and the tool has to work without it ### Data model (create this before Phase 1) - projects(id, source_path, source_sha256, duration_ms, imported_at, transcript_state, render_state) - words(project_id, ordinal, text, start_ms, end_ms, speaker, confidence) · the unit everything else is built from - segments(project_id, ordinal, start_ms, end_ms, speaker, text) · sentences assembled from words, never from the model's own punctuation guesses alone - candidates(id, project_id, start_ms, end_ms, hook_text, score, reason, source, state) · source is model or heuristic, state is pending, accepted or rejected - renders(id, candidate_id, aspect, out_path, started_at, finished_at, ffmpeg_args, actual_start_ms, actual_duration_ms, failure) - captions(render_id, ordinal, start_ms, end_ms, text, speaker) · grouped for reading, not one word per frame - actual_start_ms is measured from the produced file, not copied from the request. The gap between requested and actual is the number this build exists to keep at zero. ### Phase 1 · Import and transcribe, without touching the source Build: Import by path, hash the source, probe it for duration, streams and frame rate. Transcribe to word level timings and store every word with its start, end and confidence. Assemble sentences from word timings. Show progress with an honest estimate, because this step takes real time on a processor. Trap: Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. Done when: Words with start and end timings are stored for the whole file and sentences are derived from them, checked against a fixture with known speech. The source file's hash before and after import is identical, asserted in the suite. Re-importing the same path with an unchanged hash reuses the existing transcript rather than transcribing again. A file the probe cannot read fails with the reason and does not create a half-built project. Do not build yet: No ranking, no cutting, no captions, no rendering. ### Phase 2 · Candidates with a reason you can argue with Build: A heuristic ranker over the transcript using speaking pace, question marks, pause lengths and keyword density, producing candidates between a configured minimum and maximum length. If a model key is present, ask a model for candidates in the same shape and record which source produced each one. Snap every candidate to word boundaries from the word table. Trap: Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. Done when: With no model key configured the heuristic produces candidates and the interface says which ranker ran, tested with the key absent. Model-produced candidates are located by text span in the word table and their timings come from words, never from the model, asserted in the suite. Every candidate has a hook line, a score and a one-sentence reason, and a candidate missing any of them is not displayed. Candidate boundaries always align to word starts and ends, tested. Do not build yet: No cutting, no captions, no rendering, no export. ### Phase 3 · Cuts that land where you asked Build: Render with FFmpeg and read its own documentation on seeking before choosing argument order. As an input option before the input, a seek goes to the nearest seek point before the position; when transcoding with accurate seeking the surplus is decoded and discarded, but during stream copy or with accurate seeking disabled it is kept. Measure the produced file and compare against the request. Trap: Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. Done when: Every render probes the produced file and stores its actual start and duration, and a difference beyond one frame fails the render, tested against a fixture with a known keyframe interval. The exact FFmpeg arguments used are stored with the render and shown in the interface. A stream-copy mode, if offered, is labelled as keyframe aligned and the interface shows the boundary it will actually use. Rendering never writes over the source or an existing render. Do not build yet: No captions, no framing modes, no batch, no export. ### Phase 4 · Captions and framing, without pretending to track faces Build: Group words into caption lines by reading speed and pause length rather than fixed word counts, and burn them with a per-speaker style. Offer centre crop and blurred pad for vertical output and label each one plainly. Show a preview frame at the chosen framing before rendering the whole clip. Trap: Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. Done when: Caption lines break at pauses and respect a maximum character length, checked against a fast-speech fixture. Caption timings come from word timings and no line displays before its first word is spoken, tested. Centre crop and blurred pad are named in the interface and a preview frame is rendered before the clip. No claim of face or speaker tracking appears anywhere in the interface or the README. Do not build yet: No batch rendering, no exports, no retention. ### Phase 5 · Disk, batches and failures you can read Build: A render queue processing accepted candidates one at a time, resumable after a crash. A retention policy for renders and intermediate files with a configured ceiling, refusing to start a render when free space is below a threshold. Export a project as its JSON plus its renders. Surface the FFmpeg failure reason in the interface without exposing full paths. Trap: Letting a batch write intermediates until the disk is full produces a failure that looks like a video codec problem and takes an evening to diagnose, and by then the partial files are still there. Check free space before each render, refuse with a clear message, and delete intermediates in a finally block rather than on the success path. Done when: A render refuses to start when free space is below the configured threshold and says so, tested by pointing the output directory at a constrained volume. Intermediates are removed whether the render succeeded or failed, tested by forcing a failure mid-render. A batch killed halfway resumes and skips completed renders, tested. Every failure shows a readable reason in the interface and the full command in the project log, not in the interface. Do not build yet: Nothing after this. ### Out of scope (and why) - Face-aware reframing with speaker tracking, which needs a detection model, temporal smoothing and months of tuning, and is a genuine reason the subscription exists. - A timeline editor with tracks and effects, because building one that is worse than the free desktop editors you already have is not progress. - Downloading source video from platforms, which is a terms question rather than a technical one and is not needed when you own the recording. - Cloud rendering, since the cost model of a render farm is the actual product on the other side and there is nothing to learn from half of it. ### README must contain - That transcription and rendering are slow on a processor, with a measured figure from your own machine - That cut accuracy is measured on every render and what the one-frame tolerance means - That vertical framing is centre crop or blurred pad, with no face or speaker tracking of any kind - Where projects, renders and intermediates live, and what the disk space threshold does - That the ranking step is optional and the heuristic runs when no model key is present
# Agent instructions · diclip indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with a small local web interface, bound to the loopback address, A local speech model producing word-level timings, FFmpeg, invoked with explicit arguments you understand, JSON on disk beside the media, with renders in their own directory, A rented model if a key is present, a local heuristic if not. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. - Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. - Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. - Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly.
# Build plan · diclip A personal video clipper whose hard problems are producing word-level timings accurate enough to cut on, making the cut land exactly where the transcript said it would, and rendering captions and framing without pretending to do the face tracking that the paid product actually does. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Import and transcribe, without touching the source A long file becomes words with timings, and the original is never modified or moved. ### Steps 1. Store word timings, derive sentences from them A sentence boundary is the wrong resolution for a cut point. Files: `src/transcribe/words.py`, `tests/test_segments.py` 2. Hash the source and never write to it The recording is the one file you cannot regenerate. Files: `src/project/import.py` 3. Probe before you transcribe An unreadable file should fail in a second, not in an hour. Files: `src/media/probe.py` ### Done when - [ ] Word-level timings cover the whole file - [ ] The source hash is unchanged after import - [ ] A repeat import reuses the transcript - [ ] An unreadable file fails cleanly ### Watch out - Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. ## Phase 2 · Candidates with a reason you can argue with Every proposed clip carries a hook line, a score and a stated reason, and the tool works with no model key. ### Steps 1. Make the heuristic the default, not the fallback The tool has to work before anyone pays for a token. Files: `src/rank/heuristic.py`, `tests/test_heuristic.py` 2. Locate model spans in the word table The model reads text; the word table knows the time. Files: `src/rank/locate.py` 3. Snap every boundary to a word A cut inside a word is audible and unfixable later. Files: `src/rank/snap.py` ### Done when - [ ] Candidates appear with no model key present - [ ] Model timings are derived from the word table - [ ] Every candidate has hook, score and reason - [ ] Boundaries align to word edges ### Watch out - Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. ## Phase 3 · Cuts that land where you asked A rendered clip starts and ends within a frame of the requested times, and the build proves it. ### Steps 1. Read the seeking documentation before writing the command Argument order is the difference between correct and nearly. Files: `docs/ffmpeg-seeking.md`, `src/render/cut.py` 2. Probe the output and compare with the request The only trustworthy timing is the one measured afterwards. Files: `src/render/verify.py`, `tests/test_cut_accuracy.py` 3. Store the arguments with the render Every strange clip is explained by its own command line. Files: `src/render/record.py` ### Done when - [ ] Actual start and duration are measured per render - [ ] A drift beyond one frame fails the render - [ ] The FFmpeg arguments are stored and visible - [ ] Nothing overwrites the source or a prior render ### Watch out - Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. ## Phase 4 · Captions and framing, without pretending to track faces Burned captions are readable and correctly timed, and the vertical framing is honest about what it is doing. ### Steps 1. Group captions by pause and reading speed A fixed word count breaks lines in the wrong places. Files: `src/captions/group.py`, `tests/test_caption_grouping.py` 2. Preview the framing on one frame first Discovering the crop after a five minute render is the annoyance. Files: `src/render/preview.py` 3. Say centre crop, not smart reframing The paid product does the other thing and this one does not. Files: `src/web/templates/render.html` ### Done when - [ ] Caption lines break at pauses within a length cap - [ ] No caption appears before its first word - [ ] A preview frame precedes every render - [ ] No tracking claim appears in the interface ### Watch out - Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. ## Not in this build - Face-aware reframing and speaker tracking. - A timeline editor with tracks and effects. - Downloading source video from platforms. - Cloud rendering. ## After v1, if you want it - A per-project accuracy report of requested against measured cut points, which is the only quality number this build has - Caption style presets stored per project, so a series of clips looks like a series - A second pass that trims leading silence using the word table, which costs nothing and improves most clips
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project JSON and transcripts. The part worth backing up. PROJECT_DIR=~/clips/projects # Required. Finished clips. Regenerable, so back it up last. RENDER_DIR=~/clips/exports # Required. Renders refuse to start below this. It is the cheapest error message in the build. MIN_FREE_GB=10 # Required. Lower bound for a candidate. CLIP_MIN_SECONDS=30 # Required. Upper bound for a candidate. CLIP_MAX_SECONDS=90 # Optional · secret. Optional. Absent means the heuristic ranker runs. RANKER_API_KEY=sk-...redacted # Required. A render whose measured start drifts beyond this fails rather than shipping. CUT_TOLERANCE_FRAMES=1
# diclip · product brief ## Problem The core loop is genuinely one-shottable: transcribe, rank the strongest moments, cut vertical clips with burned captions. The gaps are execution and infra. Face-aware reframing with seat tracking, a full in-browser timeline editor, and a container-scale render pipeline are not a one-session build. A competent agent can produce a usable personal clipper quickly, but matching diclip's reframe quality and editing depth is a multi-day project. ## Product outcome A clipper that cuts where the transcript said, measures every render to prove it, burns captions that break at pauses, and declines to claim the reframing intelligence it does not have. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - FFmpeg - faster-whisper - LLM API key for moment ranking (optional, falls back to heuristics) - desktop with enough storage for source media and renders ## Explicit non-goals for v1 - Face-aware reframing and speaker tracking. - A timeline editor with tracks and effects. - Downloading source video from platforms. - Cloud rendering. - face-aware vertical reframing with seat tracking - full in-browser timeline editor with scenes, tracks, and effects - container-scale render pipeline and yt-dlp import - credit-based capacity model and priority processing - ready-to-post hooks and confidence scoring polished for clips ## Success criteria - Every render is confirmed to measure its actual start and duration - A drift beyond the tolerance is confirmed to fail the render - Candidates are confirmed to appear with no model key present - Caption lines are confirmed to break at pauses within a length cap - A low-space render is confirmed refused with a readable message
# Build brief · diclip The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local clipper that turns a long recording into vertical clips and never guesses where the cut actually landed to replace diclip. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Runtime: Python with a small local web interface, bound to the loopback address · the media is large and local and nothing here wants to be uploaded - Transcription: A local speech model producing word-level timings · the cut points come from the transcript, so the timings are the foundation - Rendering: FFmpeg, invoked with explicit arguments you understand · every video tool is calling this underneath and the argument order changes the result - Projects: JSON on disk beside the media, with renders in their own directory · a project should survive the application being deleted - Ranking: A rented model if a key is present, a local heuristic if not · the ranking is a convenience and the tool has to work without it ### Data model (create this before Phase 1) - projects(id, source_path, source_sha256, duration_ms, imported_at, transcript_state, render_state) - words(project_id, ordinal, text, start_ms, end_ms, speaker, confidence) · the unit everything else is built from - segments(project_id, ordinal, start_ms, end_ms, speaker, text) · sentences assembled from words, never from the model's own punctuation guesses alone - candidates(id, project_id, start_ms, end_ms, hook_text, score, reason, source, state) · source is model or heuristic, state is pending, accepted or rejected - renders(id, candidate_id, aspect, out_path, started_at, finished_at, ffmpeg_args, actual_start_ms, actual_duration_ms, failure) - captions(render_id, ordinal, start_ms, end_ms, text, speaker) · grouped for reading, not one word per frame - actual_start_ms is measured from the produced file, not copied from the request. The gap between requested and actual is the number this build exists to keep at zero. ### Phase 1 · Import and transcribe, without touching the source Build: Import by path, hash the source, probe it for duration, streams and frame rate. Transcribe to word level timings and store every word with its start, end and confidence. Assemble sentences from word timings. Show progress with an honest estimate, because this step takes real time on a processor. Trap: Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. Done when: Words with start and end timings are stored for the whole file and sentences are derived from them, checked against a fixture with known speech. The source file's hash before and after import is identical, asserted in the suite. Re-importing the same path with an unchanged hash reuses the existing transcript rather than transcribing again. A file the probe cannot read fails with the reason and does not create a half-built project. Do not build yet: No ranking, no cutting, no captions, no rendering. ### Phase 2 · Candidates with a reason you can argue with Build: A heuristic ranker over the transcript using speaking pace, question marks, pause lengths and keyword density, producing candidates between a configured minimum and maximum length. If a model key is present, ask a model for candidates in the same shape and record which source produced each one. Snap every candidate to word boundaries from the word table. Trap: Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. Done when: With no model key configured the heuristic produces candidates and the interface says which ranker ran, tested with the key absent. Model-produced candidates are located by text span in the word table and their timings come from words, never from the model, asserted in the suite. Every candidate has a hook line, a score and a one-sentence reason, and a candidate missing any of them is not displayed. Candidate boundaries always align to word starts and ends, tested. Do not build yet: No cutting, no captions, no rendering, no export. ### Phase 3 · Cuts that land where you asked Build: Render with FFmpeg and read its own documentation on seeking before choosing argument order. As an input option before the input, a seek goes to the nearest seek point before the position; when transcoding with accurate seeking the surplus is decoded and discarded, but during stream copy or with accurate seeking disabled it is kept. Measure the produced file and compare against the request. Trap: Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. Done when: Every render probes the produced file and stores its actual start and duration, and a difference beyond one frame fails the render, tested against a fixture with a known keyframe interval. The exact FFmpeg arguments used are stored with the render and shown in the interface. A stream-copy mode, if offered, is labelled as keyframe aligned and the interface shows the boundary it will actually use. Rendering never writes over the source or an existing render. Do not build yet: No captions, no framing modes, no batch, no export. ### Phase 4 · Captions and framing, without pretending to track faces Build: Group words into caption lines by reading speed and pause length rather than fixed word counts, and burn them with a per-speaker style. Offer centre crop and blurred pad for vertical output and label each one plainly. Show a preview frame at the chosen framing before rendering the whole clip. Trap: Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. Done when: Caption lines break at pauses and respect a maximum character length, checked against a fast-speech fixture. Caption timings come from word timings and no line displays before its first word is spoken, tested. Centre crop and blurred pad are named in the interface and a preview frame is rendered before the clip. No claim of face or speaker tracking appears anywhere in the interface or the README. Do not build yet: No batch rendering, no exports, no retention. ### Phase 5 · Disk, batches and failures you can read Build: A render queue processing accepted candidates one at a time, resumable after a crash. A retention policy for renders and intermediate files with a configured ceiling, refusing to start a render when free space is below a threshold. Export a project as its JSON plus its renders. Surface the FFmpeg failure reason in the interface without exposing full paths. Trap: Letting a batch write intermediates until the disk is full produces a failure that looks like a video codec problem and takes an evening to diagnose, and by then the partial files are still there. Check free space before each render, refuse with a clear message, and delete intermediates in a finally block rather than on the success path. Done when: A render refuses to start when free space is below the configured threshold and says so, tested by pointing the output directory at a constrained volume. Intermediates are removed whether the render succeeded or failed, tested by forcing a failure mid-render. A batch killed halfway resumes and skips completed renders, tested. Every failure shows a readable reason in the interface and the full command in the project log, not in the interface. Do not build yet: Nothing after this. ### Out of scope (and why) - Face-aware reframing with speaker tracking, which needs a detection model, temporal smoothing and months of tuning, and is a genuine reason the subscription exists. - A timeline editor with tracks and effects, because building one that is worse than the free desktop editors you already have is not progress. - Downloading source video from platforms, which is a terms question rather than a technical one and is not needed when you own the recording. - Cloud rendering, since the cost model of a render farm is the actual product on the other side and there is nothing to learn from half of it. ### README must contain - That transcription and rendering are slow on a processor, with a measured figure from your own machine - That cut accuracy is measured on every render and what the one-frame tolerance means - That vertical framing is centre crop or blurred pad, with no face or speaker tracking of any kind - Where projects, renders and intermediates live, and what the disk space threshold does - That the ranking step is optional and the heuristic runs when no model key is present
# Architecture · diclip ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small local web interface, bound to the loopback address | the media is large and local and nothing here wants to be uploaded | | Transcription | A local speech model producing word-level timings | the cut points come from the transcript, so the timings are the foundation | | Rendering | FFmpeg, invoked with explicit arguments you understand | every video tool is calling this underneath and the argument order changes the result | | Projects | JSON on disk beside the media, with renders in their own directory | a project should survive the application being deleted | | Ranking | A rented model if a key is present, a local heuristic if not | the ranking is a convenience and the tool has to work without it | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Hashing, probing and leaving the source alone. | Any importer, provided the source is never written to. | | Transcript | Word timings and derived sentences. | Any speech model, provided it returns word-level timestamps. | | Rank | Heuristic and optional model candidates. | Any ranker, provided timings still come from the word table. | | Render | FFmpeg invocation, measurement and stored arguments. | Any wrapper, provided output is probed and compared with the request. | | Queue | Batches, disk checks and cleanup. | Any queue, provided intermediates are removed on failure too. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PROJECT_DIR` · required · Project JSON and transcripts. The part worth backing up. - `RENDER_DIR` · required · Finished clips. Regenerable, so back it up last. - `MIN_FREE_GB` · required · Renders refuse to start below this. It is the cheapest error message in the build. - `CLIP_MIN_SECONDS` · required · Lower bound for a candidate. - `CLIP_MAX_SECONDS` · required · Upper bound for a candidate. - `RANKER_API_KEY` · optional, secret · Optional. Absent means the heuristic ranker runs. - `CUT_TOLERANCE_FRAMES` · required · A render whose measured start drifts beyond this fails rather than shipping. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · diclip product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python with a small local web interface, bound to the loopback address, A local speech model producing word-level timings, FFmpeg, invoked with explicit arguments you understand, JSON on disk beside the media, with renders in their own directory, A rented model if a key is present, a local heuristic if not. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. - Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. - Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. - Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. - Letting a batch write intermediates until the disk is full produces a failure that looks like a video codec problem and takes an evening to diagnose, and by then the partial files are still there. Check free space before each render, refuse with a clear message, and delete intermediates in a finally block rather than on the success path.
# Delivery milestones · diclip Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Import and transcribe, without touching the source A long file becomes words with timings, and the original is never modified or moved. ### Steps 1. Store word timings, derive sentences from them A sentence boundary is the wrong resolution for a cut point. Files: `src/transcribe/words.py`, `tests/test_segments.py` 2. Hash the source and never write to it The recording is the one file you cannot regenerate. Files: `src/project/import.py` 3. Probe before you transcribe An unreadable file should fail in a second, not in an hour. Files: `src/media/probe.py` ### Done when - [ ] Word-level timings cover the whole file - [ ] The source hash is unchanged after import - [ ] A repeat import reuses the transcript - [ ] An unreadable file fails cleanly ### Watch out - Storing only sentence timings because they are what the interface shows means every later cut is quantised to a sentence boundary, and the clip that needed to start three words earlier cannot. Store words, derive sentences, and keep the word table as the thing cuts are computed from. ## M2 · Candidates with a reason you can argue with Every proposed clip carries a hook line, a score and a stated reason, and the tool works with no model key. ### Steps 1. Make the heuristic the default, not the fallback The tool has to work before anyone pays for a token. Files: `src/rank/heuristic.py`, `tests/test_heuristic.py` 2. Locate model spans in the word table The model reads text; the word table knows the time. Files: `src/rank/locate.py` 3. Snap every boundary to a word A cut inside a word is audible and unfixable later. Files: `src/rank/snap.py` ### Done when - [ ] Candidates appear with no model key present - [ ] Model timings are derived from the word table - [ ] Every candidate has hook, score and reason - [ ] Boundaries align to word edges ### Watch out - Letting the model return timestamps directly gives you cut points that are plausible rather than real, because the model is reading text and inventing a time, and the clip then starts mid-word. Have the model return quoted text spans, locate those spans in the word table, and take the timings from the words. ## M3 · Cuts that land where you asked A rendered clip starts and ends within a frame of the requested times, and the build proves it. ### Steps 1. Read the seeking documentation before writing the command Argument order is the difference between correct and nearly. Files: `docs/ffmpeg-seeking.md`, `src/render/cut.py` 2. Probe the output and compare with the request The only trustworthy timing is the one measured afterwards. Files: `src/render/verify.py`, `tests/test_cut_accuracy.py` 3. Store the arguments with the render Every strange clip is explained by its own command line. Files: `src/render/record.py` ### Done when - [ ] Actual start and duration are measured per render - [ ] A drift beyond one frame fails the render - [ ] The FFmpeg arguments are stored and visible - [ ] Nothing overwrites the source or a prior render ### Watch out - Cutting with a stream copy because it is fast produces clips that begin at the previous keyframe, so a thirty second clip arrives with two seconds of the wrong sentence in front and the start you carefully snapped to a word is gone. Re-encode the cut, or accept keyframe-aligned boundaries explicitly, and in either case probe the output and compare its actual start and duration against what you asked for. ## M4 · Captions and framing, without pretending to track faces Burned captions are readable and correctly timed, and the vertical framing is honest about what it is doing. ### Steps 1. Group captions by pause and reading speed A fixed word count breaks lines in the wrong places. Files: `src/captions/group.py`, `tests/test_caption_grouping.py` 2. Preview the framing on one frame first Discovering the crop after a five minute render is the annoyance. Files: `src/render/preview.py` 3. Say centre crop, not smart reframing The paid product does the other thing and this one does not. Files: `src/web/templates/render.html` ### Done when - [ ] Caption lines break at pauses within a length cap - [ ] No caption appears before its first word - [ ] A preview frame precedes every render - [ ] No tracking claim appears in the interface ### Watch out - Grouping captions by a fixed word count produces lines that break mid-phrase and change at the wrong moment, which reads worse than no captions at all, and it is the first thing a viewer notices. Group by pause and reading speed with a maximum line length, and check the result against a clip where somebody speaks quickly. ## M5 · Disk, batches and failures you can read (production only) A long session does not fill the disk, a batch that fails halfway is resumable, and every failure says what went wrong. ### Steps 1. Check free space before every render A full disk masquerades as a codec error. Files: `src/render/space.py` 2. Delete intermediates in a finally block The success path is not where the leftovers come from. Files: `src/render/queue.py`, `tests/test_cleanup.py` 3. Show the reason, log the command Users need the reason; you need the arguments. Files: `src/web/errors.py` ### Done when - [ ] Low disk space refuses the render with a message - [ ] Intermediates are removed on success and failure - [ ] A killed batch resumes and skips completed work - [ ] Failures show a reason without leaking paths ### Watch out - Letting a batch write intermediates until the disk is full produces a failure that looks like a video codec problem and takes an evening to diagnose, and by then the partial files are still there. Check free space before each render, refuse with a clear message, and delete intermediates in a finally block rather than on the success path.
# Operations · diclip ## Backup Project JSON and transcripts. The renders can be regenerated from them and the source, and the transcripts are the expensive part to recompute. ## Restore Restore projects, confirm the source hashes still match the files on disk, and re-render one clip to check that the measured start still matches. A source that has been re-encoded since will fail that check, which is the point. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool this lives in the suite and on the queue page: measured cut drift per render, free space, and the count of renders that failed with a reason. A rising drift usually means an input format whose seeking behaves differently. ## Incident checklist The failure that matters is a clip that shipped starting two seconds late, because nobody checks a render they already posted. The stored arguments and the measured start are what let you find which format or which code path caused it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Every render is confirmed to measure its actual start and duration - [ ] A drift beyond the tolerance is confirmed to fail the render - [ ] Candidates are confirmed to appear with no model key present - [ ] Caption lines are confirmed to break at pauses within a length cap - [ ] A low-space render is confirmed refused with a readable message ## Launch constraint Do not market omitted diclip capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Project JSON and transcripts. The part worth backing up. PROJECT_DIR=~/clips/projects # Required. Finished clips. Regenerable, so back it up last. RENDER_DIR=~/clips/exports # Required. Renders refuse to start below this. It is the cheapest error message in the build. MIN_FREE_GB=10 # Required. Lower bound for a candidate. CLIP_MIN_SECONDS=30 # Required. Upper bound for a candidate. CLIP_MAX_SECONDS=90 # Optional · secret. Optional. Absent means the heuristic ranker runs. RANKER_API_KEY=sk-...redacted # Required. A render whose measured start drifts beyond this fails rather than shipping. CUT_TOLERANCE_FRAMES=1
$ choose a build depth, inspect the files, then open the complete pack in your agent
Creators pay to skip the editing grind: diclip reads the full transcript, explains why each clip was chosen, and opens a prepared edit. The reframe quality and editor depth are tuned over months, not a weekend, and the render pipeline just works on long sources.
xface-aware vertical reframing with seat tracking
xfull in-browser timeline editor with scenes, tracks, and effects
xcontainer-scale render pipeline and yt-dlp import
xcredit-based capacity model and priority processing
xready-to-post hooks and confidence scoring polished for clips
Nothing worth pointing at. That's why the prompt exists.
diclip pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | $0/workspace | 3 clips once; 1 source-hour total; 60 min per source upload cap |
| starter | $2/workspace | — | 30 clips/mo; 2.5 source-hours/mo; 90 min per source |
| creator | $6/workspace | — | 60 clips/mo; 5.75 source-hours/mo; 120 min per source |
| studio | $11/workspace | — | 120 clips/mo; 11 source-hours/mo; 120 min per source; priority processing |
free tier3 clips once, 1 source-hour total, 60 min upload cap
billingmonthly or prepaid 3/12 months (IDR, charged in full; capacity resets monthly)
hidden costsTop-up packs (one-time, active subscription required) for extra source minutes and clips.
verified 2026-08-16 · source ↗
Is diclip free?
Free once: 3 clips and 1 source-hour total, no renewal. Paid is Creator at $6/mo (checked 2026-08-16).
Vibecode diclip
Kinda. The core of diclip is buildable in a weekend with the prompt on this page, but there are real gaps: face-aware vertical reframing with seat tracking, full in-browser timeline editor with scenes, tracks, and effects. Read the honest list above before committing.
How much does diclip cost?
diclip costs about $6/month (Creator, checked 2026-08-16), which is $72 per year.
What do I lose by replacing diclip?
Honestly: face-aware vertical reframing with seat tracking; full in-browser timeline editor with scenes, tracks, and effects; container-scale render pipeline and yt-dlp import; credit-based capacity model and priority processing; ready-to-post hooks and confidence scoring polished for clips. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to diclip?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.