I was working on a lyric video using DaVinci Resolve and wanted a quick way to convert the Text+ elements (which displayed the lyrics) into a subtitle .SRT file. I always think it's good practice to include a subtitle file with YouTube videos whenever possible for all sorts of great reasons, so I wanted to see if there was a simple way to convert what was already in place instead of duplicating any extra work.
Thanks to this Blackmagic Forum post, Sergey Knyazkov put together the below Python script and that worked perfectly!
All of Text+ instances were converted into items on the Subtitle track and an .SRT file created.
Here's Sergey's instructions for use
Open the DaVinci Resolve console - Workspace - Console menu, you should be able to see debug messages there.
The result of the script should be a Subtitle Track on your timeline.
I've included the script below in case it is taken down from the forum at any point, and I would love to take a better look at it soon.
Great work Sergey!
#!/usr/bin/env python# -*- coding: utf-8 -*-"""Simplified Text+ to SRT Exporter for DaVinci Resolve==================================================Generates an SRT subtitle file from Text+ clips and applies it to the timeline.No GUI, runs in console, saves SRT to project cache path.
Features:- Extracts text from Text+ clips with optimized clip filtering- Converts frame-based timing to SRT timecode using timedelta- Handles timeline start timecode offset- Saves SRT to project cache path with timeline name and timestamp- Automatically imports SRT and applies it to a new subtitle track- Console output for file path and status
Requirements:- DaVinci Resolve 19+ (Free or Studio)- No external dependencies
Author: Based on script by Sergey KnyazkovLicense: MITVersion: 1.1.0Date: May 17, 2025"""
import osfrom datetime import datetime, timedelta
# Initialize Resolve APItry: # resolve = Resolve() project_manager = resolve.GetProjectManager() project = project_manager.GetCurrentProject() media_pool = project.GetMediaPool() timeline = project.GetCurrentTimeline() timeline_fps = float(project.GetSetting("timelineFrameRate") or "25") if project else 25.0except Exception as e: print(f"Error initializing Resolve API: {e}") resolve = None timeline_fps = 25.0
class SMPTE: """Handles conversion between frames and SMPTE timecode.""" def __init__(self): self.fps = 24 self.df = False
def gettc(self, frames): """Converts frame count to SMPTE timecode (HH:MM:SS:FF).""" frames = abs(frames) if not self.df: self.fps = int(round(self.fps)) frHour = self.fps * 3600 frMin = self.fps * 60 hr = int(frames // frHour) mn = int((frames - hr * frHour) // frMin) sc = int((frames - hr * frHour - mn * frMin) // self.fps) fr = int(round(frames - hr * frHour - mn * frMin - sc * self.fps)) return f"{hr:02d}:{mn:02d}:{sc:02d}:{fr:02d}" else: raise NotImplementedError("Drop-frame not supported")
def getframes(self, tc): """Converts SMPTE timecode to frame count.""" if int(tc[9:]) > self.fps: raise ValueError('SMPTE timecode to frame rate mismatch.', tc, self.fps) hours = int(tc[:2]) minutes = int(tc[3:5]) seconds = int(tc[6:8]) frames = int(tc[9:]) totalMinutes = int(60 * hours + minutes) self.fps = int(round(self.fps)) return int((totalMinutes * 60 + seconds) * self.fps + frames)
def frames_to_srt_timecode(frames, framerate): """Converts frames to SRT timecode format (HH:MM:SS,mmm) using timedelta.""" try: total_seconds = frames / framerate td = timedelta(seconds=total_seconds) hours, remainder = divmod(td.seconds, 3600) minutes, seconds = divmod(remainder, 60) milliseconds = int(td.microseconds / 1000) return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}" except Exception as e: print(f"Error converting to SRT timecode: {e}") return "00:00:00,000"
def get_text_plus_clips(smpte): """Extracts Text+ clips from the current timeline with optimized filtering.""" text_clips = [] framerate = get_timeline_fps() if not timeline: print("Error: No active timeline") return text_clips
for track_idx in range(1, timeline.GetTrackCount("video") + 1): try: if not timeline.GetIsTrackEnabled("video", track_idx): print(f"Skipping disabled video track {track_idx}") continue except AttributeError: print(f"Warning: GetIsTrackEnabled not supported for video track {track_idx}") continue
track_items = timeline.GetItemListInTrack("video", track_idx) if not track_items: continue
for tl_item in track_items: if not tl_item.GetClipEnabled(): continue if tl_item.GetFusionCompCount() == 0: continue # Skip clips without Fusion compositions start_frame = tl_item.GetStart() clip_name = tl_item.GetName() if hasattr(tl_item, 'GetName') else 'N/A' clip_id = f"{start_frame}_{tl_item.GetDuration()}_{track_idx}"
for comp_idx in range(1, tl_item.GetFusionCompCount() + 1): comp = tl_item.GetFusionCompByIndex(comp_idx) if not comp: continue tool_list = comp.GetToolList(False) for node in tool_list.values(): try: attrs = node.GetAttrs() reg_id = attrs.get('TOOLS_RegID', '') if reg_id == 'TextPlus': text = str(node.StyledText[1]) if hasattr(node.StyledText, "__getitem__") else str(node.StyledText) if text and text.lower() != "none": text_clips.append({ "type": "Text+", "start_frame": start_frame, "text": text.strip(), "framerate": framerate, "duration": tl_item.GetDuration(), "timecode": smpte.gettc(start_frame), "clip_name": clip_name, "track_idx": track_idx, "clip_id": clip_id }) except Exception as e: print(f"Error processing tool on track {track_idx}, clip {clip_name}: {str(e)}") continue
print(f"Found {len(text_clips)} Text+ clips") return text_clips
def get_timeline_fps(): """Retrieves the exact FPS of the current timeline.""" if not timeline: return 25.0 try: fps_str = timeline.GetSetting("timelineFrameRate") if fps_str: return float(fps_str) return float(project.GetSetting("timelineFrameRate") or 25.0) except Exception as e: print(f"Error getting timeline FPS: {e}") return 25.0
def import_and_apply_srt(srt_path, timeline_start_frame): """Imports an SRT file to the media pool and applies it to a new subtitle track.""" try: if not media_pool or not timeline: print("Error: Media Pool or Timeline not accessible") return False
# Import SRT to the root folder of the media pool import_options = { "ImportAsSubtitle": True, "TargetFolder": media_pool.GetRootFolder() } imported_items = media_pool.ImportMedia([srt_path], import_options) if not imported_items: print("Error: Failed to import SRT file") return False
srt_clip = imported_items[0] print(f"Imported SRT clip: {srt_clip.GetName()}")
# Disable all existing subtitle tracks subtitle_track_count = timeline.GetTrackCount("subtitle") for track_index in range(1, subtitle_track_count + 1): try: success = timeline.SetTrackEnable("subtitle", track_index, False) print(f"Disabled subtitle track {track_index}: {success}") except Exception as e: print(f"Error disabling subtitle track {track_index}: {str(e)}")
# Create a new subtitle track new_track_index = subtitle_track_count + 1 timeline.AddTrack("subtitle") success = timeline.SetTrackEnable("subtitle", new_track_index, True) if not success: print(f"Error: Failed to enable new subtitle track {new_track_index}") return False print(f"Created and enabled new subtitle track {new_track_index}")
# Set the timeline cursor to the start timeline_start_tc = timeline.GetStartTimecode() or "00:00:00:00" timeline.SetCurrentTimecode(timeline_start_tc)
# Append SRT to the timeline append_data = [{ 'mediaPoolItem': srt_clip, 'startFrame': 0, 'mediaType': "subtitle", 'recordFrame': 0 }] append_result = media_pool.AppendToTimeline(append_data) if append_result: print(f"SRT applied to timeline on track {new_track_index}") # Remove the imported clip from the media pool media_pool.DeleteClips([srt_clip]) return True else: print("Error: Failed to append SRT to timeline") return False
except Exception as e: print(f"Error applying SRT: {str(e)}") return False
def main(): """Main function to generate and apply SRT file.""" if resolve is None: print("Error: This script must be run from within DaVinci Resolve") return
if not timeline: print("Error: No active timeline found") return
# Initialize SMPTE smpte = SMPTE() smpte.fps = get_timeline_fps() timeline_start_tc = timeline.GetStartTimecode() or "00:00:00:00" timeline_start_frame = smpte.getframes(timeline_start_tc)
# Determine output path using project cache path cache_path = project.GetSetting("cachePath") or os.path.expanduser("~/Desktop") timeline_name = timeline.GetName() or "subtitles" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_filename = f"{timeline_name}_{timestamp}.srt" output_path = os.path.join(cache_path, output_filename)
print(f"Collecting Text+ clips...") clips = get_text_plus_clips(smpte) if not clips: print("No Text+ clips found") return
print(f"Processing {len(clips)} clips...") clips = [clip for clip in clips if clip["type"] == "Text+"] clips.sort(key=lambda x: x["start_frame"])
# Generate SRT file try: with open(output_path, "w", encoding="utf-8") as f: for i, clip in enumerate(clips, 1): relative_start_frame = max(0, clip["start_frame"] - timeline_start_frame) relative_end_frame = max(0, clip["start_frame"] + clip["duration"] - timeline_start_frame) start_timecode = frames_to_srt_timecode(relative_start_frame, clip["framerate"]) end_timecode = frames_to_srt_timecode(relative_end_frame, clip["framerate"]) text = clip["text"].replace("\u2028", "\n").replace("\u2029", "\n") lines = [line for line in text.splitlines() if line.strip()] final_text = "\n".join(lines) f.write(f"{i}\n") f.write(f"{start_timecode} --> {end_timecode}\n") f.write(f"{final_text}\n\n")
print(f"SRT file saved to: {output_path}")
# Apply SRT to timeline if import_and_apply_srt(output_path, timeline_start_frame): print("Success: SRT applied to timeline") else: print("Warning: SRT saved but not applied to timeline")
except Exception as e: print(f"Error: {e}")
if __name__ == "__main__": main()