Skip to content

Commit d36fda3

Browse files
committed
feat: add FCP project support with --fcp CLI option
Implement the ability to filter TCX workout data using Final Cut Pro project timeline clips. This allows users to generate WebVTT cues that are synchronized with video clips in their FCP project. Key features: - New --fcp CLI option to specify FCP project export file - FCPReader class to parse FCP XML exports (1.13+ format) - TimelineMapper for filtering TCX samples based on FCP clip times - Comprehensive test fixtures for various FCP project scenarios - Updated README with FCP usage instructions Fixes timezone handling in TCX parsing and FCP timestamp calculations to ensure proper timeline synchronization.
1 parent 330bc1d commit d36fda3

File tree

24 files changed

+1477
-102
lines changed

24 files changed

+1477
-102
lines changed

README.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# tcx2webvtt
22

3-
Convert a TCX workout file to WebVTT with JSON metadata
3+
Convert a [TCX] workout file to [WebVTT] with JSON metadata
4+
5+
[tcx]: https://en.wikipedia.org/wiki/Training_Center_XML
6+
[webvtt]: https://en.wikipedia.org/wiki/WebVTT
7+
8+
## Requirements
9+
10+
A system with [Node.js] v20 or higher.
11+
12+
[Node.js]: https://nodejs.org/
413

514
## Usage
615

@@ -10,11 +19,26 @@ Install globally, or run without installing using `npx`.
1019
npm install -g tcx2webvtt
1120
```
1221

13-
Convert a [TCX] workout file to [WebVTT].
22+
Convert a TCX workout file to WebVTT.
1423

1524
```sh
1625
tcx2webvtt my-workout.tcx > my-track.vtt
1726
```
1827

19-
[tcx]: https://en.wikipedia.org/wiki/Training_Center_XML
20-
[webvtt]: https://en.wikipedia.org/wiki/WebVTT
28+
### Using With Final Cut Pro
29+
30+
You can use an export of a Final Cut Pro project to extract the relevant parts of the TCX
31+
workout file and produce a synchronized WebVTT file.
32+
33+
To create an export of your project, first [create a metadata view] in Final Cut Pro named
34+
`tcx2webvtt`. Add the “Content Created” metadata field to this view. Then select “Export
35+
XML…” from the “File” menu. Choose the `tcx2webvtt` metadata view and XML version 1.13 or
36+
higher. (Older versions may work fine, but they are untested.)
37+
38+
[create a metadata view]: https://support.apple.com/guide/final-cut-pro/modify-metadata-views-ver397297af/11.1/mac/14.6
39+
40+
Now that you have an export, run `tcx2webvtt` with the `--fcp` option:
41+
42+
```sh
43+
tcx2webvtt --fcp 'My Project Export.fcpxmld' my-workout.tcx > my-track.vtt
44+
```
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE fcpxml>
3+
4+
<fcpxml version="1.13">
5+
<resources>
6+
<format id="r1" name="FFVideoFormat3840x2160p30" frameDuration="100/3000s" width="3840" height="2160" colorSpace="9-16-9 (Rec. 2020 PQ)"/>
7+
<asset id="r2" name="GX010163" uid="46350EE69C6E19E13D3C5A4BE8615FDA" start="1499755257/30000s" duration="32672640/30000s" hasVideo="1" format="r3" hasAudio="1" videoSources="1" audioSources="1" audioChannels="2" audioRate="48000" customLUTOverride="LUT:37ad4adf25eeec768f3139a39ac7b057 (GPLOG_Creative_EV0)">
8+
<media-rep kind="original-media" sig="46350EE69C6E19E13D3C5A4BE8615FDA" src="file:///Volumes/Pinnate/FCP/Final%20Cut%20Original%20Media/2025-04-20/GX010163.mov">
9+
<bookmark>Ym9vayQEAAAAAAQQMAAAAADqBB0xQJb1UXb7KZ2iGgLfCtt9q/jDgNWS5TD4Qb7lRAMAAAQAAAADAwAAABgAKAcAAAABAQAAVm9sdW1lcwAHAAAAAQEAAFBpbm5hdGUAAwAAAAEBAABGQ1AAGAAAAAEBAABGaW5hbCBDdXQgT3JpZ2luYWwgTWVkaWEKAAAAAQEAADIwMjUtMDQtMjAAAAwAAAABAQAAR1gwMTAxNjMubW92GAAAAAEGAAAQAAAAIAAAADAAAAA8AAAAXAAAAHAAAAAIAAAABAMAANM4AAAAAAAACAAAAAQDAAACAAAAAAAAAAgAAAAEAwAAHDwEAAAAAAAIAAAABAMAADA8BAAAAAAACAAAAAQDAACQCwUAAAAAAAgAAAAEAwAA0QoFAAAAAAAYAAAAAQYAAKQAAAC0AAAAxAAAANQAAADkAAAA9AAAAAgAAAAABAAAQcbayxuAAAAYAAAAAQIAAAEAAAAAAAAADwAAAAAAAAAAAAAAAAAAABgAAAABCQAAZmlsZTovLy9Wb2x1bWVzL1Bpbm5hdGUvCAAAAAQDAAAAQH9j6goAAAgAAAAABAAAQcYPSyqAAAAkAAAAAQEAADcwNjU4ODhGLTgyN0EtM0IzNS1CRUM4LTYyNkI3QUQwMDE5OBgAAAABAgAAAQEAAAEAAADvEwAAAQAAAAAAAAAAAAAAEAAAAAEBAAAvVm9sdW1lcy9QaW5uYXRlCAAAAAEJAABmaWxlOi8vLwwAAAABAQAATWFjaW50b3NoIEhECAAAAAQDAAAAUKEbcwAAAAgAAAAABAAAQcbVF0yAAAAkAAAAAQEAAEMyNDJEMzBELTdCOTgtNEU4Qy1CNDdCLUM5NzhBQTU4QkQ4MxgAAAABAgAAgQAAAAEAAADvEwAAAQAAAAAAAAAAAAAAAQAAAAEBAAAvAAAAYAAAAP7///8A8AAAAAAAAAcAAAACIAAAiAIAAAAAAAAFIAAA+AEAAAAAAAAQIAAACAIAAAAAAAARIAAAPAIAAAAAAAASIAAAHAIAAAAAAAATIAAALAIAAAAAAAAgIAAAaAIAAAAAAAAEAAAAAwMAAADwAAAEAAAAAwMAAAAAAAAEAAAAAwMAAAEAAAAcAAAAAQYAAPwCAAAIAwAAFAMAAAgDAAAIAwAACAMAAAgDAACoAAAA/v///wEAAACUAgAADQAAAAQQAACEAAAAAAAAAAUQAAAEAQAAAAAAABAQAAA0AQAAAAAAAEAQAAAkAQAAAAAAAAAgAAAgAwAAAAAAAAIgAADgAQAAAAAAAAUgAABUAQAAAAAAABAgAAAgAAAAAAAAABEgAACUAQAAAAAAABIgAAB0AQAAAAAAABMgAACEAQAAAAAAACAgAADAAQAAAAAAABDQAAAEAAAAAAAAAA==</bookmark>
10+
</media-rep>
11+
<metadata>
12+
<md key="com.apple.proapps.studio.metadataContentCreated" value="2025-04-20 13:53:11 -0700"/>
13+
</metadata>
14+
</asset>
15+
<format id="r3" name="FFVideoFormat3840x2160p2997" frameDuration="1001/30000s" width="3840" height="2160" colorSpace="1-1-1 (Rec. 709)"/>
16+
<asset id="r4" name="GX020163" uid="C28B666CFA40B64388E8B2B594AA6BD3" start="1532427897/30000s" duration="20900880/30000s" hasVideo="1" format="r3" hasAudio="1" videoSources="1" audioSources="1" audioChannels="2" audioRate="48000" customLUTOverride="LUT:37ad4adf25eeec768f3139a39ac7b057 (GPLOG_Creative_EV0)">
17+
<media-rep kind="original-media" sig="C28B666CFA40B64388E8B2B594AA6BD3" src="file:///Volumes/Pinnate/FCP/Final%20Cut%20Original%20Media/2025-04-20/GX020163.mov">
18+
<bookmark>Ym9vayQEAAAAAAQQMAAAAJo4I+CZFpqMMjGp5Lt8Tr7XeoCqR0bCR3Wbo1eeIFY6RAMAAAQAAAADAwAAABgAKAcAAAABAQAAVm9sdW1lcwAHAAAAAQEAAFBpbm5hdGUAAwAAAAEBAABGQ1AAGAAAAAEBAABGaW5hbCBDdXQgT3JpZ2luYWwgTWVkaWEKAAAAAQEAADIwMjUtMDQtMjAAAAwAAAABAQAAR1gwMjAxNjMubW92GAAAAAEGAAAQAAAAIAAAADAAAAA8AAAAXAAAAHAAAAAIAAAABAMAANM4AAAAAAAACAAAAAQDAAACAAAAAAAAAAgAAAAEAwAAHDwEAAAAAAAIAAAABAMAADA8BAAAAAAACAAAAAQDAACQCwUAAAAAAAgAAAAEAwAAkgsFAAAAAAAYAAAAAQYAAKQAAAC0AAAAxAAAANQAAADkAAAA9AAAAAgAAAAABAAAQcbazT0AAAAYAAAAAQIAAAEAAAAAAAAADwAAAAAAAAAAAAAAAAAAABgAAAABCQAAZmlsZTovLy9Wb2x1bWVzL1Bpbm5hdGUvCAAAAAQDAAAAQH9j6goAAAgAAAAABAAAQcYPSyqAAAAkAAAAAQEAADcwNjU4ODhGLTgyN0EtM0IzNS1CRUM4LTYyNkI3QUQwMDE5OBgAAAABAgAAAQEAAAEAAADvEwAAAQAAAAAAAAAAAAAAEAAAAAEBAAAvVm9sdW1lcy9QaW5uYXRlCAAAAAEJAABmaWxlOi8vLwwAAAABAQAATWFjaW50b3NoIEhECAAAAAQDAAAAUKEbcwAAAAgAAAAABAAAQcbVF0yAAAAkAAAAAQEAAEMyNDJEMzBELTdCOTgtNEU4Qy1CNDdCLUM5NzhBQTU4QkQ4MxgAAAABAgAAgQAAAAEAAADvEwAAAQAAAAAAAAAAAAAAAQAAAAEBAAAvAAAAYAAAAP7///8A8AAAAAAAAAcAAAACIAAAiAIAAAAAAAAFIAAA+AEAAAAAAAAQIAAACAIAAAAAAAARIAAAPAIAAAAAAAASIAAAHAIAAAAAAAATIAAALAIAAAAAAAAgIAAAaAIAAAAAAAAEAAAAAwMAAADwAAAEAAAAAwMAAAAAAAAEAAAAAwMAAAEAAAAcAAAAAQYAAPwCAAAIAwAAFAMAAAgDAAAIAwAACAMAAAgDAACoAAAA/v///wEAAACUAgAADQAAAAQQAACEAAAAAAAAAAUQAAAEAQAAAAAAABAQAAA0AQAAAAAAAEAQAAAkAQAAAAAAAAAgAAAgAwAAAAAAAAIgAADgAQAAAAAAAAUgAABUAQAAAAAAABAgAAAgAAAAAAAAABEgAACUAQAAAAAAABIgAAB0AQAAAAAAABMgAACEAQAAAAAAACAgAADAAQAAAAAAABDQAAAEAAAAAAAAAA==</bookmark>
19+
</media-rep>
20+
<metadata>
21+
<md key="com.apple.proapps.studio.metadataContentCreated" value="2025-04-20 14:11:22 -0700"/>
22+
</metadata>
23+
</asset>
24+
</resources>
25+
<library location="file:///Users/eric/Movies/Travelogues.fcpbundle/" colorProcessing="wide-hdr">
26+
<event name="To Honeybee Canyon Ride" uid="8F4438E0-7641-44A0-8F00-65BD45F925D6">
27+
<project name="tcx2webvtt Hard Cuts Fixture" uid="2EBD323F-4E1C-4875-9609-9B1CADFE4EF8" modDate="2025-05-11 09:55:21 -0700">
28+
<sequence format="r1" duration="2641/30s" tcStart="0s" tcFormat="NDF" audioLayout="stereo" audioRate="48k">
29+
<spine>
30+
<asset-clip ref="r2" offset="0s" name="GX010163" start="63019957/1250s" duration="233/15s" format="r3" tcFormat="DF" audioRole="dialogue">
31+
<conform-rate srcFrameRate="29.97"/>
32+
</asset-clip>
33+
<asset-clip ref="r2" offset="466/30s" name="GX010163" start="304745441/6000s" duration="556/30s" format="r3" tcFormat="DF" audioRole="dialogue">
34+
<conform-rate srcFrameRate="29.97"/>
35+
</asset-clip>
36+
<asset-clip ref="r2" offset="1022/30s" name="GX010163" start="153194041/3000s" duration="487/30s" format="r3" tcFormat="DF" audioRole="dialogue">
37+
<conform-rate srcFrameRate="29.97"/>
38+
</asset-clip>
39+
<asset-clip ref="r4" offset="1509/30s" name="GX020163" start="510809299/10000s" duration="619/30s" format="r3" tcFormat="DF" audioRole="dialogue">
40+
<conform-rate srcFrameRate="29.97"/>
41+
</asset-clip>
42+
<asset-clip ref="r4" offset="2128/30s" name="GX020163" start="31993962/625s" duration="171/10s" format="r3" tcFormat="DF" audioRole="dialogue">
43+
<conform-rate srcFrameRate="29.97"/>
44+
</asset-clip>
45+
</spine>
46+
<metadata>
47+
<md key="com.apple.proapps.studio.metadataContentCreated" value="2025-05-11 09:52:31 -0700"/>
48+
</metadata>
49+
</sequence>
50+
</project>
51+
</event>
52+
<smart-collection name="Projects" match="all">
53+
<match-clip rule="is" type="project"/>
54+
</smart-collection>
55+
<smart-collection name="All Video" match="any">
56+
<match-media rule="is" type="videoOnly"/>
57+
<match-media rule="is" type="videoWithAudio"/>
58+
</smart-collection>
59+
<smart-collection name="Audio Only" match="all">
60+
<match-media rule="is" type="audioOnly"/>
61+
</smart-collection>
62+
<smart-collection name="Stills" match="all">
63+
<match-media rule="is" type="stills"/>
64+
</smart-collection>
65+
<smart-collection name="Favorites" match="all">
66+
<match-ratings value="favorites"/>
67+
</smart-collection>
68+
</library>
69+
</fcpxml>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE fcpxml>
3+
4+
<fcpxml version="1.13">
5+
<resources>
6+
<format id="r1" name="FFVideoFormat3840x2160p30" frameDuration="100/3000s" width="3840" height="2160" colorSpace="9-16-9 (Rec. 2020 PQ)"/>
7+
<format id="r2" name="FFVideoFormat3840x2160p2997" frameDuration="1001/30000s" width="3840" height="2160" colorSpace="1-1-1 (Rec. 709)"/>
8+
<asset id="r3" name="GX010163" uid="46350EE69C6E19E13D3C5A4BE8615FDA" start="1499755257/30000s" duration="32672640/30000s" hasVideo="1" format="r2" hasAudio="1" videoSources="1" audioSources="1" audioChannels="2" audioRate="48000">
9+
<media-rep kind="original-media" sig="46350EE69C6E19E13D3C5A4BE8615FDA" src="file:///Volumes/Test/GX010163.mov">
10+
</media-rep>
11+
<!-- Asset has no metadata section -->
12+
</asset>
13+
<asset id="r4" name="GX020163" uid="56350EE69C6E19E13D3C5A4BE8615FDB" start="1499755257/30000s" duration="32672640/30000s" hasVideo="1" format="r2" hasAudio="1" videoSources="1" audioSources="1" audioChannels="2" audioRate="48000">
14+
<media-rep kind="original-media" sig="56350EE69C6E19E13D3C5A4BE8615FDB" src="file:///Volumes/Test/GX020163.mov">
15+
</media-rep>
16+
<metadata>
17+
<!-- Metadata section exists but no metadataContentCreated -->
18+
<md key="com.apple.proapps.other.metadata" value="some value"/>
19+
</metadata>
20+
</asset>
21+
</resources>
22+
<library location="file:///Users/eric/Movies/Test.fcpbundle/" colorProcessing="wide-hdr">
23+
<event name="Test Event" uid="8F4438E0-7641-44A0-8F00-65BD45F925D6">
24+
<project name="Missing Metadata Test" uid="E88373D9-4B69-4B19-B4F3-66E75C844051" modDate="2025-05-10 09:58:35 -0700">
25+
<sequence format="r1" duration="900/30s" tcStart="0s" tcFormat="NDF" audioLayout="stereo" audioRate="48k">
26+
<spine>
27+
<clip offset="0s" name="GX010163" start="1519326809/30000s" duration="453/30s" format="r2" tcFormat="DF">
28+
<video ref="r3" offset="1499755257/30000s" start="1499755257/30000s" duration="32672640/30000s"/>
29+
</clip>
30+
<clip offset="453/30s" name="GX020163" start="1519326809/30000s" duration="447/30s" format="r2" tcFormat="DF">
31+
<video ref="r4" offset="1499755257/30000s" start="1499755257/30000s" duration="32672640/30000s"/>
32+
</clip>
33+
</spine>
34+
</sequence>
35+
</project>
36+
</event>
37+
</library>
38+
</fcpxml>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE fcpxml>
3+
4+
<fcpxml version="1.13">
5+
<resources>
6+
<format id="r1" name="FFVideoFormat3840x2160p30" frameDuration="100/3000s" width="3840" height="2160" colorSpace="9-16-9 (Rec. 2020 PQ)"/>
7+
<format id="r2" name="FFVideoFormat3840x2160p2997" frameDuration="1001/30000s" width="3840" height="2160" colorSpace="1-1-1 (Rec. 709)"/>
8+
<asset id="r3" name="GX010163" uid="46350EE69C6E19E13D3C5A4BE8615FDA" start="1499755257/30000s" duration="32672640/30000s" hasVideo="1" format="r2" hasAudio="1" videoSources="1" audioSources="1" audioChannels="2" audioRate="48000">
9+
<media-rep kind="original-media" sig="46350EE69C6E19E13D3C5A4BE8615FDA" src="file:///Volumes/Test/GX010163.mov">
10+
</media-rep>
11+
<!-- Asset has no metadata section -->
12+
</asset>
13+
</resources>
14+
<library location="file:///Users/eric/Movies/Test.fcpbundle/" colorProcessing="wide-hdr">
15+
<event name="Test Event" uid="8F4438E0-7641-44A0-8F00-65BD45F925D6">
16+
<project name="Missing Metadata Asset Clip Test" uid="E88373D9-4B69-4B19-B4F3-66E75C844051" modDate="2025-05-10 09:58:35 -0700">
17+
<sequence format="r1" duration="453/30s" tcStart="0s" tcFormat="NDF" audioLayout="stereo" audioRate="48k">
18+
<spine>
19+
<!-- asset-clip element with missing metadata -->
20+
<asset-clip ref="r3" offset="0s" start="1519326809/30000s" duration="453/30s" format="r2"/>
21+
</spine>
22+
</sequence>
23+
</project>
24+
</event>
25+
</library>
26+
</fcpxml>

0 commit comments

Comments
 (0)