cli: forge proxy — per-shot proxy render from library, source matching, grade bake + text burn flags
This commit is contained in:
parent
b7d43785cf
commit
92a29d5a60
1 changed files with 82 additions and 1 deletions
|
|
@ -8,13 +8,94 @@ import ForgeSim
|
||||||
import ForgeLibrary
|
import ForgeLibrary
|
||||||
import ForgeExport
|
import ForgeExport
|
||||||
import ForgeOffload
|
import ForgeOffload
|
||||||
|
import ForgeProxy
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Forge: AsyncParsableCommand {
|
struct Forge: AsyncParsableCommand {
|
||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
commandName: "forge",
|
commandName: "forge",
|
||||||
abstract: "On-set live grading + DIT toolkit: camera look push, clip logging, offload, proxies.",
|
abstract: "On-set live grading + DIT toolkit: camera look push, clip logging, offload, proxies.",
|
||||||
subcommands: [Push.self, Status.self, Sim.self, Export.self, Offload.self, Verify.self])
|
subcommands: [Push.self, Status.self, Sim.self, Export.self, Offload.self, Verify.self, Proxy.self])
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Proxy: AsyncParsableCommand {
|
||||||
|
static let configuration = CommandConfiguration(abstract: "Render proxies for a day's shots, optionally with grade + burn-ins.")
|
||||||
|
|
||||||
|
@Option(name: .long, help: "Library database path")
|
||||||
|
var db: String
|
||||||
|
|
||||||
|
@Option(name: .long, help: "Day label")
|
||||||
|
var day: String
|
||||||
|
|
||||||
|
@Option(name: .long, help: "Directory containing source clips (matched by clip name)")
|
||||||
|
var media: String
|
||||||
|
|
||||||
|
@Option(name: .long, help: "Output directory")
|
||||||
|
var out: String
|
||||||
|
|
||||||
|
@Option(name: .long, help: "Preset: prores-proxy|dnxhr-lb|h264")
|
||||||
|
var preset: String = "h264"
|
||||||
|
|
||||||
|
@Option(name: .long, help: "Scale: full|half|quarter")
|
||||||
|
var scale: String = "half"
|
||||||
|
|
||||||
|
@Flag(name: .long, help: "Bake shot grade into proxy")
|
||||||
|
var burnLook = false
|
||||||
|
|
||||||
|
@Flag(name: .long, help: "Burn clip name + timecode overlay")
|
||||||
|
var burnText = false
|
||||||
|
|
||||||
|
@Option(name: .long, help: "ffmpeg binary path")
|
||||||
|
var ffmpeg: String = "/usr/bin/ffmpeg"
|
||||||
|
|
||||||
|
func run() async throws {
|
||||||
|
guard let presetVal = ProxyPreset(rawValue: preset) else {
|
||||||
|
throw ValidationError("unknown preset '\(preset)'")
|
||||||
|
}
|
||||||
|
guard let scaleVal = ProxyScale(rawValue: scale) else {
|
||||||
|
throw ValidationError("unknown scale '\(scale)'")
|
||||||
|
}
|
||||||
|
let store = try LibraryStore(path: db)
|
||||||
|
var target: ForgeLibrary.Day?
|
||||||
|
for p in try store.listProjects() {
|
||||||
|
if let d = try store.listDays(projectID: p.id).first(where: { $0.label == day }) {
|
||||||
|
target = d
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let target else { throw ValidationError("day '\(day)' not found") }
|
||||||
|
let shots = try store.listShots(dayID: target.id)
|
||||||
|
try FileManager.default.createDirectory(atPath: out, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
var rendered = 0
|
||||||
|
var missing = 0
|
||||||
|
for shot in shots {
|
||||||
|
// Find source by clip name with common extensions.
|
||||||
|
let candidates = ["mov", "mxf", "mp4", "braw", "arriraw"].map {
|
||||||
|
"\(media)/\(shot.clipName).\($0)"
|
||||||
|
}
|
||||||
|
guard let input = candidates.first(where: { FileManager.default.fileExists(atPath: $0) }) else {
|
||||||
|
print("skip \(shot.clipName): no source in \(media)")
|
||||||
|
missing += 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let ext = presetVal == .h264 ? "mp4" : (presetVal == .dnxhrLB ? "mxf" : "mov")
|
||||||
|
let job = ProxyJob(
|
||||||
|
input: input,
|
||||||
|
output: "\(out)/\(shot.clipName).\(ext)",
|
||||||
|
preset: presetVal,
|
||||||
|
scale: scaleVal,
|
||||||
|
gradeSnapshot: burnLook ? shot.gradeSnapshot : nil,
|
||||||
|
burnIn: burnText ? BurnIn(
|
||||||
|
clipName: shot.clipName,
|
||||||
|
timecode: shot.timecodeIn ?? "00:00:00:00",
|
||||||
|
fps: Int(shot.fps ?? 24)) : nil)
|
||||||
|
try await job.run(ffmpegPath: ffmpeg)
|
||||||
|
print("rendered \(shot.clipName)")
|
||||||
|
rendered += 1
|
||||||
|
}
|
||||||
|
print("proxies: \(rendered) rendered, \(missing) missing sources")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Offload: AsyncParsableCommand {
|
struct Offload: AsyncParsableCommand {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue