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 ForgeExport
|
||||
import ForgeOffload
|
||||
import ForgeProxy
|
||||
|
||||
@main
|
||||
struct Forge: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "forge",
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue