@@ -247,7 +247,6 @@ export function VJApp() {
247247 sendFrames : aiSendFrames ,
248248 showCaptureDebug : aiShowCaptureDebug ,
249249 prompt : aiPrompt ,
250- captureSize : aiCaptureSize ,
251250 outputWidth : aiOutputWidth ,
252251 outputHeight : aiOutputHeight ,
253252 frameRate : aiFrameRate ,
@@ -268,7 +267,6 @@ export function VJApp() {
268267 setSendFrames : setAiSendFrames ,
269268 setShowCaptureDebug : setAiShowCaptureDebug ,
270269 setPrompt : setAiPrompt ,
271- setCaptureSize : setAiCaptureSize ,
272270 setOutputSize : setAiOutputSize ,
273271 setFrameRate : setAiFrameRate ,
274272 setSeed : setAiSeed ,
@@ -286,8 +284,6 @@ export function VJApp() {
286284 updatePromptPreset,
287285 } = useAiSettingsStore ( ) ;
288286
289- const aiOutputLong = Math . max ( aiOutputWidth , aiOutputHeight ) ;
290-
291287 // BroadcastChannel to the /vj/stage tab
292288 const stageChannelRef = useRef < BroadcastChannel | null > ( null ) ;
293289 const stageFrameSeqRef = useRef < number > ( 0 ) ;
@@ -323,20 +319,11 @@ export function VJApp() {
323319 const flushSettingsNow = useCallback ( ( ) => {
324320 if ( ! aiTransport . isConnected ( ) ) return ;
325321 const s = useAiSettingsStore . getState ( ) ;
326- const aspect = s . outputWidth / s . outputHeight ;
327- const captureW =
328- aspect >= 1
329- ? s . captureSize
330- : Math . max ( 16 , Math . round ( s . captureSize * aspect ) ) ;
331- const captureH =
332- aspect >= 1
333- ? Math . max ( 16 , Math . round ( s . captureSize / aspect ) )
334- : s . captureSize ;
335322 const payload : Record < string , unknown > = {
336323 prompt : s . prompt ,
337324 seed : s . seed ,
338- captureWidth : captureW ,
339- captureHeight : captureH ,
325+ captureWidth : s . outputWidth ,
326+ captureHeight : s . outputHeight ,
340327 width : s . outputWidth ,
341328 height : s . outputHeight ,
342329 } ;
@@ -587,6 +574,10 @@ export function VJApp() {
587574 const [ aiLogs , setAiLogs ] = useState < string [ ] > ( [ ] ) ;
588575 const [ aiImageUrl , setAiImageUrl ] = useState < string | null > ( null ) ;
589576 const [ aiGenTime , setAiGenTime ] = useState < number | null > ( null ) ;
577+ // Actual received-frames-per-second counter — measures real canvas output
578+ // rate (what the user sees), not per-frame server latency.
579+ const [ aiRecvFps , setAiRecvFps ] = useState < number | null > ( null ) ;
580+ const aiRecvFpsRef = useRef ( { count : 0 , last : performance . now ( ) } ) ;
590581 // Per-stage timing breakdown emitted by inference_server.py — surfaces where
591582 // the frame budget is going (vae_encode vs transformer vs jpeg vs python glue).
592583 // Useful for spotting the next bottleneck without re-instrumenting the worker.
@@ -773,7 +764,13 @@ export function VJApp() {
773764 await audioEngine . init ( source ) ;
774765 audioEngineRef . current = audioEngine ;
775766
776- const visualEngine = new VisualEngine ( canvas , audioEngine , SCENES ) ;
767+ const visualEngine = new VisualEngine (
768+ canvas ,
769+ audioEngine ,
770+ SCENES ,
771+ aiOutputWidth ,
772+ aiOutputHeight ,
773+ ) ;
777774 visualEngineRef . current = visualEngine ;
778775
779776 const lightingEngine = new LightingEngine (
@@ -805,9 +802,16 @@ export function VJApp() {
805802 ) ;
806803 }
807804 } ,
808- [ fetchDevices , handleLightingFrame , handleDmxFrame , fixtures , persistedSceneId ]
805+ [ fetchDevices , handleLightingFrame , handleDmxFrame , fixtures , persistedSceneId , aiOutputWidth , aiOutputHeight ]
809806 ) ;
810807
808+ // Resize the waveform canvas when the AI output resolution changes so the
809+ // input canvas always matches the output dimensions — what you see is what
810+ // the AI sees, no hidden crop or stretch step.
811+ useEffect ( ( ) => {
812+ visualEngineRef . current ?. handleResize ( aiOutputWidth , aiOutputHeight ) ;
813+ } , [ aiOutputWidth , aiOutputHeight ] ) ;
814+
811815 // ============================================================================
812816 // UI event handlers
813817 // ============================================================================
@@ -1103,6 +1107,17 @@ export function VJApp() {
11031107 return ;
11041108 }
11051109
1110+ // Count received frames for real FPS measurement
1111+ const fpsState = aiRecvFpsRef . current ;
1112+ fpsState . count ++ ;
1113+ const now = performance . now ( ) ;
1114+ const elapsed = now - fpsState . last ;
1115+ if ( elapsed >= 1000 ) {
1116+ setAiRecvFps ( Math . round ( ( fpsState . count / elapsed ) * 1000 ) ) ;
1117+ fpsState . count = 0 ;
1118+ fpsState . last = now ;
1119+ }
1120+
11061121 const url = URL . createObjectURL ( frame . blob ) ;
11071122 setAiImageUrl ( ( prev ) => {
11081123 if ( prev ) URL . revokeObjectURL ( prev ) ;
@@ -1259,11 +1274,11 @@ export function VJApp() {
12591274 return ;
12601275 }
12611276
1262- const outAspect = aiOutputWidth / aiOutputHeight ;
1263- const capW =
1264- outAspect >= 1 ? aiCaptureSize : Math . max ( 16 , Math . round ( aiCaptureSize * outAspect ) ) ;
1265- const capH =
1266- outAspect >= 1 ? Math . max ( 16 , Math . round ( aiCaptureSize / outAspect ) ) : aiCaptureSize ;
1277+ // Capture at the output resolution — VisualEngine already renders at
1278+ // the same dimensions, so this is a straight 1:1 copy. No crop, no
1279+ // scale, no aspect-ratio mismatch.
1280+ const capW = aiOutputWidth ;
1281+ const capH = aiOutputHeight ;
12671282 const resolutionKey = capW * 10000 + capH ;
12681283 if ( ! sender . captureCanvas || sender . resolution !== resolutionKey ) {
12691284 sender . captureCanvas = document . createElement ( "canvas" ) ;
@@ -1278,21 +1293,7 @@ export function VJApp() {
12781293 return ;
12791294 }
12801295
1281- let srcCropW = src . width ;
1282- let srcCropH = src . height ;
1283- if ( src . width / src . height > outAspect ) {
1284- srcCropW = src . height * outAspect ;
1285- } else {
1286- srcCropH = src . width / outAspect ;
1287- }
1288- const srcX = ( src . width - srcCropW ) / 2 ;
1289- const srcY = ( src . height - srcCropH ) / 2 ;
1290-
1291- ctx . drawImage (
1292- src ,
1293- srcX , srcY , srcCropW , srcCropH ,
1294- 0 , 0 , capW , capH
1295- ) ;
1296+ ctx . drawImage ( src , 0 , 0 , capW , capH ) ;
12961297
12971298 sender . lastFrameTime = now ;
12981299 sender . frameCount ++ ;
@@ -1332,7 +1333,7 @@ export function VJApp() {
13321333 "image/jpeg" ,
13331334 0.85
13341335 ) ;
1335- } , [ aiTransport , aiCaptureSize , aiFrameRate , aiOutputWidth , aiOutputHeight ] ) ;
1336+ } , [ aiTransport , aiFrameRate , aiOutputWidth , aiOutputHeight ] ) ;
13361337
13371338 useEffect ( ( ) => {
13381339 // Catch-all for non-hotkey state changes (slider drags, backend swap,
@@ -1346,7 +1347,6 @@ export function VJApp() {
13461347 aiBackend ,
13471348 aiPrompt ,
13481349 aiSeed ,
1349- aiCaptureSize ,
13501350 aiOutputWidth ,
13511351 aiOutputHeight ,
13521352 aiKleinAlpha ,
@@ -1550,6 +1550,14 @@ export function VJApp() {
15501550 < SystemsBar
15511551 audioStatus = { status }
15521552 audioDeviceLabel = { selectedDeviceLabel }
1553+ devices = { devices }
1554+ selectedDeviceId = { selectedDeviceId }
1555+ onDeviceChange = { handleDeviceChange }
1556+ systemAudioSupported = { systemAudioSupported }
1557+ systemAudioValue = { SYSTEM_AUDIO_VALUE }
1558+ scenes = { SCENES }
1559+ currentSceneId = { currentSceneId }
1560+ onSceneChange = { handleSceneChange }
15531561 aiStatus = { aiStatus }
15541562 aiBackend = { aiBackend }
15551563 onBackendChange = { ( b ) => {
@@ -1563,6 +1571,7 @@ export function VJApp() {
15631571 onConnect = { ( ) => void aiTransport . start ( ) }
15641572 onDisconnect = { ( ) => void aiTransport . stop ( ) }
15651573 aiGenTimeMs = { aiStatus === "connected" ? aiGenTime : null }
1574+ aiRecvFps = { aiStatus === "connected" ? aiRecvFps : null }
15661575 aiTiming = { aiStatus === "connected" ? aiTiming : null }
15671576 dmxStatus = { dmxStatus }
15681577 dmxFixtureCount = { fixtures . length }
@@ -1611,14 +1620,6 @@ export function VJApp() {
16111620 < WaveformSourceCard
16121621 canvasRef = { canvasRef }
16131622 status = { status }
1614- scenes = { SCENES }
1615- currentSceneId = { currentSceneId }
1616- onSceneChange = { handleSceneChange }
1617- systemAudioSupported = { systemAudioSupported }
1618- systemAudioValue = { SYSTEM_AUDIO_VALUE }
1619- selectedDeviceId = { selectedDeviceId }
1620- devices = { devices }
1621- onDeviceChange = { handleDeviceChange }
16221623 showDebug = { showDebug }
16231624 setShowDebug = { setShowDebug }
16241625 debugFeatures = { debugFeatures }
@@ -1656,9 +1657,6 @@ export function VJApp() {
16561657 onKleinAlphaChange = { setAiKleinAlpha }
16571658 aiKleinSteps = { aiKleinSteps }
16581659 onKleinStepsChange = { setAiKleinSteps }
1659- aiCaptureSize = { aiCaptureSize }
1660- onCaptureSizeChange = { setAiCaptureSize }
1661- aiOutputLong = { aiOutputLong }
16621660 aiStageSharpen = { aiStageSharpen }
16631661 aiStagePixelate = { aiStagePixelate }
16641662 aiStagePixelateSize = { aiStagePixelateSize }
0 commit comments