88from .clipData import ClipData
99from . import makeOtio
1010
11- #currently only handles video and audio tracks
12- def diff (timelineA , timelineB ):
13- # TODO: put docstring here, descriptive name, most wordy descrip
11+ def diffTimelines (timelineA , timelineB ):
12+ '''Diff two OTIO timelines and identify how clips on video and/or audio tracks changed from timeline A to timeline B.
13+ Return an annotated otio file with the differences and print a text summary to console.
14+
15+ Parameters:
16+ timelineA (otio.schema.Timeline()): timeline from the file you want to compare against, ex. clip1 version 1
17+ timelineB (otio.schema.Timeline()): timeline from the file you want to compare, ex. clip1 version 2
18+
19+ Returns:
20+ outputTimeline (otio.schema.Timeline()): timeline with color coded clips and marker annotations showing the
21+ differences between the input tracks with the tracks from timeline B stacked on top of timeline A
22+ '''
1423 hasVideo = False
1524 hasAudio = False
1625
@@ -25,61 +34,58 @@ def diff(timelineA, timelineB):
2534 # else:
2635 # print("no audio tracks")
2736
28- makeTlSummary (timelineA , timelineB )
37+ makeTimelineSummary (timelineA , timelineB )
2938
30- outputTl = None
39+ outputTimeline = None
3140 # process video tracks, audio tracks, or both
3241 if hasVideo and hasAudio :
33- videoClipTable = processTracks (timelineA .video_tracks (), timelineB .video_tracks ())
34- audioClipTable = processTracks (timelineA .audio_tracks (), timelineB .audio_tracks ())
42+ videoClipTable = categorizeClipsByTracks (timelineA .video_tracks (), timelineB .video_tracks ())
43+ audioClipTable = categorizeClipsByTracks (timelineA .audio_tracks (), timelineB .audio_tracks ())
3544
3645 makeSummary (videoClipTable , otio .schema .Track .Kind .Video , "perTrack" )
3746 makeSummary (audioClipTable , otio .schema .Track .Kind .Audio , "summary" )
3847
3948 videoTl = makeNewOtio (videoClipTable , otio .schema .Track .Kind .Video )
40- outputTl = makeNewOtio (audioClipTable , otio .schema .Track .Kind .Audio )
49+ outputTimeline = makeNewOtio (audioClipTable , otio .schema .Track .Kind .Audio )
4150 # combine
4251 for t in videoTl .tracks :
43- outputTl .tracks .append (copy .deepcopy (t ))
52+ outputTimeline .tracks .append (copy .deepcopy (t ))
4453
4554 elif hasVideo :
46- videoClipTable = processTracks (timelineA .video_tracks (), timelineB .video_tracks ())
55+ videoClipTable = categorizeClipsByTracks (timelineA .video_tracks (), timelineB .video_tracks ())
4756 makeSummary (videoClipTable , otio .schema .Track .Kind .Video , "summary" )
48- outputTl = makeNewOtio (videoClipTable , otio .schema .Track .Kind .Video )
57+ outputTimeline = makeNewOtio (videoClipTable , otio .schema .Track .Kind .Video )
4958
5059 elif hasAudio :
51- audioClipTable = processTracks (timelineA .audio_tracks (), timelineB .audio_tracks ())
60+ audioClipTable = categorizeClipsByTracks (timelineA .audio_tracks (), timelineB .audio_tracks ())
5261 makeSummary (audioClipTable , "Audio" , "summary" )
53- outputTl = makeNewOtio (audioClipTable , otio .schema .Track .Kind .Audio )
62+ outputTimeline = makeNewOtio (audioClipTable , otio .schema .Track .Kind .Audio )
5463
5564 else :
56- # TODO: log no vid/aud or throw
57- pass
65+ print ("No video or audio tracks found in both timelines." )
5866
5967 # Debug
6068 # origClipCount = len(timelineA.find_clips()) + len(timelineB.find_clips())
6169
6270 # print(origClipCount)
63- # print(len(outputTl .find_clips()))
71+ # print(len(outputTimeline .find_clips()))
6472
65- return outputTl
73+ return outputTimeline
6674
67- def toOtio (data , path ):
68- otio .adapters .write_to_file (data , path )
69-
70- # for debugging, put response into file
71- def toJson (file ):
72- with open ("clipDebug.json" , "w" ) as f :
73- f .write (file )
75+ # TODO: make nonClones a set rather than a list
76+ def findClones (clips ):
77+ """Separate the cloned ClipDatas (ones that share the same name) from the unique ClipDatas and return both
78+
79+ Paramenters:
80+ clips (list of ClipDatas): list of ClipDatas
7481
75- def toTxt (file ):
76- with open ("report.txt" , "w" ) as f :
77- f .write (file )
82+ Returns:
83+ clones (dictionary): dictionary of all clones in the group of ClipDatas
84+ keys: name of clone
85+ values: list of ClipDatas of that name
86+ nonClones (list): list of unique clones in group of ClipDatas\
87+ """
7888
79- # create a dictionary with all the cloned clips (ones that share the same truncated name)
80- # key is the truncated name, value is a list of ClipDatas
81- # @parameter clips, list of ClipDatas
82- def findClones (clips ):
8389 clones = {}
8490 nonClones = []
8591 names = []
@@ -98,6 +104,8 @@ def findClones(clips):
98104 return clones , nonClones
99105
100106def sortClones (clipDatasA , clipDatasB ):
107+ """Identify cloned ClipDatas (ones that share the same name) across two groups of ClipDatas and separate from the unique
108+ ClipDatas (ones that only appear once in each group)"""
101109 # find cloned clips and separate out from unique clips
102110 clonesA , nonClonesA = findClones (clipDatasA )
103111 clonesB , nonClonesB = findClones (clipDatasB )
@@ -120,8 +128,8 @@ def sortClones(clipDatasA, clipDatasB):
120128 # clipCountB = 0
121129 return (clonesA , nonClonesA ), (clonesB , nonClonesB )
122130
123- # compare all clips that had a clone
124131def compareClones (clonesA , clonesB ):
132+ """Compare two groups of cloned ClipDatas and categorize into added, unchanged, or deleted"""
125133 added = []
126134 unchanged = []
127135 deleted = []
@@ -160,8 +168,8 @@ def compareClones(clonesA, clonesB):
160168
161169 return added , unchanged , deleted
162170
163- # compare all strictly unique clips
164171def compareClips (clipDatasA , clipDatasB ):
172+ """Compare two groups of unique ClipDatas and categorize into added, edited, unchanged, and deleted"""
165173 namesA = {}
166174 namesB = {}
167175
@@ -176,6 +184,7 @@ def compareClips(clipDatasA, clipDatasB):
176184 namesB [c .name ] = c
177185
178186 for cB in clipDatasB :
187+
179188 if cB .name not in namesA :
180189 added .append (cB )
181190 else :
@@ -205,29 +214,8 @@ def compareClips(clipDatasA, clipDatasB):
205214# TODO: some can be sets instead of lists
206215 return added , edited , unchanged , deleted
207216
208- # # clip is an otio Clip
209- # def getTake(clip):
210- # take = None
211- # if(len(clip.name.split(" ")) > 1):
212- # take = clip.name.split(" ")[1]
213- # else:
214- # take = None
215- # return take
216-
217- # TODO: change name, make comparable rep? clip comparator?
218- # TODO: learn abt magic functions ex __eq__
219- # def makeClipData(clip, trackNum):
220- # cd = ClipData(clip.name.split(" ")[0],
221- # clip.media_reference,
222- # clip.source_range,
223- # clip.trimmed_range_in_parent(),
224- # trackNum,
225- # clip,
226- # getTake(clip))
227- # return cd
228-
229- # the consolidated version of processVideo and processAudio, meant to replace both
230217def compareTracks (trackA , trackB , trackNum ):
218+ """Compare clipis in two OTIO tracks and categorize into added, edited, same, and deleted"""
231219 clipDatasA = []
232220 clipDatasB = []
233221
@@ -244,27 +232,25 @@ def compareTracks(trackA, trackB, trackNum):
244232 (clonesA , nonClonesA ), (clonesB , nonClonesB ) = sortClones (clipDatasA , clipDatasB )
245233
246234 # compare clips and put into categories
247- addV = []
248- editV = []
249- sameV = []
250- deleteV = []
235+ added = []
236+ edited = []
237+ unchanged = []
238+ deleted = []
251239
252240 # compare and categorize unique clips
253- addV , editV , sameV , deleteV = compareClips (nonClonesA , nonClonesB )
241+ added , edited , unchanged , deleted = compareClips (nonClonesA , nonClonesB )
254242
255243 # compare and categorize cloned clips
256- addCloneV , sameCloneV , deleteCloneV = compareClones (clonesA , clonesB )
257- addV .extend (addCloneV )
258- sameV .extend (sameCloneV )
259- deleteV .extend (deleteCloneV )
244+ addedClone , unchangedClone , deletedClone = compareClones (clonesA , clonesB )
245+ added .extend (addedClone )
246+ unchanged .extend (unchangedClone )
247+ deleted .extend (deletedClone )
260248
261- # SortedClipDatas = namedtuple('VideoGroup', ['add', 'edit', 'same', 'delete'])
262- # videoGroup = SortedClipDatas(addV, editV, sameV, deleteV)
263-
264- return addV , editV , sameV , deleteV
265- # return videoGroup
249+ return added , edited , unchanged , deleted
266250
251+ # TODO? account for move edit, currently only identifies strictly moved
267252def checkMoved (allDel , allAdd ):
253+ """Identify ClipDatas that have moved between different tracks"""
268254 # ones found as same = moved
269255 # ones found as edited = moved and edited
270256
@@ -290,8 +276,8 @@ def checkMoved(allDel, allAdd):
290276
291277 return newAdd , moveEdit , moved , newDel
292278
293- # TODO? account for move edit, currently only identifies strictly moved
294279def sortMoved (clipTable ):
280+ """Put ClipDatas that have moved between tracks into their own category and remove from their previous category"""
295281 allAdd = []
296282 allEdit = []
297283 allSame = []
@@ -323,8 +309,17 @@ def sortMoved(clipTable):
323309 return clipTable
324310
325311def makeNewOtio (clipTable , trackType ):
312+ """Make a new annotated OTIO timeline showing the change from timeline A to timeline B, with the tracks
313+ from timeline B stacked on top of the tracks from timeline A
314+
315+ Ex. New timeline showing the differences of timeline A and B with 2 tracks each
316+ Track 2B
317+ Track 1B
318+ ========
319+ Track 2A
320+ Track 1A
321+ """
326322 newTl = otio .schema .Timeline (name = "diffed" )
327- # TODO: rename into track sets
328323 tracksInA = []
329324 tracksInB = []
330325
@@ -362,15 +357,29 @@ def makeNewOtio(clipTable, trackType):
362357
363358# TODO: rename to create bucket/cat/db/stuff; categorizeClipsByTracks + comment
364359
365- def processTracks (tracksA , tracksB ):
366- # TODO: add docstring like this for public facing functions, otherwise comment is ok
367- """Return a copy of the input timelines with only tracks that match
368- either the list of names given, or the list of track indexes given."""
369- clipTable = {}
370- # READ ME IMPORTANT READ MEEEEEEE clipTable structure: {1:{"add": [], "edit": [], "same": [], "delete": []}
371- # clipTable keys are track numbers, values are dictionaries
372- # per track dictionary keys are clip categories, values are lists of clips of that category
360+ def categorizeClipsByTracks (tracksA , tracksB ):
361+ """Compare the clips in each track in tracksB against the corresponding track in tracksA
362+ and categorize based on how they have changed. Return a dictionary table of ClipDatas
363+ categorized by added, edited, unchanged, deleted, and moved and ordered by track.
364+
365+ Parameters:
366+ tracksA (list of otio.schema.Track() elements): list of tracks from timeline A
367+ tracksB (list of otio.schema.Track() elements): list of tracks from timeline B
368+
369+ Returns:
370+ clipTable (dictionary): dictionary holding categorized ClipDatas, organized by the track number of the ClipDatas
371+ dictionary keys: track number (int)
372+ dictionary values: dictionary holding categorized ClipDatas of that track
373+ nested dictionary keys: category name (string)
374+ nested dictionary values: list of ClipDatas that fall into the category
375+
376+ ex: clipTable when tracksA and tracksB contain 3 tracks
377+ {1 : {"add": [], "edit": [], "same": [], "delete": [], "move": []}
378+ 2 : {"add": [], "edit": [], "same": [], "delete": [], "move": []}
379+ 3 : {"add": [], "edit": [], "same": [], "delete": []}, "move": []}
380+ """
373381
382+ clipTable = {}
374383 # TODO? ^change to class perhaps? low priority
375384
376385 shorterTlTracks = tracksA if len (tracksA ) < len (tracksB ) else tracksB
@@ -428,6 +437,8 @@ def processTracks(tracksA, tracksB):
428437 return clipTable
429438
430439def makeSummary (clipTable , trackType , mode ):
440+ """Summarize what clips got changed and how they changed and print to console."""
441+
431442 print (trackType .upper (), "CLIPS" )
432443 print ("===================================" )
433444 print (" Overview Summary " )
@@ -464,10 +475,11 @@ def makeSummary(clipTable, trackType, mode):
464475 print (cat .upper (), ":" , len (clipGroup [cat ]))
465476 if cat != "same" :
466477 for i in clipGroup [cat ]:
467- print (i .name )
478+ print (i .name + ": " + i . note ) if i . note is not None else print ( i . name )
468479 print ("" )
469480
470- def makeTlSummary (timelineA , timelineB ):
481+ def makeTimelineSummary (timelineA , timelineB ):
482+ """Summarize information about the two timelines compared and print to console."""
471483 print ("Comparing Timeline B:" , timelineB .name , "vs" )
472484 print (" Timeline A:" , timelineA .name )
473485 print ("" )
@@ -494,31 +506,12 @@ def makeTlSummary(timelineA, timelineB):
494506 print ("" )
495507
496508''' ======= Notes =======
497- maybe can make use of algorithms.filter.filter_composition
498-
499- # a test using python difflib, prob not useful
500- # # find deltas of 2 files and print into html site
501- # d = HtmlDiff(wrapcolumn=100)
502- # diff = d.make_file(file1.splitlines(), file2.splitlines(), context=True)
503- # with open("diff.html", "w", encoding="utf-8") as f:
504- # f.write(diff)
505-
506- # s = SequenceMatcher(None, file1, file2)
507- # print(s.quick_ratio())
508-
509- # each one in new check with each one in old
510- # if everything matches, unchanged <- can't just check with first instance because might have added one before it
511- # if everything matches except for timeline position-> moved
512- # if length doesn't match, look for ordering? or just classify as added/deleted
513- # if counts of old and new dif then def add/deleted
514-
515-
516509 Test shot simple:
517- python ./src/getDif.py /Users/yingjiew/Documents/testDifFiles/h150_104a.105j_2025.04.04_ANIM-flat.otio /Users/yingjiew/Documents/testDifFiles/150_104a.105jD_2025.06.27-flat.otio
510+ /Users/yingjiew/Documents/testDifFiles/h150_104a.105j_2025.04.04_ANIM-flat.otio /Users/yingjiew/Documents/testDifFiles/150_104a.105jD_2025.06.27-flat.otio
518511
519512 Test seq matching edit's skywalker:
520- python ./src/getDif.py /Users/yingjiew/Folio/casa/Dream_EP101_2024.02.09_Skywalker_v3.0_ChangeNotes.Relinked.01.otio /Users/yingjiew/Folio/casa/Dream_EP101_2024.02.23_Skywalker_v4.0_ChangeNotes.otio
513+ /Users/yingjiew/Folio/casa/Dream_EP101_2024.02.09_Skywalker_v3.0_ChangeNotes.Relinked.01.otio /Users/yingjiew/Folio/casa/Dream_EP101_2024.02.23_Skywalker_v4.0_ChangeNotes.otio
521514
522515 Test shot multitrack:
523- python ./src/getDif.py /Users/yingjiew/Folio/edit-dept/More_OTIO/i110_BeliefSystem_2022.07.28_BT3.otio /Users/yingjiew/Folio/edit-dept/More_OTIO/i110_BeliefSystem_2023.06.09.otio
516+ /Users/yingjiew/Folio/edit-dept/More_OTIO/i110_BeliefSystem_2022.07.28_BT3.otio /Users/yingjiew/Folio/edit-dept/More_OTIO/i110_BeliefSystem_2023.06.09.otio
524517'''
0 commit comments