Add Mimir output controls

This commit is contained in:
Meta Vens 2026-06-01 14:43:12 +01:00
commit f083a4fbae
8 changed files with 321 additions and 66 deletions

View file

@ -281,17 +281,22 @@ a named invariant that the native runtime cannot protect yet.
with preset paths and strength; until the renderer grows LUT texture sampling,
preset exposure and bloom are mapped into existing `GraphicsSettings`.
`Mimir.BufferSmoke --presentation-control-smoke` proves the state owner.
- `MimirProgramRecorder` is the first app-owned recording control. The Mimir UI
exposes `Record` / `Stop` in the Video Sources rail, tracks child-process
status, writes the latest MP4 under `artifacts/runtime/stream-proof/`,
and kills the process tree on stop. The current actuator deliberately delegates
to `scripts/record-stream-proof-output.ps1`, so the app records the same
stream-proof composite intended for the Yggdrasil/public path: Raven program
feed, Kiyo picture-in-picture, Starfire Scarlett audio, and NVENC/AAC
broadcast settings. `MIMIR_RECORD_START_RAVEN=1` lets the app ask the script
to start Raven's sender; otherwise it records from the already-running feed.
Native program-output recording can replace that actuator later behind the
same UI owner.
- The right rail `Output` panel owns OBS-like program actions below the
inspector. `MimirProgramRecorder` exposes `Record` / `Stop`, tracks
child-process status, writes timestamped MP4 receipts under
`C:\Users\Meta\Videos\Mimir` by default, and kills the process tree on stop.
`MIMIR_RECORD_OUTPUT_DIR` can redirect receipts. The current actuator
deliberately delegates to `scripts/record-stream-proof-output.ps1`, so app
recording produces the same stream-proof composite intended for the
Yggdrasil/public path: Raven program feed, Kiyo picture-in-picture, Starfire
Scarlett audio, and NVENC/AAC broadcast settings.
`MIMIR_RECORD_START_RAVEN=1` lets the app ask the script to start Raven's
sender; otherwise it records from the already-running feed. `MimirProgramStreamer`
exposes `Live` / `Stop` for the configured `Mimir.Broadcast` RTMP path, but it
refuses to publish unless `MIMIR_STREAM_INPUT` is set or
`MIMIR_STREAM_ALLOW_TEST_PATTERN=1` explicitly authorizes a synthetic smoke.
Native program-output recording/streaming can replace those actuators later
behind the same UI owner.
- `MimirSceneEditorState` owns the new Mimir-window editor graph. It is not the
OBS program output: it owns editor camera, selected node, sensor-feed panels,
SDF text-panel nodes, model placeholders, visibility, locks, transforms, reset

View file

@ -346,16 +346,19 @@ proves the boundary: alignment frames queue the cleaner DSP and input block, and
cleaner output frames are what enter `NativeProgram` stem publication. This is
the first hot-lane cleaner socket, not proof that the final studio denoiser is
solved.
`MimirProgramRecorder` is the app feature for local broadcast-proof recording,
not for the isolated Kiyo/Faust mic receipt. The Video Sources rail exposes
`Record` / `Stop`; the recorder owns child-process status, latest output path,
and process-tree shutdown. Its current actuator is
`scripts/record-stream-proof-output.ps1`, so app recording produces the same
stream-proof composite intended for Yggdrasil/public judgment: Raven program
feed, Kiyo picture-in-picture, Starfire Scarlett audio, and NVENC/AAC broadcast
settings under `artifacts/runtime/stream-proof/`. `MIMIR_RECORD_START_RAVEN=1`
asks the app to start Raven's sender for the receipt. Later native recording
should replace the actuator behind this owner, not add another UI truth.
The right rail `Output` panel is the app feature for local broadcast-proof
recording and live publish control, not the isolated Kiyo/Faust mic receipt.
`MimirProgramRecorder` owns `Record` / `Stop`, child-process status, timestamped
MP4 receipts under `C:\Users\Meta\Videos\Mimir` by default, and process-tree
shutdown. Its current actuator is `scripts/record-stream-proof-output.ps1`, so
app recording produces the same stream-proof composite intended for
Yggdrasil/public judgment: Raven program feed, Kiyo picture-in-picture, Starfire
Scarlett audio, and NVENC/AAC broadcast settings. `MIMIR_RECORD_START_RAVEN=1`
asks the app to start Raven's sender for the receipt. `MimirProgramStreamer`
owns `Live` / `Stop` for the `Mimir.Broadcast` RTMP path; it requires
`MIMIR_STREAM_INPUT` unless `MIMIR_STREAM_ALLOW_TEST_PATTERN=1` explicitly
authorizes a synthetic smoke stream. Later native recording/streaming should
replace these actuators behind this owner, not add another UI truth.
`MimirTargetedBioacousticProbePlanner` is the matching "ask the room a better
question" owner. It consumes per-source spectral residuals and optional source
pathology notes, then emits gain-bounded shaped probe bands for the

View file

@ -175,15 +175,18 @@ Get-Content .\state\evidence.jsonl -Tail 8
Fresh run `20260601-051044` measured cardioid delay 359.398 samples
(7487.452 us) at 48 kHz and applied holdback 359.398/0.000 to
shotgun/cardioid before cleanup.
- `MimirProgramRecorder` exposes local broadcast-proof recording as a Mimir app
feature. The Video Sources rail has `Record` / `Stop`, status text, and latest
output under
`artifacts/runtime/stream-proof/mimir-program-record-latest.mp4`. It currently
delegates to `scripts/record-stream-proof-output.ps1`, so the app records the
same Raven + Kiyo PIP + Starfire Scarlett NVENC/AAC composite intended for
- The right rail `Output` panel now exposes OBS-like program controls below the
inspector. `MimirProgramRecorder` owns `Record` / `Stop`, status text, and
timestamped receipts under `C:\Users\Meta\Videos\Mimir` by default
(`MIMIR_RECORD_OUTPUT_DIR` overrides). It delegates to
`scripts/record-stream-proof-output.ps1`, so the app records the same Raven +
Kiyo PIP + Starfire Scarlett NVENC/AAC composite intended for
Yggdrasil/public judgment. `MIMIR_RECORD_START_RAVEN=1` asks it to launch the
Raven sender too. Native program-output recording should replace that
actuator behind the same owner.
Raven sender too. `MimirProgramStreamer` owns `Live` / `Stop` for
`Mimir.Broadcast`; it requires `MIMIR_STREAM_INPUT` unless
`MIMIR_STREAM_ALLOW_TEST_PATTERN=1` explicitly allows a synthetic smoke.
Native program-output recording/streaming should replace those actuators
behind the same owner.
- OBS receives final program surfaces; it does not own synchronization.
## Current Pressure

View file

