Add value source card crosslinks and fix scene initial value bias
- Audio value source cards link to the referenced audio source - Adaptive scene cards link to the referenced picture source - Fix SceneValueStream starting at 0.5 regardless of actual scene; first frame now skips smoothing to avoid artificial bias - Add crosslinks guidance to CLAUDE.md card appearance section Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -156,6 +156,7 @@ When creating or modifying entity cards (devices, targets, CSS sources, streams,
|
|||||||
- Clone (📋) and Edit (✏️) icon buttons in `.template-card-actions`
|
- Clone (📋) and Edit (✏️) icon buttons in `.template-card-actions`
|
||||||
- Delete (✕) button as `.card-remove-btn`
|
- Delete (✕) button as `.card-remove-btn`
|
||||||
- Property badges in `.stream-card-props` with emoji icons
|
- Property badges in `.stream-card-props` with emoji icons
|
||||||
|
- **Crosslinks**: When a card references another entity (audio source, picture source, capture template, PP template, etc.), make the property badge a clickable link using the `stream-card-link` CSS class and an `onclick` handler calling `navigateToCard(tab, subTab, sectionKey, cardAttr, cardValue)`. Only add the link when the referenced entity is found (to avoid broken navigation). Example: `<span class="stream-card-prop stream-card-link" onclick="event.stopPropagation(); navigateToCard('streams','audio','audio-multi','data-id','${id}')">🎵 Name</span>`
|
||||||
|
|
||||||
### Modal footer buttons
|
### Modal footer buttons
|
||||||
|
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ class SceneValueStream(ValueStream):
|
|||||||
self._max = max_value
|
self._max = max_value
|
||||||
self._live_stream_manager = live_stream_manager
|
self._live_stream_manager = live_stream_manager
|
||||||
self._live_stream = None
|
self._live_stream = None
|
||||||
self._prev_value = 0.5 # neutral start
|
self._prev_value = None # None = no frame seen yet; skip smoothing on first frame
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
if self._live_stream_manager and self._picture_source_id:
|
if self._live_stream_manager and self._picture_source_id:
|
||||||
@@ -471,15 +471,15 @@ class SceneValueStream(ValueStream):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"SceneValueStream failed to release live stream: {e}")
|
logger.warning(f"SceneValueStream failed to release live stream: {e}")
|
||||||
self._live_stream = None
|
self._live_stream = None
|
||||||
self._prev_value = 0.5
|
self._prev_value = None
|
||||||
|
|
||||||
def get_value(self) -> float:
|
def get_value(self) -> float:
|
||||||
if self._live_stream is None:
|
if self._live_stream is None:
|
||||||
return self._prev_value
|
return self._prev_value if self._prev_value is not None else 0.0
|
||||||
|
|
||||||
frame = self._live_stream.get_latest_frame()
|
frame = self._live_stream.get_latest_frame()
|
||||||
if frame is None:
|
if frame is None:
|
||||||
return self._prev_value
|
return self._prev_value if self._prev_value is not None else 0.0
|
||||||
|
|
||||||
# Fast luminance: subsample to ~64x64 via numpy stride (zero-copy view)
|
# Fast luminance: subsample to ~64x64 via numpy stride (zero-copy view)
|
||||||
img = frame.image
|
img = frame.image
|
||||||
@@ -500,7 +500,10 @@ class SceneValueStream(ValueStream):
|
|||||||
if self._behavior == "complement":
|
if self._behavior == "complement":
|
||||||
raw = 1.0 - raw
|
raw = 1.0 - raw
|
||||||
|
|
||||||
# Temporal smoothing (EMA)
|
# Temporal smoothing (EMA) — skip on first frame (no history to blend with)
|
||||||
|
if self._prev_value is None:
|
||||||
|
smoothed = raw
|
||||||
|
else:
|
||||||
smoothed = self._smoothing * self._prev_value + (1.0 - self._smoothing) * raw
|
smoothed = self._smoothing * self._prev_value + (1.0 - self._smoothing) * raw
|
||||||
self._prev_value = smoothed
|
self._prev_value = smoothed
|
||||||
|
|
||||||
|
|||||||
@@ -482,9 +482,13 @@ export function createValueSourceCard(src) {
|
|||||||
} else if (src.source_type === 'audio') {
|
} else if (src.source_type === 'audio') {
|
||||||
const audioSrc = _cachedAudioSources.find(a => a.id === src.audio_source_id);
|
const audioSrc = _cachedAudioSources.find(a => a.id === src.audio_source_id);
|
||||||
const audioName = audioSrc ? audioSrc.name : (src.audio_source_id || '-');
|
const audioName = audioSrc ? audioSrc.name : (src.audio_source_id || '-');
|
||||||
|
const audioSection = audioSrc ? (audioSrc.source_type === 'mono' ? 'audio-mono' : 'audio-multi') : 'audio-multi';
|
||||||
const modeLabel = src.mode || 'rms';
|
const modeLabel = src.mode || 'rms';
|
||||||
|
const audioBadge = audioSrc
|
||||||
|
? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('value_source.audio_source'))}" onclick="event.stopPropagation(); navigateToCard('streams','audio','${audioSection}','data-id','${src.audio_source_id}')">🎵 ${escapeHtml(audioName)}</span>`
|
||||||
|
: `<span class="stream-card-prop" title="${escapeHtml(t('value_source.audio_source'))}">🎵 ${escapeHtml(audioName)}</span>`;
|
||||||
propsHtml = `
|
propsHtml = `
|
||||||
<span class="stream-card-prop" title="${escapeHtml(t('value_source.audio_source'))}">🎵 ${escapeHtml(audioName)}</span>
|
${audioBadge}
|
||||||
<span class="stream-card-prop">📈 ${modeLabel.toUpperCase()}</span>
|
<span class="stream-card-prop">📈 ${modeLabel.toUpperCase()}</span>
|
||||||
<span class="stream-card-prop">↕️ ${src.min_value ?? 0}–${src.max_value ?? 1}</span>
|
<span class="stream-card-prop">↕️ ${src.min_value ?? 0}–${src.max_value ?? 1}</span>
|
||||||
`;
|
`;
|
||||||
@@ -497,8 +501,16 @@ export function createValueSourceCard(src) {
|
|||||||
} else if (src.source_type === 'adaptive_scene') {
|
} else if (src.source_type === 'adaptive_scene') {
|
||||||
const ps = _cachedStreams.find(s => s.id === src.picture_source_id);
|
const ps = _cachedStreams.find(s => s.id === src.picture_source_id);
|
||||||
const psName = ps ? ps.name : (src.picture_source_id || '-');
|
const psName = ps ? ps.name : (src.picture_source_id || '-');
|
||||||
|
let psSubTab = 'raw', psSection = 'raw-streams';
|
||||||
|
if (ps) {
|
||||||
|
if (ps.stream_type === 'static_image') { psSubTab = 'static_image'; psSection = 'static-streams'; }
|
||||||
|
else if (ps.stream_type === 'processed') { psSubTab = 'processed'; psSection = 'proc-streams'; }
|
||||||
|
}
|
||||||
|
const psBadge = ps
|
||||||
|
? `<span class="stream-card-prop stream-card-link" onclick="event.stopPropagation(); navigateToCard('streams','${psSubTab}','${psSection}','data-stream-id','${src.picture_source_id}')" title="${escapeHtml(t('value_source.picture_source'))}">🖥️ ${escapeHtml(psName)}</span>`
|
||||||
|
: `<span class="stream-card-prop">🖥️ ${escapeHtml(psName)}</span>`;
|
||||||
propsHtml = `
|
propsHtml = `
|
||||||
<span class="stream-card-prop">🖥️ ${escapeHtml(psName)}</span>
|
${psBadge}
|
||||||
<span class="stream-card-prop">🔄 ${src.scene_behavior || 'complement'}</span>
|
<span class="stream-card-prop">🔄 ${src.scene_behavior || 'complement'}</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user