{"id":181,"date":"2025-12-21T16:55:18","date_gmt":"2025-12-21T16:55:18","guid":{"rendered":"https:\/\/www.visitnyc.store\/blog\/?p=181"},"modified":"2025-12-21T16:55:18","modified_gmt":"2025-12-21T16:55:18","slug":"back-to-topaz-for-sbs-vr180","status":"publish","type":"post","link":"https:\/\/www.visitnyc.store\/blog\/back-to-topaz-for-sbs-vr180\/","title":{"rendered":"Back to Topaz for SBS VR180"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The Topaz FFMPEG CLI is the best tool because the UI is &#8230; awful. I&#8217;m sure it has improved since the 2024 license that we are using, but a CLI is all we need.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s a nice utility for building the CLI commands for nodejs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ TvaiSettings: holds the Topaz model + slider values\nexport class TvaiSettings {\n  constructor({\n    model = 'prob-4',\n    scale = 0,\n    width = 8192,\n    height = 4096,\n    preblur = -0.220446,\n    noise = 0.39,\n    details = 0.42,\n    halo = 0.02,\n    blur = 0.39,\n    compression = 0.25,\n    blend = 0.2,\n    device = 0,\n    vram = 1,\n    instances = 1,\n  } = {}) {\n    this.model = model;\n    this.scale = scale;\n    this.width = width;\n    this.height = height;\n    this.preblur = preblur;\n    this.noise = noise;\n    this.details = details;\n    this.halo = halo;\n    this.blur = blur;\n    this.compression = compression;\n    this.blend = blend;\n    this.device = device;\n    this.vram = vram;\n    this.instances = instances;\n  }\n\n  \/\/ Build the tvai_up filter string for ffmpeg\n  toFilterString() {\n    const parts = &#91;\n      `model=${this.model}`,\n      `scale=${this.scale}`,\n      `w=${this.width}`,\n      `h=${this.height}`,\n      `preblur=${this.preblur}`,\n      `noise=${this.noise}`,\n      `details=${this.details}`,\n      `halo=${this.halo}`,\n      `blur=${this.blur}`,\n      `compression=${this.compression}`,\n      `blend=${this.blend}`,\n      `device=${this.device}`,\n      `vram=${this.vram}`,\n      `instances=${this.instances}`,\n    ];\n    return `tvai_up=${parts.join(':')}`;\n  }\n\n  \/\/ Build a human-readable metadata string similar to Topaz UI\n  toMetadataString() {\n    const pct = (v) => Math.round(v * 100);\n    const preblurVal = (this.preblur * 100).toFixed(4);\n\n    return &#91;\n      `Enhanced using ${this.model}`,\n      'mode: manual',\n      `revert compression at ${pct(this.compression)}`,\n      `recover details at ${pct(this.details)}`,\n      `sharpen at ${pct(this.blur)}`,\n      `reduce noise at ${pct(this.noise)}`,\n      `dehalo at ${pct(this.halo)}`,\n      `anti-alias\/deblur at ${preblurVal}`,\n      'focus fix Off',\n      `and recover original detail at ${pct(this.blend)}`,\n    ].join('; ');\n  }\n\n  \/\/ For saving to a DB\n  toJSON() {\n    return {\n      model: this.model,\n      scale: this.scale,\n      width: this.width,\n      height: this.height,\n      preblur: this.preblur,\n      noise: this.noise,\n      details: this.details,\n      halo: this.halo,\n      blur: this.blur,\n      compression: this.compression,\n      blend: this.blend,\n      device: this.device,\n      vram: this.vram,\n      instances: this.instances,\n    };\n  }\n\n  \/\/ For restoring from DB JSON\n  static fromJSON(json) {\n    return new TvaiSettings(json);\n  }\n}\n\n\/\/ TvaiCommandBuilder: builds the actual ffmpeg commands\nexport class TvaiCommandBuilder {\n  constructor({\n    ffmpegPath = '\/Applications\/Topaz Video AI.app\/Contents\/MacOS\/ffmpeg',\n    tvaiSettings = new TvaiSettings(),\n    swsFlags = 'spline+accurate_rnd+full_chroma_int',\n    videoCodec = 'prores_videotoolbox',\n    videoProfile = 'hq',\n    pixFmt = 'p210le',\n    movflags = 'frag_keyframe+empty_moov+delay_moov+use_metadata_tags+write_colr',\n  } = {}) {\n    this.ffmpegPath = ffmpegPath;\n    this.tvaiSettings = tvaiSettings;\n    this.swsFlags = swsFlags;\n    this.videoCodec = videoCodec;\n    this.videoProfile = videoProfile;\n    this.pixFmt = pixFmt;\n    this.movflags = movflags;\n  }\n\n  \/\/ Convenience for tweaking sliders programmatically\n  setTvaiSettings(partial) {\n    Object.assign(this.tvaiSettings, partial);\n    return this;\n  }\n\n  \/\/ Build a preview command (short segment, usually no audio)\n  buildPreview({\n    inputPath,\n    outputPath,\n    startSeconds = 0,\n    durationSeconds = 1.0,\n    includeAudio = false,\n  }) {\n    if (!inputPath || !outputPath) {\n      throw new Error('inputPath and outputPath are required');\n    }\n\n    const args = &#91;\n      '-hide_banner',\n      '-t',\n      String(durationSeconds),\n      '-ss',\n      String(startSeconds),\n      '-i',\n      inputPath,\n      '-flush_packets',\n      '1',\n      '-sws_flags',\n      this.swsFlags,\n      '-filter_complex',\n      this.tvaiSettings.toFilterString(),\n      '-fflags',\n      '+flush_packets',\n      '-c:v',\n      this.videoCodec,\n      '-profile:v',\n      this.videoProfile,\n      '-pix_fmt',\n      this.pixFmt,\n      '-allow_sw',\n      '1',\n    ];\n\n    if (!includeAudio) {\n      args.push('-an');\n    } else {\n      \/\/ Optional: preview with audio\n      args.push('-map', '0:a?', '-c:a', 'copy');\n    }\n\n    args.push(\n      '-map_metadata',\n      '0',\n      '-map_metadata:s:v',\n      '0:s:v',\n      '-fps_mode:v',\n      'passthrough',\n      '-movflags',\n      this.movflags,\n      '-bf',\n      '0',\n      '-metadata',\n      `videoai=${this.tvaiSettings.toMetadataString()}`,\n      outputPath\n    );\n\n    return { command: this.ffmpegPath, args };\n  }\n\n  \/\/ Build a trimmed preview of the untouched source\n  buildSourcePreview({\n    inputPath,\n    outputPath,\n    startSeconds = 0,\n    durationSeconds = 1.0,\n    includeAudio = false,\n  }) {\n    if (!inputPath || !outputPath) {\n      throw new Error('inputPath and outputPath are required');\n    }\n\n    const args = &#91;\n      '-hide_banner',\n      '-t',\n      String(durationSeconds),\n      '-ss',\n      String(startSeconds),\n      '-i',\n      inputPath,\n      '-flush_packets',\n      '1',\n      '-fflags',\n      '+flush_packets',\n      '-c:v',\n      this.videoCodec,\n      '-profile:v',\n      this.videoProfile,\n      '-pix_fmt',\n      this.pixFmt,\n      '-allow_sw',\n      '1',\n    ];\n\n    if (!includeAudio) {\n      args.push('-an');\n    } else {\n      args.push('-map', '0:a?', '-c:a', 'copy');\n    }\n\n    args.push(\n      '-map_metadata',\n      '0',\n      '-map_metadata:s:v',\n      '0:s:v',\n      '-fps_mode:v',\n      'passthrough',\n      '-movflags',\n      this.movflags,\n      '-bf',\n      '0',\n      outputPath\n    );\n\n    return { command: this.ffmpegPath, args };\n  }\n\n  \/\/ Build a full render command\n  buildFullRender({\n    inputPath,\n    outputPath,\n    copyAudio = true,\n  }) {\n    if (!inputPath || !outputPath) {\n      throw new Error('inputPath and outputPath are required');\n    }\n\n    const args = &#91;\n      '-hide_banner',\n      '-i',\n      inputPath,\n      '-sws_flags',\n      this.swsFlags,\n      '-filter_complex',\n      this.tvaiSettings.toFilterString(),\n      '-c:v',\n      this.videoCodec,\n      '-profile:v',\n      this.videoProfile,\n      '-pix_fmt',\n      this.pixFmt,\n      '-allow_sw',\n      '1',\n    ];\n\n    if (copyAudio) {\n      args.push(\n        '-map',\n        '0:a?',\n        '-map_metadata:s:a:0',\n        '0:s:a:0',\n        '-c:a',\n        'copy'\n      );\n    } else {\n      args.push('-an');\n    }\n\n    args.push(\n      '-map_metadata',\n      '0',\n      '-map_metadata:s:v',\n      '0:s:v',\n      '-fps_mode:v',\n      'passthrough',\n      '-movflags',\n      this.movflags,\n      '-bf',\n      '0',\n      '-metadata',\n      `videoai=${this.tvaiSettings.toMetadataString()}`,\n      outputPath\n    );\n\n    return { command: this.ffmpegPath, args };\n  }\n\n  \/\/ Serialize builder + settings to DB\n  toJSON() {\n    return {\n      ffmpegPath: this.ffmpegPath,\n      tvaiSettings: this.tvaiSettings.toJSON(),\n      swsFlags: this.swsFlags,\n      videoCodec: this.videoCodec,\n      videoProfile: this.videoProfile,\n      pixFmt: this.pixFmt,\n      movflags: this.movflags,\n    };\n  }\n\n  static fromJSON(json) {\n    return new TvaiCommandBuilder({\n      ffmpegPath: json.ffmpegPath,\n      tvaiSettings: TvaiSettings.fromJSON(json.tvaiSettings),\n      swsFlags: json.swsFlags,\n      videoCodec: json.videoCodec,\n      videoProfile: json.videoProfile,\n      pixFmt: json.pixFmt,\n      movflags: json.movflags,\n    });\n  }\n}\n\n\/\/ Optional helper if you want a printable shell command:\nexport function toShellString({ command, args }) {\n  const quote = (s) =>\n    \/&#91;^A-Za-z0-9_\\\/.\\-:+=]\/.test(s) ? `\"${String(s).replace(\/\"\/g, '\\\\\"')}\"` : s;\n  return &#91;quote(command), ...args.map(quote)].join(' ');\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">usage looks something like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\n\nconst builder = new TvaiCommandBuilder({\n    tvaiSettings: new TvaiSettings({\n      preblur: -0.220446,\n      noise: 0.39,\n      details: 0.42,\n      halo: 0.02,\n      blur: 0.39,\n      compression: 0.25,\n      blend: 0.2,\n    }),\n  });\n\n  const command = builder.buildPreview({\n    inputPath: source.path,\n    outputPath: previewFile.outputPath,\n    startSeconds: source.previewOptions.startSeconds,\n    durationSeconds: source.previewOptions.durationSeconds,\n    includeAudio: source.previewOptions.includeAudio,\n  });\n\n  await runCommand(command.command, command.args);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and with this we have scripted out a workflow that goes something like this for SBS VR180 shot with the Canon R5C:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Canon EOS VR Utility Export 8192&#215;4096 ProRes 442, CLOG3<\/li>\n\n\n\n<li>Import into Adobe Premiere, Override color to CLOG3, trim and mix audio as needed<\/li>\n\n\n\n<li>Export from Premiere to ProRes 442 HQ<\/li>\n\n\n\n<li>Run through Topaz with TvaiCommandBuilder<\/li>\n\n\n\n<li>Take final Topaz SBS video, run through avconvert or spatial command to get the final APMP or spatial video output<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This workflow has been working well, and the denoise and sharpening out of the Topaz workflow yields a better image on our test shots than just plain exporting from Premiere Pro -> RealESRGANx2_plus upscale (JPEG) -> re-stitch back to MV-HEVC.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Further testing is being done with taking the Topaz output and running that through the Upscaler, but strangely, new noise artifacts appear on the JPEG out after the upscaling &#8230;. another mystery for another day.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Topaz FFMPEG CLI is the best tool because the UI is &#8230; awful. I&#8217;m sure it has improved since the 2024 license that we are using, but a CLI is all we need. Here&#8217;s a nice utility for building the CLI commands for nodejs: usage looks something like: and with this we have scripted [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-181","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/posts\/181","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/comments?post=181"}],"version-history":[{"count":1,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/posts\/181\/revisions"}],"predecessor-version":[{"id":182,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/posts\/181\/revisions\/182"}],"wp:attachment":[{"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/media?parent=181"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/categories?post=181"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.visitnyc.store\/blog\/wp-json\/wp\/v2\/tags?post=181"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}