cli: forge push/status/sim — flatten+push looks over IP, split-CDL flag, live sim; verified end-to-end

This commit is contained in:
Forge Dev 2026-07-10 19:33:31 +00:00
parent 5730fb53a2
commit 642af95d4e

View file

@ -1 +1,115 @@
@main struct ForgeCLI { static func main() { print("forge") } }
import ArgumentParser
import Foundation
import ForgeCamera
import ForgeCameraARRI
import ForgeGrade
import ForgeColor
import ForgeSim
@main
struct Forge: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "forge",
abstract: "On-set live grading: push looks to cameras over IP.",
subcommands: [Push.self, Status.self, Sim.self])
}
struct CameraOptions: ParsableArguments {
@Option(name: .long, help: "Camera IP/host")
var host: String
@Option(name: .long, help: "Camera port")
var port: Int
@Option(name: .long, help: "Driver: arri")
var driver: String = "arri"
func makeDriver() throws -> any CameraDriver {
switch driver {
case "arri":
return ArriDriver(host: host, port: port)
default:
throw ValidationError("unknown driver '\(driver)' (supported: arri)")
}
}
}
struct Push: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "Flatten a grade stack and push to camera.")
@OptionGroup var camera: CameraOptions
@Option(name: .long, help: "Grade snapshot JSON file")
var look: String
@Option(name: .long, help: "LUT lattice size")
var lattice: Int = 33
@Flag(name: .long, help: "Keep leading CDL native (camera trim), bake remainder")
var splitCdl = false
func run() async throws {
let data = try Data(contentsOf: URL(fileURLWithPath: look))
let stack = try GradeSnapshot.decode(data)
let driver = try camera.makeDriver()
let flattened = Flattener.flatten(
stack: stack,
latticeSize: lattice,
splitLeadingCDL: splitCdl && driver.capabilities.supportsNativeCDL)
try await driver.connect()
try await driver.push(look: flattened)
await driver.disconnect()
print("pushed: lattice=\(lattice) cdl=\(flattened.cdl != nil ? "native" : "baked") -> \(camera.host):\(camera.port)")
}
}
struct Status: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "Print camera connection/rec state and metadata.")
@OptionGroup var camera: CameraOptions
func run() async throws {
let driver = try camera.makeDriver()
var iterator = driver.state.makeAsyncIterator()
try await driver.connect()
// First polled state carries metadata.
var reported = false
for _ in 0..<10 {
guard let s = await iterator.next() else { break }
if let md = s.metadata {
print("connection: \(s.connection.rawValue)")
print("recording: \(s.isRecording)")
print("clip: \(s.clipName ?? "-")")
print("ei: \(md.exposureIndex.map(String.init) ?? "-") wb: \(md.whiteBalance.map(String.init) ?? "-") tint: \(md.tint.map(String.init) ?? "-")")
print("fps: \(md.fps.map { String($0) } ?? "-") tc: \(md.timecode ?? "-")")
reported = true
break
}
}
if !reported {
print("connection: connected (no metadata yet)")
}
await driver.disconnect()
}
}
struct Sim: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "Run an ARRI-style camera simulator.")
@Option(name: .long, help: "Listen port")
var port: Int = 8035
func run() async throws {
let sim = ArriSimulator()
try await sim.start(port: port)
let bound = await sim.boundPort ?? port
print("arri sim listening on 127.0.0.1:\(bound)")
// Run until killed.
while true {
try await Task.sleep(for: .seconds(3600))
}
}
}