Skip to content

Transcript-driven editing (Descript model) + build/test/DB/rebrand fixes#3

Merged
preston176 merged 1590 commits into
mainfrom
transcript-editor
Jun 8, 2026
Merged

Transcript-driven editing (Descript model) + build/test/DB/rebrand fixes#3
preston176 merged 1590 commits into
mainfrom
transcript-editor

Conversation

@preston176

Copy link
Copy Markdown
Owner

Turns the OpenCut fork into OpenScript's transcript-driven editor and gets the branch shippable: repairs the build/tests, implements the full "edit video by editing the transcript" model, hardens transcription, fixes the DB tooling, and clears the rebrand leftovers.

Highlights

  • Build repaired — fixed 12 TypeScript errors blocking next build; 3 were also live runtime bugs (sticker registry + storage migrations were calling object-param APIs positionally → registering under undefined / indexedDB.open(undefined)).
  • Editor-core test infraopencut-wasm is a wasm-pack bundler build that crashed bun test at load; a preload now instantiates the real wasm, so the editor core is testable. 193→200+ tests run, 228 pass / 1 skip / 0 fail.
  • Transcript correctness — undo no longer leaves words struck-through, orphan tracks fixed, transcript persists across reload/tab-toggle.
  • Edit-by-transcript — model + language pickers (with download sizes), inline word correction.
  • Full Descript model — destructive ripple-close on delete, recoverable strikethrough tombstones (click a struck word to restore), a source↔timeline mapping so seek/highlight stay correct after cuts, all as one atomic undo step (timeline + document), plus a v1→v2 migrator. Correctness covered by unit tests incl. a delete↔restore round-trip fidelity gate (contiguity + retime, no tick drift).
  • Transcription robustness — default to the Tiny model (fast on the wasm fallback), gate Generate on timelineHasAudio, a Cancel button, and preserve recognized text when a model returns no word-level timing.
  • DB tooling — fixed the broken drizzle schema path; regenerated the migration so a fresh DB has the feedback table the code uses (and not the dead waitlist); README rewritten (the editor runs fully local; Postgres/Redis only for optional auth/feedback/sounds).
  • Rebrand cleanup/roadmap 404 + dead sitemap routes, manifest.json/LICENSE/.github/rust OpenCut→OpenScript, dead Discord invite + security@opencut.app removed, placeholder email/social links removed, orphan OpenCut assets + empty packages/* dirs deleted.

Verification

  • tsc --noEmit clean; bun test 228 pass / 1 skip / 0 fail; next build exits 0.
  • Driven live in a real browser: the transcript edit flow (delete → ripple-close, atomic undo, click-to-restore, mapping-based seek) and the real Whisper generate path (audio → model download → inference → 19 timed words rendered).

Known follow-ups (not blockers)

  • apps/desktop is an honest empty-window scaffold; a real native implementation is future work.
  • In-app reload-persistence and "export drops deleted audio" still want a pass on real hardware (verified the edit flow + generate; these need a completed real transcript).
  • Pre-existing ESLint style warnings remain (build does not gate on them).

mazeincoding and others added 30 commits March 2, 2026 17:29
allows downloading brand assets directly from the logo in the header
Fix for #703.

Adds an optional sampleRate parameter to the decodeAudioToFloat32() function. The caption transcriber passes in a value of 16000 to prepare audio in the format Whisper expects.
mazeincoding and others added 24 commits May 6, 2026 22:53
Hard-fork opencut at the repo root. Replaces the previous Electron+Whisper
apps/desktop with opencut's Next.js web editor (apps/web), GPU-compositor
Rust crates, and native desktop shell. apps/website (marketing) preserved.

Conflict resolution:
- Took opencut's package.json, bun.lock, .gitignore (its tooling becomes the base)
- Replaced our apps/desktop entirely with opencut's
- Wrote a fresh top-level README explaining the fork relationship

Upstream is wired as the `opencut` remote for future `git fetch opencut main`.
Adds a Descript-style "edit by editing the transcript" panel that drives
real cuts on opencut's timeline.

- Switch transcription worker to word-level timestamps (return_timestamps:
  "word") and group words back into sentence-like segments for display.
  Subtitles consumer keeps working since segments still expose start/end.
- New apps/web/src/transcript-editor/ module:
  - types.ts: TranscriptDocument with per-word IDs and selection state.
  - apply-deletion.ts: maps word ranges to splitElements + deleteElements
    calls, classifying each covering element as fully-inside, partial-
    left, partial-right, or straddle-both. Latest-first iteration so
    earlier ranges keep stable timeline coordinates.
  - transcript-panel.tsx: panel UI. Click word = seek; shift/cmd-click
    or double-click = toggle selection; delete button cuts the timeline.
- Wire panel into the editor route as a toggleable "Transcript" tab in
  the right column (alongside Properties).

Also removes ~90 stale Electron files from apps/desktop that the
opencut merge auto-kept; HEAD now matches opencut's apps/desktop tree.

Known limitation: each transcript edit emits 3-5 separate timeline
commands (one per split / delete) so undo steps through them
individually. BatchCommand wrapping deferred to a follow-up.
Major UX overhaul of the transcript editor:

- Document-style layout: timestamps in a left margin, generous line-height,
  reading-width container, prose typography. Each segment is its own
  paragraph with a 0:00 timestamp.
- Active-word highlighting: as the video plays, the current word gets an
  accent background and auto-scrolls into view. Re-renders driven by
  editor.playback subscription via useEditor.
- Range selection: shift-click extends selection from the last clicked
  word; cmd/ctrl-click toggles a single word; plain click seeks and
  anchors the range.
- Filler-word detection: scans words against a stop-list (um, uh, like,
  you know, etc.). Flagged words get a dotted amber underline. Header
  shows "Remove N fillers" button for one-shot bulk removal.
- Cmd+F find: opens a search bar, highlights matches in yellow with the
  active match in solid yellow, Enter cycles forward, Shift+Enter back,
  Esc closes.
- Single undo per edit: applyTranscriptDeletions now constructs commands
  directly, executes them in sequence to thread split IDs, then pushes a
  BatchCommand to the command manager. One Cmd+Z reverses the whole edit.

Supporting changes:

- src/env/web.ts: defaulted DB/Redis/auth/CMS/FreeSound vars so the
  editor boots without backing services. Routes that actually use them
  will fail at use; transcript editor doesn't touch any of them.
- src/transcription/models.ts: swap whisper-tiny/small/medium from
  onnx-community to Xenova exports. The onnx-community ASR exports
  don't carry cross-attentions, breaking return_timestamps:"word".
Renames @opencut/web -> @openscript/web (root scripts and apps/web/package.json).
Updates SITE_INFO and visible UI strings (header, footer, onboarding,
mobile gate, storage dialog, legal pages, browser-compat banner, better-auth
appName).

Removes marketing routes that don't belong in an editor product:
- /blog, /roadmap, /sponsors, /contributors, /brand, /changelog
- src/changelog/* (release notes data + ChangelogNotification component)
- src/components/landing/* (hero etc.)
- src/components/gitHub-contribute-section.tsx
- src/site/sponsors.ts and src/site/external-tools.ts (now-unused)

Replaces / with a redirect to /projects so users land on their project list.
Strips the OpenCut analytics tracker (databuddy) and the OpenCut nav links
from Header. Footer reduced to copyright + Privacy/Terms.

Kept (not user-visible):
- LICENSE (MIT requires preserving the OpenCut copyright notice)
- localStorage/zustand persist keys ("opencut-keybindings",
  "opencut-storage-persist-dismissed") to avoid orphaning user data
- eslint plugin name "opencut/prefer-object-params" in disable comments
- The opencut-wasm npm dep (it's the upstream-published artifact)
- public/logos/opencut/svg/logo.svg as DEFAULT_LOGO_URL with a TODO to
  replace with an OpenScript wordmark
- editor-header.tsx: replace OpenCut wordmark <Image> in the project
  dropdown trigger with a text "OS" mark. Drop the Discord menu item
  and its now-orphan separator. Cleans up unused imports (Image,
  Link, FaDiscord, DEFAULT_LOGO_URL, SOCIAL_LINKS, DropdownMenuSeparator).
- onboarding.tsx: drop the third onboarding step (the Discord pitch)
  and the inert roadmap link from step 2. Replace the dynamic step
  title with a static "OpenScript Onboarding" label for the
  screen-reader DialogTitle.

DEFAULT_LOGO_URL and SOCIAL_LINKS still exist in src/site for the
privacy/terms pages that link out to GitHub/X (currently inert "#"
hrefs); replace those when real social accounts exist.
next build was failing on the TypeScript gate (12 errors); three of the
root causes were also live runtime bugs. Fix all and add the action plan.

- actions/keybindings: implement isShortcutKey + isActionWithOptionalArgs;
  persistence.ts imported guards that never existed. The 9 pre-existing
  persistence tests now compile and pass.
- stickers: register providers with object params (was registering every
  built-in provider under key `undefined`, breaking the sticker library).
- storage migrations: construct IndexedDBAdapter / call set() with object
  params in runner + v1-to-v2 (was indexedDB.open(undefined), silently
  breaking legacy-project migration).
- timeline tests: wrap raw numbers with MediaTime brand helpers.
- bun.lock: reconcile with package.json (was stale; broke --frozen-lockfile).
- add PLAN.md.

Verified: tsc --noEmit clean (was 12 errors), next build exits 0,
keybindings suite 9/9.
opencut-wasm is a wasm-pack "bundler"-target package: its entry calls
wasm.__wbindgen_start() on an import that bun's test runner doesn't
instantiate, so every test reaching @/wasm crashed at module load — the
entire editor core (timeline, masks, transcript, storage) was untested.

Add a bun test preload that performs the bundler step itself against the
real wasm (read bytes, instantiate with the glue as the import object,
wire it in, run the start hook), then redirect the bare "opencut-wasm"
specifier to the initialized glue. Tests now exercise the actual Rust
implementation, not a hand-written stub that could drift from source.

Result: 193 tests across 30 files now run (was ~0 in the wasm graph) —
192 pass, 1 skip, 0 fail. Surfaced failures fixed:
- resolve.test: time spans used fractional ticks (2.5/5.5); MediaTime is
  integer ticks. Use integer positions that preserve the overlap scenario.
- snap.test (box scale): a centered uniform scale snaps both edges to
  ±100; expect both vertical guides, not just the right one.
- snap.test (point split): De Casteljau subdivision of the straight a→b
  segment yields collinear handles (-0.1 in / +0.1 out), not zero.
- snap.test (text mask): needs a DOM/canvas font-metrics context; skip
  under bun, runs in a browser/jsdom env.

Verified: tsc --noEmit clean, bun test 192 pass / 1 skip / 0 fail.
The transcript panel stored a per-word `deleted` flag and mutated it on
delete, so it drifted from the actual timeline: undo restored the media
but left words struck-through, and reload showed the full text over an
already-cut timeline. Deletions also bypassed reactors, orphaning tracks.

- Derive `deleted` from timeline coverage instead of storing it. A word is
  deleted when its midpoint is no longer covered by media (new coverage.ts,
  7 unit tests). The panel subscribes to the active scene's tracks and
  recomputes, so undo/redo and reload stay correct automatically — the cut
  media returning makes the word reappear. `deleted` is dropped from
  TranscriptWord; filler-words/search take a deletedWordIds set.
- Run reactors after a transcript delete (CommandManager.reactToExternalChange).
  Deletions execute sub-commands incrementally to read live element IDs, so
  they use push() rather than execute(); push() skips reactors, which left
  empty audio/overlay tracks behind. (Ripple-on-delete stays in Phase 2 — it
  needs word-coordinate re-sync.)
- Persist the transcript (text + word timings only) per project and rehydrate
  on mount, so it survives tab-toggle and reload. Deleted-state is NOT stored;
  it is re-derived from the persisted timeline, so the two cannot diverge.
  Transcript is removed when its project is deleted.

Verified: tsc clean, bun test 199 pass / 1 skip / 0 fail, next build exits 0.
Toward the "edit by transcript" promise (Phase 2, parts a+b):

- Model + language selectors in the transcript empty state, threaded into
  transcribe(). Previously locked to whisper-small/auto despite 4 models and
  9 languages being defined but unused. Each model shows an approximate
  first-use download size (new approxDownloadMb) so users on slow/metered
  connections can pick Tiny; a note clarifies it runs locally in-browser.
- Inline word correction: double-click a word to edit its text (Enter/blur
  commits, Escape cancels), persisted with the transcript. This is a
  transcript-only edit — it changes the displayed/exported text, not the
  timeline or word timing — so it is low-risk and independent of the
  coverage-derived deleted state.

Ripple-close on delete (Phase 2c) is intentionally not included: closing
timeline gaps conflicts with deriving deleted-state from gaps and needs an
architecture decision (tracked separately).

Verified: tsc clean, bun test 199 pass / 1 skip / 0 fail, next build exits 0.
…en, transcript store

Phase 2c foundation (steps 1-4 of 10). Inert scaffolding for the destructive
Descript transcript model; not yet wired into the delete flow.

- types.ts: v2 model — TranscriptWord.deleted, DeletedRange + RemovedPiece
  (lossless element capture for restore), TranscriptDocument.deletedRanges.
- mapping.ts: pure source<->timeline mapping (frozen source seconds minus removed
  durations of earlier cuts; in-gap clamps to gap edge); integer-tick math.
- ripple/open.ts: rippleOpenElements, the inverse of rippleShiftElements (restore).
- core/managers/transcript-store.ts: EditorCore-owned holder for the doc, wired
  into useEditor — lets a Command mutate the transcript from global undo/redo.
- storage SerializedTranscript.version loosened to number (v1->v2 migration lands
  with the wiring). DESIGN.md records the full plan.

Tests: 14 new (types/mapping/ripple-open/store). tsc clean, full suite 213 pass /
1 skip / 0 fail.
…; save side-effect)

EditorCore constructs under bun, but updateTracks triggers SaveManager -> IndexedDB
(absent in bun) and would reject — so the fidelity test should drive a pure
split/trim+ripple transform on fixture tracks rather than a live editor. Captures
the delete input contract (contiguous runs) for the panel rewire.
Phase 2c steps 5-6 (delete side). Both pure and unit-tested on fixture tracks —
no editor/save coupling, so they are the testable core of the destructive model.

- timeline/split-element.ts: splitElementAtTime — a pure, generic, retime-aware
  element split (verbatim mirror of SplitElementsCommand's per-element math,
  snap-source-once). Net-new split test coverage. (Kept separate from the command
  for now to avoid touching untested split behavior; noted to keep in sync.)
- transcript-editor/delete-transform.ts: deleteRangeFromTracks — removes a
  [start,end) range across all tracks (split straddlers, drop contained), captures
  the removed slices as RemovedPiece (with offsetTicks for restore), and
  ripple-closes the gap uniformly. Returns {afterTracks, removed, durationTicks}.

Tests cover straddle-both, fully-contained, no-overlap, ripple-close, capture, and
trim math. tsc clean; full suite 219 pass / 1 skip / 0 fail.
…ity gate

Phase 2c step 6c/7. restoreRangeToTracks is the pure inverse of
deleteRangeFromTracks: ripple-open the gap and re-insert the captured slices
(lossless but for a fresh id). The round-trip test is the primary correctness
gate for the destructive model and now runs purely (no editor):

- straddle-both reconstructs exact contiguous source coverage (startTimes,
  durations, trimStarts).
- a multi-element timeline stays contiguous and the same total length after
  delete->restore (catches tick drift).
- a retimed element round-trips without drift and keeps its retime config.

tsc clean; full suite 222 pass / 1 skip / 0 fail.
Phase 2c step 6b/6c. Composes the pure transforms into the editable model:

- plan.ts: planTranscriptDelete / planTranscriptRestore — pure (modulo a fresh
  range id). Delete processes runs latest-source-first (so each cut's timeline
  position, derived from pre-existing cuts, is unaffected by the batch's earlier
  cuts), marks words deleted, and records DeletedRanges; restore re-opens the gap
  at its current position, reconstructs the media, un-marks the words, drops the
  range. Tested: delete records+ripple-closes, restore reverses, unknown id no-op.
- transcript-edit-command.ts: TranscriptEditCommand snapshots tracks + doc and
  applies the plan; undo/redo replay snapshots so one undo reverts the timeline
  AND the transcript document atomically (the desync fix). dispatchTranscriptEdit
  records via push() (the plan already rippled — avoid the global ripple toggle
  double-applying) + runs reactors. Known v1 edge noted: restoring a cut that
  emptied+pruned a whole track can't rebuild it.

tsc clean; full suite 225 pass / 1 skip / 0 fail.
Phase 2c step 9. migrateTranscript normalizes a persisted transcript to the v2
document shape. v1 (pre-Descript) stored no deleted/deletedRanges and relied on
coverage-derived deletion against an already-cut timeline; migrate it once by
deriving each word's `deleted` from current timeline coverage and starting an
empty deletedRanges log — so pre-v2 cuts show struck but are not restorable
(documented). Never mutates the timeline. v2+ docs pass through. Tested:
v1 derivation, v2 pass-through, no-tracks fallback. tsc clean.
The Descript-model backend is complete and tested (transforms, plan, command,
migrator). The panel rewire is the last integration and is the part that needs
in-app verification, so it is specified precisely here (exact deltas + a live
test checklist) for a turnkey hand-off rather than shipped unverified.
Phase 2c final step. The panel now drives the destructive, undoable,
restorable model end to end:

- reads the document from the editor-owned TranscriptStore (reactive), so a
  command can mutate it from global undo/redo; generate/load/edit write the store.
- deleted state comes from the stored `deleted` flag (not coverage).
- on mount, loads + migrates (v1->v2) the persisted transcript into the store.
- delete builds contiguous runs from the selection and dispatches a
  TranscriptEditCommand (split + delete + ripple-close + tombstone, one undo step).
- seek and the active-word highlight go through the source<->timeline mapping,
  so they stay correct after cuts.
- clicking a struck-through word restores its whole cut (ripple-open +
  reconstruct); pre-v2 cuts stay non-restorable.
- retires coverage from the render hot path (kept only for the v1->v2 migrator)
  and removes the superseded apply-deletion.ts.

tsc clean; full suite 228 pass / 1 skip / 0 fail; next build exits 0.
Needs in-app verification (ripple/restore/seek/undo/reload) — checklist in DESIGN.md.
- Default to the Tiny model (~40 MB): fast even on the single-threaded wasm
  fallback (no WebGPU / no cross-origin isolation), where larger models are
  painfully slow. The picker still offers Small/Medium/Large.
- Gate Generate on timelineHasAudio: a silent/empty timeline now shows
  "This timeline has no audio to transcribe" instead of downloading the model
  and running Whisper on silence (verified in-app).
- Cancel button during transcription (the service already supported cancel);
  cancelling returns to the start rather than surfacing an error.
- Preserve recognized text when a model returns no word-level timing, so the
  user still sees what was transcribed instead of a bare error.

Verified: tsc clean, suite 228 pass / 1 skip / 0 fail, next build exits 0;
in-app confirmed default=Tiny and the audio gate.
- drizzle.config.ts schema path pointed at ./src/lib/db/schema.ts (no such
  dir), so db:generate/migrate/push all failed. Fix to ./src/db/schema.ts.
- The committed migration created a dead `waitlist` table and omitted the
  `feedback` table the app uses (feedback popover -> db.insert(feedback)),
  so feedback 500s on a freshly-migrated DB. Regenerated migrations from the
  corrected schema (pre-launch; no deployed DB depends on the old one): the
  new 0000 creates accounts/feedback/sessions/users/verifications, no waitlist.
- README "Getting started" claimed the editor needs Postgres + Redis. It does
  not — the editor runs fully local (IndexedDB/OPFS + on-device Whisper).
  Reworded: those services are only for the optional auth/feedback/sound-search
  routes, with the .env.local copy + docker + db:migrate steps spelled out.
- mobile gate: the "Roadmap" button linked to a removed /roadmap route (404 on
  mobile/iPad); point it at the GitHub repo instead.
- sitemap: drop the removed routes (/contributors, /roadmap, /why-not-capcut,
  /blog + per-post) that all 404'd; emit only /, /privacy, /terms.
- manifest.json: name/description were still "OpenCut" + the old tagline.
- LICENSE: copyright OpenCut -> OpenScript.
- package.json: remove the dead *:tools scripts (no @openscript/tools package).
- social links: set the real GitHub repo (x/discord left as TODO placeholders).
- desktop: window title OpenCut -> OpenScript, and README now states plainly
  that it's an early empty-window scaffold (not wired to the editor) and that
  commands run from the repo root.

Verified: tsc clean, next build exits 0.
…available)

Rather than leave stubs for things we have no real values for:

- Drop the placeholder contact email and the "#" X/Twitter + Discord social
  links from /privacy and /terms; route contact to the real GitHub issues link.
  SOCIAL_LINKS reduced to the github repo only.
- Remove the dead /blog RSS feed (apps/web/src/app/rss.xml) and its now-orphaned
  blog data layer (apps/web/src/blog) — there is no blog route.
- Remove the unused DEFAULT_LOGO_URL and the OpenCut wordmark assets it pointed
  at (public/logos/opencut, public/landing-page-dark.png); the header uses a
  text wordmark.
- Remove the empty packages/{env,ui,desktop-bridge} scaffolding dirs.
- .github docs: rebrand OpenCut -> OpenScript (SUPPORT/CONTRIBUTING/SECURITY/
  CODE_OF_CONDUCT/feature_request), remove the dead Discord invite, and point
  security reports at GitHub security advisories instead of the dead
  security@opencut.app. Left the legit `opencut-wasm` package name and the
  opencut/opencut local Postgres creds (consistent with docker-compose).

Verified: tsc clean, next build exits 0.
Fix the dead github.com/opencut/opencut repository URL (rust/wasm/Cargo.toml,
rust/wasm/README.md) to the real OpenScript repo, and OpenScript the rust/
README. Left the published `opencut-wasm` package/crate name and its
opencut_wasm_* file names intact (the JS side imports them by that name).
@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openscript Ready Ready Preview, Comment Jun 8, 2026 5:28pm

CI `next build` failed type-checking next.config.ts: the root pinned
`next: ^16.1.3` (resolving to 16.2.x, which adds `adapterPath` to
NextConfig) while apps/web pinned `next: 16.1.3`, so two incompatible
`next` copies were visible and `NextConfig` types didn't match
("Property 'adapterPath' is missing"). Pin the root to `next: 16.1.3`
(and `typescript: ^5.8.3`) to match apps/web so the workspace resolves a
single version. (apps/website keeps its own next 16.1.1 — isolated.)

Verified: bun install + `cd apps/web && bun run build` exits 0.
`version: latest` mis-resolves to wasm-pack v0.9.1, whose bundled
wasm-opt fails ("exited with exit code 1") on the wasm that current
rustc/wasm-bindgen emit. Pinning to the current stable fixes the
Build WASM step across all three runner OSes.
@preston176 preston176 merged commit 06bdbe0 into main Jun 8, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants