Summary
Add the ability to put an external file (e.g. a cleaned .m4a from Auphonic, or
a graded .mov) back into a project as a media-bin source — imported but
not placed on the timeline. Scope for v1 is strictly "add to the bin"; no
timeline placement, no clip replacement. That keeps it low-risk and additive.
Proposed command:
camkit import-media <file> [--project PATH] [--dry-run] [--force]
Why this is separate from export-video
Unlike rendering (which must go through Camtasia — see the export-video issue),
adding a bin source is pure project.tscproj JSON mutation plus a metadata
probe. No Camtasia render, no AppleScript. So it belongs in @camkit/core
behind unit tests, mirroring how rebuild and captions mutate the doc. It was
floated as "part of export-video if easy" — it isn't the same machinery, so it's
its own command. Doing this also makes the round-trip useful: cleaned audio /
graded video currently just sit in ~/Downloads; their value is back in the
project.
How the source bin works (already mapped in the codebase)
packages/core/src/project.ts documents the shape. Each entry in
doc.sourceBin looks like:
listSources() derives duration as (range[1] - range[0]) / editRate, and
planAudioExport/exportAudio resolve src against the project dir falling
back to media/ (resolveSource in media.ts). A new source must follow the
same conventions.
Implementation notes
- Reverse-engineer the full entry shape from a real project. project.ts only
models the fields camkit reads. Open an existing .cmproj, copy the shape of a
comparable already-imported source (an audio source for .m4a, a video source
for .mov), and fill in metadata rather than inventing fields. Add a fixture
for tests.
- Probe metadata with ffprobe (ffmpeg is already a documented prerequisite).
Need duration → range/editRate, plus dimensions/channels/framerate for
video vs audio sources. Put the ffprobe call in the CLI/media layer
(side-effecting); keep the pure "build a sourceBin entry from probed metadata"
function in @camkit/core and unit-test it.
- New source id:
Math.max(...doc.sourceBin.map(s => s.id)) + 1 (handle the
empty bin). Confirm ids are also referenced nowhere else that needs updating
for a bin-only add (timeline medias reference ids, but we're not touching the
timeline).
- File placement: decide and document whether the command copies the file
into the project's media/ dir (so src stays a stable relative path, matching
how Camtasia stores imports) or stores the given path. Copying into media/ is
recommended — it matches resolveSource's lookup and survives the project
being moved. State the choice in --help.
- Safety — reuse the rebuild/captions pattern exactly (
cmdRebuild /
cmdCaptions in camkit.ts): back up to project.tscproj.bak, refuse to run
with a ~project.tscproj lock present or an existing .bak unless --force,
and support --dry-run to print the planned source entry and write nothing.
- macOS-independent: this is cross-platform (no AppleScript). Don't gate it
behind assertDarwin().
- Reopen note in output: like
captions, remind the user that Camtasia must be
closed when writing and to reopen to see the new bin item.
Where the code goes
@camkit/core: a planMediaImport(doc, {file, probed}) → sourceBin entry
(pure, tested) and the mutation that appends it. Export from core/src/index.ts.
@camkit/cli: cmdImportMedia in camkit.ts — arg parse, ffprobe call,
optional copy into media/, backup/lock/dry-run handling, console output;
register in COMMANDS, add HELP["import-media"] and a printHelp summary.
Acceptance criteria
Out of scope (v1)
Placing the source on the timeline, replacing the original audio/video clip with
the cleaned/graded version, and any keyframe/gain handling. Those are a likely
follow-up ("round-trip the cleaned audio onto the timeline") but explicitly not
part of this issue.
Summary
Add the ability to put an external file (e.g. a cleaned
.m4afrom Auphonic, ora graded
.mov) back into a project as a media-bin source — imported butnot placed on the timeline. Scope for v1 is strictly "add to the bin"; no
timeline placement, no clip replacement. That keeps it low-risk and additive.
Proposed command:
Why this is separate from
export-videoUnlike rendering (which must go through Camtasia — see the export-video issue),
adding a bin source is pure
project.tscprojJSON mutation plus a metadataprobe. No Camtasia render, no AppleScript. So it belongs in
@camkit/corebehind unit tests, mirroring how
rebuildandcaptionsmutate the doc. It wasfloated as "part of export-video if easy" — it isn't the same machinery, so it's
its own command. Doing this also makes the round-trip useful: cleaned audio /
graded video currently just sit in
~/Downloads; their value is back in theproject.
How the source bin works (already mapped in the codebase)
packages/core/src/project.tsdocuments the shape. Each entry indoc.sourceBinlooks like:{ "id": <number>, // unique source id "src": "<relative path>", // resolved against project dir, then media/ "sourceTracks": [{ "range": [start, end], // in that track's editRate units "editRate": <number>, // ...type, dimensions, channels, etc. }] }listSources()derives duration as(range[1] - range[0]) / editRate, andplanAudioExport/exportAudioresolvesrcagainst the project dir fallingback to
media/(resolveSourceinmedia.ts). A new source must follow thesame conventions.
Implementation notes
models the fields camkit reads. Open an existing
.cmproj, copy the shape of acomparable already-imported source (an audio source for
.m4a, a video sourcefor
.mov), and fill in metadata rather than inventing fields. Add a fixturefor tests.
Need duration →
range/editRate, plus dimensions/channels/framerate forvideo vs audio sources. Put the ffprobe call in the CLI/media layer
(side-effecting); keep the pure "build a sourceBin entry from probed metadata"
function in
@camkit/coreand unit-test it.Math.max(...doc.sourceBin.map(s => s.id)) + 1(handle theempty bin). Confirm ids are also referenced nowhere else that needs updating
for a bin-only add (timeline
mediasreference ids, but we're not touching thetimeline).
into the project's
media/dir (sosrcstays a stable relative path, matchinghow Camtasia stores imports) or stores the given path. Copying into
media/isrecommended — it matches
resolveSource's lookup and survives the projectbeing moved. State the choice in
--help.cmdRebuild/cmdCaptionsincamkit.ts): back up toproject.tscproj.bak, refuse to runwith a
~project.tscprojlock present or an existing.bakunless--force,and support
--dry-runto print the planned source entry and write nothing.behind
assertDarwin().captions, remind the user that Camtasia must beclosed when writing and to reopen to see the new bin item.
Where the code goes
@camkit/core: aplanMediaImport(doc, {file, probed}) → sourceBin entry(pure, tested) and the mutation that appends it. Export from
core/src/index.ts.@camkit/cli:cmdImportMediaincamkit.ts— arg parse, ffprobe call,optional copy into
media/, backup/lock/dry-run handling, console output;register in
COMMANDS, addHELP["import-media"]and aprintHelpsummary.Acceptance criteria
camkit import-media cleaned.m4aadds the file todoc.sourceBin(and tomedia/if copying), with correct duration and a unique id, leaving thetimeline untouched. Verified by
camkit sourcesshowing it asbin only..m4a) and a video (.mov) source.--dry-runprints the planned entry and writes nothing;.bak+ locksafety match
rebuild/captions.--helpupdated.bun testandbun run typecheckpass.item (note this in the PR per CONTRIBUTING).
Out of scope (v1)
Placing the source on the timeline, replacing the original audio/video clip with
the cleaned/graded version, and any keyframe/gain handling. Those are a likely
follow-up ("round-trip the cleaned audio onto the timeline") but explicitly not
part of this issue.