@ -45,6 +45,7 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
private readonly MimirPresentationControlState presentationControls = new();
private readonly MimirSceneEditorState sceneEditor = new();
private readonly MimirProgramRecorder programRecorder = new();
private readonly MimirProgramStreamer programStreamer = new();
private readonly MimirProgramSurfaceConfigDocument programSurfaceConfig;
private readonly MimirObsStemPublicationState obsStemPublication = new(MimirObsPublicationConfigurations.NativeProgram);
private readonly MimirObsStemSharedMemoryPublisher? obsStemPublisher;
@ -827,6 +828,7 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
UpdateAudioSpectra();
UpdateAudioSynchronization();
programRecorder.Refresh();
programStreamer.Refresh();
EmitTelemetry();
}
@ -858,6 +860,7 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
{
obsStemPublisher?.Dispose();
programRecorder.Dispose();
programStreamer.Dispose();
synchronization.Dispose();
}
@ -871,12 +874,6 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
shell.Pane("mimir.video-sources", "Video Sources", graph =>
{
graph.Text("video.program-summary", DescribeProgramVideo, "caption", weight: 0.75f);
graph.Row("video.program-recording", row =>
{
row.Button("program.record", "Record", StartProgramRecording, weight: 0.7f);
row.Button("program.record-stop", "Stop", programRecorder.Stop, weight: 0.55f);
row.Text("program.record-status", programRecorder.Describe, "caption", weight: 1.65f);
}, weight: 0.65f);
graph.Text("video.source-list-title", "Program Sources", "strong", weight: 0.55f);
for (var slot = 0; slot < VideoSourceListSlotCount; slot++)
{
@ -926,34 +923,56 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
}, weight: 0.95f);
}, weight: 1.75f);
shell.Pane("mimir.inspector", "Inspector", inspector =>
shell.Vertical("mimir.right-rail", right =>
{
inspector.Text("inspector.selection", sceneEditor.DescribeSelection, "strong");
inspector.Options("inspector.video-feed", "Feed", () => presentationControls.SelectedVideoIndex, SelectVideoLayer, VideoFeedOptions());
inspector.Vertical("inspector.source-controls", source =>
right.Pane("mimir.inspector", "Inspector", inspector =>
{
source.Toggle("video.visible", "Visible", () => sceneEditor.SelectedNode?.Visible ?? false, SetSelectedVideoVisible);
source.Toggle("video.solo", "Solo", () => presentationControls.SelectedVideo?.Solo ?? false, SetSelectedVideoSolo);
source.Slider("video.opacity", "Opacity", () => presentationControls.SelectedVideo?.Opacity ?? 1.0f, SetSelectedVideoOpacity, 0.0f, 1.0f, "0.00");
source.Text("inspector.program-placement", "Program Frame", "strong");
source.Slider("program.x", "X", () => sceneEditor.SelectedProgramX, sceneEditor.SetSelectedProgramX, 0.0f, 1.0f, "0.000");
source.Slider("program.y", "Y", () => sceneEditor.SelectedProgramY, sceneEditor.SetSelectedProgramY, 0.0f, 1.0f, "0.000");
source.Slider("program.w", "W", () => sceneEditor.SelectedProgramWidth, sceneEditor.SetSelectedProgramWidth, 0.01f, 1.0f, "0.000");
source.Slider("program.h", "H", () => sceneEditor.SelectedProgramHeight, sceneEditor.SetSelectedProgramHeight, 0.01f, 1.0f, "0.000");
source.Slider("program.rotation", "Rotation", () => sceneEditor.SelectedNode?.Transform.RotationRadians ?? 0.0f, sceneEditor.SetSelectedRotation, -MathF.PI, MathF.PI, "0.00");
source.Options("program.fit-mode", "Fit", () => sceneEditor.SelectedFitModeIndex, sceneEditor.SetSelectedFitModeIndex, sceneEditor.FitModeOptions);
source.Row("program.fit", fit =>
inspector.Text("inspector.selection", sceneEditor.DescribeSelection, "strong");
inspector.Options("inspector.video-feed", "Feed", () => presentationControls.SelectedVideoIndex, SelectVideoLayer, VideoFeedOptions());
inspector.Vertical("inspector.source-controls", source =>
{
fit.Button("program.center", "Center", sceneEditor.CenterSelectedProgramLayer);
fit.Button("program.fit-selected", "Fit", sceneEditor.FitSelectedProgramLayer);
fit.Button("program.fill-selected", "Fill", sceneEditor.FillSelectedProgramLayer);
});
source.Row("program.order", order =>
source.Toggle("video.visible", "Visible", () => sceneEditor.SelectedNode?.Visible ?? false, SetSelectedVideoVisible);
source.Toggle("video.solo", "Solo", () => presentationControls.SelectedVideo?.Solo ?? false, SetSelectedVideoSolo);
source.Slider("video.opacity", "Opacity", () => presentationControls.SelectedVideo?.Opacity ?? 1.0f, SetSelectedVideoOpacity, 0.0f, 1.0f, "0.00");
source.Text("inspector.program-placement", "Program Frame", "strong");
source.Slider("program.x", "X", () => sceneEditor.SelectedProgramX, sceneEditor.SetSelectedProgramX, 0.0f, 1.0f, "0.000");
source.Slider("program.y", "Y", () => sceneEditor.SelectedProgramY, sceneEditor.SetSelectedProgramY, 0.0f, 1.0f, "0.000");
source.Slider("program.w", "W", () => sceneEditor.SelectedProgramWidth, sceneEditor.SetSelectedProgramWidth, 0.01f, 1.0f, "0.000");
source.Slider("program.h", "H", () => sceneEditor.SelectedProgramHeight, sceneEditor.SetSelectedProgramHeight, 0.01f, 1.0f, "0.000");
source.Slider("program.rotation", "Rotation", () => sceneEditor.SelectedNode?.Transform.RotationRadians ?? 0.0f, sceneEditor.SetSelectedRotation, -MathF.PI, MathF.PI, "0.00");
source.Options("program.fit-mode", "Fit", () => sceneEditor.SelectedFitModeIndex, sceneEditor.SetSelectedFitModeIndex, sceneEditor.FitModeOptions);
source.Row("program.fit", fit =>
{
fit.Button("program.center", "Center", sceneEditor.CenterSelectedProgramLayer);
fit.Button("program.fit-selected", "Fit", sceneEditor.FitSelectedProgramLayer);
fit.Button("program.fill-selected", "Fill", sceneEditor.FillSelectedProgramLayer);
});
source.Row("program.order", order =>
{
order.Button("program.layer-up", "Layer Up", () => MoveSelectedVideoLayer(-1));
order.Button("program.layer-down", "Layer Down", () => MoveSelectedVideoLayer(1));
});
}, isVisible: () => sceneEditor.HasSelectedProgramSource);
}, weight: 1.05f);
right.Pane("mimir.output", "Output", output =>
{
output.Text("output.program-summary", DescribeProgramOutput, "caption", weight: 0.6f);
output.Row("output.recording", row =>
{
order.Button("program.layer-up", "Layer Up", () => MoveSelectedVideoLayer(-1));
order.Button("program.layer-down", "Layer Down", () => MoveSelectedVideoLayer(1));
});
}, isVisible: () => sceneEditor.HasSelectedProgramSource);
row.Button("program.record", "Record", StartProgramRecording, weight: 0.7f);
row.Button("program.record-stop", "Stop", programRecorder.Stop, weight: 0.55f);
row.Text("program.record-status", programRecorder.Describe, "caption", weight: 1.75f);
}, weight: 0.7f);
output.Text("output.record-path", () => $"folder {programRecorder.OutputDirectory}", "caption", weight: 0.7f);
output.Row("output.streaming", row =>
{
row.Button("program.stream", "Live", StartProgramStreaming, weight: 0.7f);
row.Button("program.stream-stop", "Stop", programStreamer.Stop, weight: 0.55f);
row.Text("program.stream-status", programStreamer.Describe, "caption", weight: 1.75f);
}, weight: 0.7f);
output.Text("output.stream-target", () => $"target {programStreamer.TargetUrl}", "caption", weight: 0.7f);
}, weight: 0.95f);
}, weight: 0.95f);
});
});
@ -1135,6 +1154,11 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
programRecorder.Start();
}
private void StartProgramStreaming()
{
programStreamer.Start();
}
private string DescribeVideoSourceSlot(int index)
{
var feeds = presentationControls.VideoFeeds;
@ -1320,6 +1344,13 @@ public sealed class MimirRuntime : IAquariumRuntime, IAquariumRuntimeServicesRec
return string.Join(" | ", active);
}
private string DescribeProgramOutput()
{
var videoCount = presentationControls.VideoFeeds.Count(feed => presentationControls.IncludesVideo(feed.SourceId));
var audioCount = presentationControls.AudioFeeds.Count;
return $"program {videoCount} video / {audioCount} audio";
}
private string DescribePostprocess()
{
var post = presentationControls.Postprocess;

View file

@ -15,13 +15,16 @@ public sealed class MimirProgramRecorder : IDisposable
this.repositoryRoot = string.IsNullOrWhiteSpace(repositoryRoot)
? ResolveRepositoryRoot()
: Path.GetFullPath(repositoryRoot);
LatestOutputPath = Path.Combine(this.repositoryRoot, "artifacts", "runtime", "stream-proof", "mimir-program-record-latest.mp4");
OutputDirectory = ResolveOutputDirectory();
LatestOutputPath = CreateDefaultOutputPath(OutputDirectory);
}
public bool IsRecording => process is { HasExited: false };
public string LatestOutputPath { get; private set; }
public string OutputDirectory { get; }
public string Describe()
{
Refresh();
@ -53,7 +56,7 @@ public sealed class MimirProgramRecorder : IDisposable
var duration = Math.Max(1, durationSeconds ?? ReadDurationSeconds());
LatestOutputPath = Path.GetFullPath(string.IsNullOrWhiteSpace(outputPath)
? Path.Combine(repositoryRoot, "artifacts", "runtime", "stream-proof", "mimir-program-record-latest.mp4")
? CreateDefaultOutputPath(OutputDirectory)
: Path.IsPathRooted(outputPath) ? outputPath : Path.Combine(repositoryRoot, outputPath));
var logRoot = Path.Combine(repositoryRoot, "artifacts", "runtime", "stream-proof");
Directory.CreateDirectory(logRoot);
@ -164,6 +167,32 @@ public sealed class MimirProgramRecorder : IDisposable
: DefaultDurationSeconds;
}
private static string ResolveOutputDirectory()
{
var configured = Environment.GetEnvironmentVariable("MIMIR_RECORD_OUTPUT_DIR");
if (!string.IsNullOrWhiteSpace(configured))
{
return Path.GetFullPath(configured);
}
var videos = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos);
if (string.IsNullOrWhiteSpace(videos))
{
var profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
videos = string.IsNullOrWhiteSpace(profile)
? @"C:\Users\Meta\Videos"
: Path.Combine(profile, "Videos");
}
return Path.Combine(videos, "Mimir");
}
private static string CreateDefaultOutputPath(string outputDirectory)
{
var stamp = DateTimeOffset.Now.ToString("yyyyMMdd-HHmmss", System.Globalization.CultureInfo.InvariantCulture);
return Path.Combine(outputDirectory, $"mimir-program-{stamp}.mp4");
}
private static bool IsTruthy(string? value) =>
!string.IsNullOrWhiteSpace(value) &&
(string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) ||

View file

@ -0,0 +1,179 @@
using System.Diagnostics;
namespace Mimir.Runtime.Synchronization;
public sealed class MimirProgramStreamer : IDisposable
{
private readonly string repositoryRoot;
private Process? process;
private string status = "idle";
private DateTimeOffset? startedAt;
public MimirProgramStreamer(string? repositoryRoot = null)
{
this.repositoryRoot = string.IsNullOrWhiteSpace(repositoryRoot)
? ResolveRepositoryRoot()
: Path.GetFullPath(repositoryRoot);
}
public bool IsStreaming => process is { HasExited: false };
public string TargetUrl => Environment.GetEnvironmentVariable("MIMIR_STREAM_TARGET")
?? "rtmp://127.0.0.1:11935/live/mimir";
public string Describe()
{
Refresh();
if (IsStreaming)
{
var elapsed = startedAt == null
? TimeSpan.Zero
: DateTimeOffset.Now - startedAt.Value;
return $"live {elapsed.TotalSeconds:0}s pid={process!.Id}";
}
return status;
}
public bool Start()
{
Refresh();
if (IsStreaming)
{
return false;
}
var projectPath = Path.Combine(repositoryRoot, "src", "Mimir.Broadcast", "Mimir.Broadcast.csproj");
if (!File.Exists(projectPath))
{
status = "broadcast project missing";
return false;
}
var inputPath = Environment.GetEnvironmentVariable("MIMIR_STREAM_INPUT");
var allowTestPattern = IsTruthy(Environment.GetEnvironmentVariable("MIMIR_STREAM_ALLOW_TEST_PATTERN"));
if (string.IsNullOrWhiteSpace(inputPath) && !allowTestPattern)
{
status = "stream input missing: set MIMIR_STREAM_INPUT";
return false;
}
var logRoot = Path.Combine(repositoryRoot, "artifacts", "runtime", "stream-proof");
Directory.CreateDirectory(logRoot);
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = repositoryRoot,
};
startInfo.ArgumentList.Add("run");
startInfo.ArgumentList.Add("--project");
startInfo.ArgumentList.Add(projectPath);
startInfo.ArgumentList.Add("--");
startInfo.ArgumentList.Add("--target");
startInfo.ArgumentList.Add(TargetUrl);
if (!string.IsNullOrWhiteSpace(inputPath))
{
startInfo.ArgumentList.Add("--no-test-pattern");
startInfo.ArgumentList.Add("--input");
startInfo.ArgumentList.Add(Path.GetFullPath(Path.IsPathRooted(inputPath) ? inputPath : Path.Combine(repositoryRoot, inputPath)));
}
process = Process.Start(startInfo);
if (process == null)
{
status = "stream launch failed";
return false;
}
_ = DrainAsync(process.StandardOutput, Path.Combine(logRoot, "mimir-app-stream.out.log"));
_ = DrainAsync(process.StandardError, Path.Combine(logRoot, "mimir-app-stream.err.log"));
startedAt = DateTimeOffset.Now;
status = $"live pid={process.Id}";
return true;
}
public void Stop()
{
Refresh();
if (!IsStreaming)
{
return;
}
try
{
process!.Kill(entireProcessTree: true);
status = "stream stopped";
}
catch (Exception ex)
{
status = $"stream stop failed: {ex.Message}";
}
finally
{
process?.Dispose();
process = null;
startedAt = null;
}
}
public void Refresh()
{
if (process == null || !process.HasExited)
{
return;
}
var exitCode = process.ExitCode;
process.Dispose();
process = null;
startedAt = null;
status = exitCode == 0
? "stream ended"
: $"stream failed exit={exitCode}";
}
public void Dispose()
{
Stop();
}
private static async Task DrainAsync(StreamReader reader, string path)
{
await using var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Read);
await using var writer = new StreamWriter(stream);
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line)
{
await writer.WriteLineAsync(line).ConfigureAwait(false);
}
}
private static bool IsTruthy(string? value) =>
!string.IsNullOrWhiteSpace(value) &&
(string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "on", StringComparison.OrdinalIgnoreCase));
private static string ResolveRepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory != null)
{
if (Directory.Exists(Path.Combine(directory.FullName, "scripts")) &&
File.Exists(Path.Combine(directory.FullName, "src", "Mimir.Runtime", "Mimir.Runtime.csproj")))
{
return directory.FullName;
}
directory = directory.Parent;
}
return Directory.GetCurrentDirectory();
}
}

View file

@ -331,3 +331,4 @@
{"ts": "2026-06-01T13:32:22+01:00", "type": "implementation", "status": "ok", "note": "Direct compositor handles are now wired: Fensalir CultUI preview hit-tests selected item corner handles/body drags and emits normalized AquariumUiPreviewInteraction gestures, while MimirSceneEditorState handles those gestures through the same snapping/aspect-preserving placement owner. Sliders remain fallback/precision controls."}
{"ts": "2026-06-01T13:37:34+01:00", "type": "implementation", "status": "ok", "note": "Gated explicit Mimir inspector source/transform controls behind HasSelectedProgramSource. Direct compositor preview handles remain the primary edit path; inspector sliders/buttons are selected-source precision/fallback controls only. Scene-editor smoke now proves the editor camera is not a program source."}
{"ts": "2026-06-01T14:26:12+01:00", "type": "implementation", "status": "ok", "note": "Picked the compositor UX low-hanging fruit: hover/active preview state, snap guides, empty-canvas deselect, selected-source contain/cover/stretch fit mode, arrow-key nudging that bypasses snap for precision, and PageUp/PageDown layer ordering. Scene-editor smoke now asserts hover, guide, fit, nudge, empty-deselect, direct move/resize handles, aspect lock, snap, and non-source inspector gating."}
{"ts": "2026-06-01T14:39:51+01:00", "type": "decision", "status": "ok", "note": "2026-06-01: Mimir editor output authority moved to a right-rail Output panel below Inspector. MimirProgramRecorder now writes timestamped receipts to the user Videos\\\\Mimir folder by default, while MimirProgramStreamer owns Live/Stop for the configured Broadcast RTMP path and refuses to publish without MIMIR_STREAM_INPUT unless a synthetic smoke is explicitly enabled."}

View file

@ -70,15 +70,19 @@ invariants:
CultUI preview state; keyboard arrows nudge selected layers without
snapping, PageUp/PageDown reorder layers, and the inspector owns the
selected source contain/cover/stretch fit mode.
- `MimirProgramRecorder` owns the first app-level recording control. The Video
Sources rail exposes `Record` / `Stop`, status text, latest output path, and
- The right rail `Output` panel owns OBS-like program actions below the
inspector. `MimirProgramRecorder` owns `Record` / `Stop`, status text,
timestamped receipts under `C:\Users\Meta\Videos\Mimir` by default, and
process-tree shutdown. Its current actuator delegates to
`scripts/record-stream-proof-output.ps1`, so the app records the same Raven
+ Kiyo PIP + Starfire Scarlett NVENC/AAC composite intended for
Yggdrasil/public judgment under `artifacts/runtime/stream-proof/`.
Yggdrasil/public judgment. `MIMIR_RECORD_OUTPUT_DIR` redirects receipts and
`MIMIR_RECORD_START_RAVEN=1` asks the app to launch Raven's sender too.
Native program-output recording should replace that actuator behind the
same owner.
`MimirProgramStreamer` owns `Live` / `Stop` for the `Mimir.Broadcast` RTMP
path and requires `MIMIR_STREAM_INPUT` unless
`MIMIR_STREAM_ALLOW_TEST_PATTERN=1` explicitly authorizes a synthetic
smoke. Native program-output recording/streaming should replace those
actuators behind the same owner.
- Fast stereo depth belongs to Fensalir's D3D12 compute/resource authority.
`research/d3d12-stereo-depth-provenance-2026-05-29.md` records libSGM as
the permissive north-star reference for a future SGM-shaped stereo