fix(align): CH3 flicker 무한 대기 방지 — sliding window majority + relaxed mode

인체 데이터 (data123/) 분석 결과 CH3 (-13.66° down-tilt) 검출률이
Phantom 95.2% → Human 31.8% 로 급락하며 YYNNYY flick 패턴 관측.
기존 "3연속 hit" 진입 조건이 무한 대기에 빠져 사용자에게
"↑ 위로 조금씩" 문구만 반복 노출되는 문제를 해결.

- VERTICAL_CLIMB 진입: 3연속 → 최근 6프레임 중 3회 (majority)
- CH3_STABILIZE / CENTER_OPTIMIZE lost 판정: 4프레임 중 3회 (majority)
- Stuck detection (20프레임 ≈ 5초 대기 시): 상단 3채널 relaxed mode
  진입 → CH3 없이도 정렬 완료 진행
- Soft hint (12프레임 ≈ 3초 후): "CH3 확인 중 · 위치 유지/미세 조정"
  으로 문구 완화 (사용자 답답함 완화)
- AdvisorState.relaxedMode 플래그 추가 (후속 저장 로직 대비)

Python replay (VBT26050202_0CM 22프레임) 로 검증:
- LEGACY: VERTICAL 무한 대기 → 정렬 실패
- NEW: soft-hint → threshold 도달 시 relaxed → 3프레임 만에 LR_BAL 도달

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 17:45:14 +09:00
parent 45f5efc278
commit e808b5bc57
3 changed files with 154 additions and 56 deletions
@@ -41,6 +41,8 @@ data class AdvisorState(
val summary: AlignSummary?, val summary: AlignSummary?,
val accumProgress: Int = 0, // 'accum' 일 때 (현재/총) val accumProgress: Int = 0, // 'accum' 일 때 (현재/총)
val accumTarget: Int = 0, val accumTarget: Int = 0,
/** 2026-07-06: CH3 signal 미약으로 상단 3채널 (CH0~CH2) 만으로 정렬 진행 중. */
val relaxedMode: Boolean = false,
) )
data class AlignSummary( data class AlignSummary(
@@ -76,6 +78,22 @@ object AlignmentConstants {
const val CV_THRESHOLD_LOOSE = 0.20 const val CV_THRESHOLD_LOOSE = 0.20
/** CENTER_OPTIMIZE 통과 요구 연속 commit 수 (hysteresis). */ /** CENTER_OPTIMIZE 통과 요구 연속 commit 수 (hysteresis). */
const val CENTER_OPT_CONFIRM_STREAK = 3 const val CENTER_OPT_CONFIRM_STREAK = 3
// 2026-07-06: CH3 flicker 인체 대응 — sliding window majority + relaxed mode.
// 실측: Human VBT26050202 0CM 세션에서 CH3 hit pattern = Y Y N N Y Y N N (최대 2연속).
// 기존 "3연속 hit" 진입 조건이 절대 통과 못해 VERTICAL_CLIMB 무한 대기 → 사용자 UX 악화.
/** VERTICAL_CLIMB 진입 판정 sliding window (프레임). */
const val VERTICAL_HIT_WINDOW = 6
/** 위 window 내 CH3 hit 최소 횟수 (3/6 = 50%). */
const val VERTICAL_HIT_MAJORITY = 3
/** CH3_STABILIZE / CENTER_OPTIMIZE 의 CH3 lost 판정 sliding window. */
const val STABILIZE_LOST_WINDOW = 4
/** 위 window 내 lost 최소 횟수 — 진짜 lost 로 판정. */
const val STABILIZE_LOST_MAJORITY = 3
/** VERTICAL_CLIMB 무한 대기 감지 임계 (프레임, 약 5초). */
const val VERTICAL_STUCK_THRESHOLD = 20
/** VERTICAL_CLIMB 반복 안내 문구 완화 시점 (프레임, 약 3초). */
const val VERTICAL_SOFT_HINT_AFTER = 12
} }
/** stateless 단일 스냅샷 advisor — alignment.py:alignment_advice(). */ /** stateless 단일 스냅샷 advisor — alignment.py:alignment_advice(). */
@@ -207,12 +225,17 @@ class AlignGuide4Stage(
val msgKey: Int? = null, val msgKey: Int? = null,
/** Args for msgKey formatting. */ /** Args for msgKey formatting. */
val msgArgs: List<Any> = emptyList(), val msgArgs: List<Any> = emptyList(),
/** 2026-07-06: CH3 미약 → 상단 3채널 relaxed mode 진행 중. */
val relaxedMode: Boolean = false,
) )
var phase: Phase4 = Phase4.INITIAL_ACCUM var phase: Phase4 = Phase4.INITIAL_ACCUM
private set private set
var bestCenterSet: Set<Int> = emptySet() var bestCenterSet: Set<Int> = emptySet()
private set private set
/** 2026-07-06: CH3 신호 미약으로 CH0~CH2 로만 정렬 진행하는 완화 모드. */
var relaxedMode: Boolean = false
private set
private var initialCount = 0 private var initialCount = 0
private var verticalHitStreak = 0 private var verticalHitStreak = 0
private var stabilizeCount = 0 private var stabilizeCount = 0
@@ -223,9 +246,22 @@ class AlignGuide4Stage(
private var lostStreak = 0 // LR_BALANCE 채널 lost hysteresis private var lostStreak = 0 // LR_BALANCE 채널 lost hysteresis
private var centerOptimizeStreak = 0 // CENTER_OPTIMIZE CV 통과 연속 카운터 private var centerOptimizeStreak = 0 // CENTER_OPTIMIZE CV 통과 연속 카운터
// 2026-07-06 CH3 flicker 대응 링버퍼 & stuck counter.
// history 는 true = hit / detected, false = miss / lost.
private val verticalHitHistory = ArrayDeque<Boolean>()
private val stabilizeLostHistory = ArrayDeque<Boolean>() // true = lost this frame
private val centerOptLostHistory = ArrayDeque<Boolean>() // true = lost this frame
private var verticalStuckCount = 0 // VERTICAL_CLIMB 체류 프레임 수
private fun pushHistory(dq: ArrayDeque<Boolean>, v: Boolean, window: Int) {
dq.addLast(v)
while (dq.size > window) dq.removeFirst()
}
fun reset() { fun reset() {
phase = Phase4.INITIAL_ACCUM phase = Phase4.INITIAL_ACCUM
bestCenterSet = emptySet() bestCenterSet = emptySet()
relaxedMode = false
initialCount = 0 initialCount = 0
verticalHitStreak = 0 verticalHitStreak = 0
stabilizeCount = 0 stabilizeCount = 0
@@ -235,6 +271,10 @@ class AlignGuide4Stage(
finalConfirmCount = 0 finalConfirmCount = 0
lostStreak = 0 lostStreak = 0
centerOptimizeStreak = 0 centerOptimizeStreak = 0
verticalHitHistory.clear()
stabilizeLostHistory.clear()
centerOptLostHistory.clear()
verticalStuckCount = 0
} }
fun step(s: AlignSummary): Out { fun step(s: AlignSummary): Out {
@@ -253,43 +293,74 @@ class AlignGuide4Stage(
} }
// === Phase 2: VERTICAL_CLIMB === // === Phase 2: VERTICAL_CLIMB ===
// 2026-07-06: 연속 카운터 대신 sliding window majority (window=6, hit≥3).
// 실측 인체 데이터에서 CH3 이 YYNN 패턴으로 flick 하여 3연속을 절대 못 채우는
// 무한 대기 케이스가 있어, 최근 6 프레임 중 3회 hit 이면 진입 허용.
// 또한 20 프레임 지나도 통과 못하면 RELAXED_MODE ON → 상단 3채널로 진행.
if (phase == Phase4.VERTICAL_CLIMB) { if (phase == Phase4.VERTICAL_CLIMB) {
if (!s.ch3) { pushHistory(verticalHitHistory, s.ch3, AlignmentConstants.VERTICAL_HIT_WINDOW)
verticalHitStreak = 0 // 연속 카운터 리셋 — false detection 거부 verticalStuckCount++
return Out(AlignAction.MOVE_UP, "↑ 위로 조금씩 올리세요 (ch3 미검출)", val hitCount = verticalHitHistory.count { it }
phase, 0,
msgKey = R.string.align_move_up) // Stuck detection → Relaxed mode 진입 조건:
} // (a) VERTICAL_CLIMB 체류 ≥ 20 프레임
verticalHitStreak++ // (b) 최근 window 내 CH3 hit < majority (신호가 여전히 미약)
if (verticalHitStreak < verticalHitRequired) { // (c) CH0+CH1+CH2 상단 3채널이 detected
// ch3 일시 검출 — verticalHitRequired 회 연속 확인 전까지 사용자 정지. if (!relaxedMode
// (현재 위치가 진짜 ch3 인지 평균이 만든 false echo 인지 확인 중) && verticalStuckCount >= AlignmentConstants.VERTICAL_STUCK_THRESHOLD
&& hitCount < AlignmentConstants.VERTICAL_HIT_MAJORITY
&& (0..2).all { it < s.detected.size && s.detected[it] }) {
relaxedMode = true
phase = Phase4.CENTER_OPTIMIZE
centerOptimizeStreak = 0
bestCenterSet = (0..2).toSet()
return Out(AlignAction.STOP, return Out(AlignAction.STOP,
"✓ ch3 일시 검출 — 확인 중 (${verticalHitStreak}/${verticalHitRequired})", "⚠ CH3 미약 — 상단 3채널 relaxed 정렬 진입",
phase, verticalHitStreak, phase, 0,
msgKey = R.string.align_hold_position, msgKey = R.string.align_ch3_relaxed_mode,
msgArgs = listOf(verticalHitStreak, verticalHitRequired)) relaxedMode = true)
}
if (hitCount >= AlignmentConstants.VERTICAL_HIT_MAJORITY) {
phase = Phase4.CH3_STABILIZE
stabilizeCount = 0
bestCenterSet = curSet
verticalStuckCount = 0
verticalHitHistory.clear()
} else {
// 진입 조건 미달 — MOVE_UP 힌트. 다만 오래 반복되면 문구를 부드럽게.
val useSoftHint = verticalStuckCount >= AlignmentConstants.VERTICAL_SOFT_HINT_AFTER
val msgKey = if (useSoftHint) R.string.align_ch3_soft_hint else R.string.align_move_up
val label = if (useSoftHint)
"· CH3 확인 중 · 유지/미세 조정 (hit ${hitCount}/${AlignmentConstants.VERTICAL_HIT_WINDOW})"
else
"↑ 위로 조금씩 올리세요 (hit ${hitCount}/${AlignmentConstants.VERTICAL_HIT_WINDOW})"
return Out(AlignAction.MOVE_UP, label, phase, hitCount, msgKey = msgKey)
} }
phase = Phase4.CH3_STABILIZE
stabilizeCount = 0
bestCenterSet = curSet
} }
// === Phase 3: CH3_STABILIZE === // === Phase 3: CH3_STABILIZE ===
// 2026-07-06: CH3 lost 즉시 회귀 대신 sliding window (window=4, lost≥3) 로 판정.
// 단발 flick 은 무시하여 stabilize 진행. 진짜 lost 는 원위치 (VERTICAL_CLIMB) 복귀.
if (phase == Phase4.CH3_STABILIZE) { if (phase == Phase4.CH3_STABILIZE) {
stabilizeCount++ stabilizeCount++
if (curSet.size > bestCenterSet.size) bestCenterSet = curSet if (curSet.size > bestCenterSet.size) bestCenterSet = curSet
if (!s.ch3) { pushHistory(stabilizeLostHistory, !s.ch3, AlignmentConstants.STABILIZE_LOST_WINDOW)
// ch3 잃었으면 → VERTICAL_CLIMB 회귀 val lostCount = stabilizeLostHistory.count { it }
if (lostCount >= AlignmentConstants.STABILIZE_LOST_MAJORITY) {
// 진짜 lost — VERTICAL_CLIMB 회귀 (기존 로직 유지)
phase = Phase4.VERTICAL_CLIMB phase = Phase4.VERTICAL_CLIMB
stabilizeCount = 0 stabilizeCount = 0
return Out(AlignAction.MOVE_UP, "↑ ch3 다시 잃음 — 위로", stabilizeLostHistory.clear()
verticalHitHistory.clear()
verticalStuckCount = 0
return Out(AlignAction.MOVE_UP, "↑ ch3 진짜 놓침 — 다시 위로",
phase, 0, phase, 0,
msgKey = R.string.align_recover_up) msgKey = R.string.align_recover_up)
} }
if (stabilizeCount < stabilizeTarget) { if (stabilizeCount < stabilizeTarget) {
return Out(AlignAction.STOP, return Out(AlignAction.STOP,
"✓ ch3 검출! 위치 유지 (${stabilizeCount}/${stabilizeTarget})", "✓ ch3 검출 — 위치 유지 (${stabilizeCount}/${stabilizeTarget}, flick ${lostCount}/${AlignmentConstants.STABILIZE_LOST_WINDOW})",
phase, stabilizeCount, phase, stabilizeCount,
msgKey = R.string.align_hold_position, msgKey = R.string.align_hold_position,
msgArgs = listOf(stabilizeCount, stabilizeTarget)) msgArgs = listOf(stabilizeCount, stabilizeTarget))
@@ -297,51 +368,71 @@ class AlignGuide4Stage(
// 2026-07-02: CH3_STABILIZE 통과 → CENTER_OPTIMIZE (CV 로 4채널 균등 확인) // 2026-07-02: CH3_STABILIZE 통과 → CENTER_OPTIMIZE (CV 로 4채널 균등 확인)
phase = Phase4.CENTER_OPTIMIZE phase = Phase4.CENTER_OPTIMIZE
centerOptimizeStreak = 0 centerOptimizeStreak = 0
stabilizeLostHistory.clear()
} }
// === Phase 4a: CENTER_OPTIMIZE (신규) — CH0~CH3 chord CV 로 최적 vertical 정밀 조정 === // === Phase 4a: CENTER_OPTIMIZE — CH chord CV 로 최적 vertical 정밀 조정 ===
// 전제: CH3 는 이미 안정 검출됨. 4채널 chord CV 가 낮으면 (0.15 이하) 정렬 완료로 인정. // Normal mode: CH0~CH3 4채널 CV. CV ≤ 0.15 이면 정렬 완료로 인정.
// CV 높으면: chord[3] > chord[0] 이면 probe 가 high → MOVE_DOWN, // CV 높으면 chord[3] > chord[0] → MOVE_DOWN, chord[0] > chord[3] → MOVE_UP.
// chord[0] > chord[3] 이면 probe 가 low → MOVE_UP. // Relaxed mode (2026-07-06): CH3 미약 시 CH0~CH2 3채널로 CV, 방향은 chord[2]:chord[0].
// CH3 lost 감지는 sliding window (window=4, lost≥3) — 단발 flick 무시.
if (phase == Phase4.CENTER_OPTIMIZE) { if (phase == Phase4.CENTER_OPTIMIZE) {
val center4Detected = (0..3).all { it < s.detected.size && s.detected[it] } // Relaxed 모드가 아니면 CH3 flicker 검사.
if (!s.ch3) { if (!relaxedMode) {
// 2026-07-06 fix: CH3 를 CENTER_OPTIMIZE 진입 후 잃음 = probe 가 위로 올라가 pushHistory(centerOptLostHistory, !s.ch3, AlignmentConstants.STABILIZE_LOST_WINDOW)
// CH3 beam 이 방광 inferior edge 를 넘어감 → 방광이 probe 아래로 이동. val lostCount = centerOptLostHistory.count { it }
// → MOVE_DOWN 으로 CH3 회복. 이전 MOVE_UP 은 잃음 원인과 반대라 무한 상승. if (lostCount >= AlignmentConstants.STABILIZE_LOST_MAJORITY) {
phase = Phase4.CH3_STABILIZE // 진짜 lost — CH3_STABILIZE 회귀. 원인: probe 가 위로 올라가 CH3 beam 이 방광
stabilizeCount = 0 // inferior edge 를 넘어감. → 사용자에게 MOVE_DOWN 안내.
centerOptimizeStreak = 0 phase = Phase4.CH3_STABILIZE
return Out(AlignAction.MOVE_DOWN, "↓ ch3 재확인 필요", stabilizeCount = 0
phase, 0, msgKey = R.string.align_recover_down_short) centerOptimizeStreak = 0
centerOptLostHistory.clear()
return Out(AlignAction.MOVE_DOWN, "↓ ch3 재확인 필요",
phase, 0, msgKey = R.string.align_recover_down_short)
}
if (!s.ch3) {
// 단발 flick — 위치 유지. streak 은 리셋해서 CV 통과 재검증 필요.
centerOptimizeStreak = 0
return Out(AlignAction.STOP,
"· CH3 확인 중 · 위치 유지 (flick ${lostCount}/${AlignmentConstants.STABILIZE_LOST_WINDOW})",
phase, 0, msgKey = R.string.align_ch3_soft_hint)
}
} }
if (!center4Detected) {
// 2026-07-06 fix: 미검출 채널 위치로 probe 위치 판단. // 검출 채널 범위 결정 — relaxed = CH0~CH2, normal = CH0~CH3.
// CH0 (+6.89° 위로 tilt, 상단 channel) 만 놓치고 CH3 검출 = probe 가 방광 val centerRange = if (relaxedMode) 0..2 else 0..3
val centerAllDetected = centerRange.all { it < s.detected.size && s.detected[it] }
if (!centerAllDetected) {
// 미검출 채널 위치로 probe 위치 판단.
// CH0 (+6.89° up-tilt, 상단 channel) 만 놓치고 하단 detected = probe 가 방광
// dome 위에 있어 CH0 beam 이 위로 빠짐 → MOVE_DOWN 으로 방광 중심 재정렬. // dome 위에 있어 CH0 beam 이 위로 빠짐 → MOVE_DOWN 으로 방광 중심 재정렬.
// CH1/CH2 만 놓치고 CH0+CH3 검출 = 드물지만 부분적 dropout, default MOVE_UP.
// 상단 (CH0 or CH0+CH1) 만 놓침 = probe too high → MOVE_DOWN. // 상단 (CH0 or CH0+CH1) 만 놓침 = probe too high → MOVE_DOWN.
// 그 외 (CH1 이하만 미검출) = default MOVE_UP.
centerOptimizeStreak = 0 centerOptimizeStreak = 0
val missing = (0..3).filter { !s.detected[it] } val missing = centerRange.filter { !s.detected[it] }
val missingLabel = missing.joinToString(",") { "ch$it" } val missingLabel = missing.joinToString(",") { "ch$it" }
val topOnly = missing.isNotEmpty() && missing.all { it <= 1 } // {0} or {1} or {0,1} val topOnly = missing.isNotEmpty() && missing.all { it <= 1 }
val (direction, arrow, msgKey) = if (topOnly) val (direction, arrow, msgKey) = if (topOnly)
Triple(AlignAction.MOVE_DOWN, "↓", R.string.align_recover_down_short) Triple(AlignAction.MOVE_DOWN, "↓", R.string.align_recover_down_short)
else else
Triple(AlignAction.MOVE_UP, "↑", R.string.align_move_up) Triple(AlignAction.MOVE_UP, "↑", R.string.align_move_up)
return Out(direction, return Out(direction,
"$arrow 조금 ${if (topOnly) "아래" else "더 위"}로 ($missingLabel 미검출)", "$arrow 조금 ${if (topOnly) "아래" else "더 위"}로 ($missingLabel 미검출)",
phase, 0, msgKey = msgKey) phase, 0, msgKey = msgKey, relaxedMode = relaxedMode)
} }
// 4채널 모두 검출됨 → chord CV 계산 (summarize 에서 angle-corrected mm 값 저장됨) // 모두 검출됨 → chord CV 계산.
val chords = DoubleArray(4) { ch -> s.chords[ch] ?: 0.0 } val chordN = centerRange.count()
val chords = DoubleArray(chordN) { i -> s.chords[centerRange.first + i] ?: 0.0 }
val chordMean = chords.average() val chordMean = chords.average()
if (chordMean <= 0.1) { if (chordMean <= 0.1) {
phase = Phase4.VERTICAL_CLIMB phase = Phase4.VERTICAL_CLIMB
verticalHitStreak = 0 verticalHitStreak = 0
verticalHitHistory.clear()
verticalStuckCount = 0
centerOptimizeStreak = 0 centerOptimizeStreak = 0
return Out(AlignAction.MOVE_UP, "↑ 재정렬 필요", phase, 0, return Out(AlignAction.MOVE_UP, "↑ 재정렬 필요", phase, 0,
msgKey = R.string.align_move_up) msgKey = R.string.align_move_up, relaxedMode = relaxedMode)
} }
val variance = chords.map { (it - chordMean) * (it - chordMean) }.average() val variance = chords.map { (it - chordMean) * (it - chordMean) }.average()
val chordStd = kotlin.math.sqrt(variance) val chordStd = kotlin.math.sqrt(variance)
@@ -350,33 +441,35 @@ class AlignGuide4Stage(
if (cv <= AlignmentConstants.CV_THRESHOLD_STRICT) { if (cv <= AlignmentConstants.CV_THRESHOLD_STRICT) {
centerOptimizeStreak++ centerOptimizeStreak++
if (centerOptimizeStreak >= AlignmentConstants.CENTER_OPT_CONFIRM_STREAK) { if (centerOptimizeStreak >= AlignmentConstants.CENTER_OPT_CONFIRM_STREAK) {
bestCenterSet = (0..3).toSet() bestCenterSet = centerRange.toSet()
phase = Phase4.LR_BALANCE phase = Phase4.LR_BALANCE
return Out(AlignAction.STOP, return Out(AlignAction.STOP,
"✓ 최적 vertical 확인 (CV=${"%.2f".format(cv)}) — 좌우 균형 확인", "✓ 최적 vertical 확인 (CV=${"%.2f".format(cv)}${if (relaxedMode) " relaxed" else ""}) — 좌우 균형",
phase, 0, phase, 0,
msgKey = R.string.align_hold_position, msgKey = R.string.align_hold_position,
msgArgs = listOf(centerOptimizeStreak, AlignmentConstants.CENTER_OPT_CONFIRM_STREAK)) msgArgs = listOf(centerOptimizeStreak, AlignmentConstants.CENTER_OPT_CONFIRM_STREAK),
relaxedMode = relaxedMode)
} }
return Out(AlignAction.STOP, return Out(AlignAction.STOP,
"✓ 최적 근접 (CV=${"%.2f".format(cv)}) — 확인 중 " + "✓ 최적 근접 (CV=${"%.2f".format(cv)}) — 확인 중 " +
"(${centerOptimizeStreak}/${AlignmentConstants.CENTER_OPT_CONFIRM_STREAK})", "(${centerOptimizeStreak}/${AlignmentConstants.CENTER_OPT_CONFIRM_STREAK})",
phase, centerOptimizeStreak, phase, centerOptimizeStreak,
msgKey = R.string.align_hold_position, msgKey = R.string.align_hold_position,
msgArgs = listOf(centerOptimizeStreak, AlignmentConstants.CENTER_OPT_CONFIRM_STREAK)) msgArgs = listOf(centerOptimizeStreak, AlignmentConstants.CENTER_OPT_CONFIRM_STREAK),
relaxedMode = relaxedMode)
} }
// CV 너무 큼 → 방향 힌트 // CV 너무 큼 → 방향 힌트 (relaxed 는 CH2 기준, normal 은 CH3 기준).
centerOptimizeStreak = 0 centerOptimizeStreak = 0
val ch0Chord = chords[0] val topChord = chords[0] // CH0
val ch3Chord = chords[3] val botChord = chords[chords.size - 1] // CH3 or CH2
return if (ch3Chord > ch0Chord) { return if (botChord > topChord) {
Out(AlignAction.MOVE_DOWN, Out(AlignAction.MOVE_DOWN,
"↓ 조금 아래로 (CV=${"%.2f".format(cv)}, CH3 극대)", phase, 0, "↓ 조금 아래로 (CV=${"%.2f".format(cv)}, 하단 극대)", phase, 0,
msgKey = R.string.align_recover_down_short) msgKey = R.string.align_recover_down_short, relaxedMode = relaxedMode)
} else { } else {
Out(AlignAction.MOVE_UP, Out(AlignAction.MOVE_UP,
"↑ 조금 위로 (CV=${"%.2f".format(cv)}, CH0 극대)", phase, 0, "↑ 조금 위로 (CV=${"%.2f".format(cv)}, CH0 극대)", phase, 0,
msgKey = R.string.align_recover_up_short) msgKey = R.string.align_recover_up_short, relaxedMode = relaxedMode)
} }
} }
@@ -602,6 +695,7 @@ class RollingAligner(
dets = dets, summary = summary, dets = dets, summary = summary,
accumProgress = if (state == "accum") out.progress else effectiveTarget, accumProgress = if (state == "accum") out.progress else effectiveTarget,
accumTarget = effectiveTarget, accumTarget = effectiveTarget,
relaxedMode = out.relaxedMode,
) )
lastCommit = st lastCommit = st
return st return st
+2
View File
@@ -191,6 +191,8 @@
<string name="align_left">← 왼쪽으로 살짝</string> <string name="align_left">← 왼쪽으로 살짝</string>
<string name="align_right">→ 오른쪽으로 살짝</string> <string name="align_right">→ 오른쪽으로 살짝</string>
<string name="align_probe_lr">↔ 좌우로 천천히 움직여 보세요</string> <string name="align_probe_lr">↔ 좌우로 천천히 움직여 보세요</string>
<string name="align_ch3_soft_hint">CH3 신호 확인 중 · 위치 유지 또는 미세 조정</string>
<string name="align_ch3_relaxed_mode">CH3 신호 미약 — 상단 3채널로 정렬 진행</string>
<!-- Exit Dialog --> <!-- Exit Dialog -->
<string name="exit_app_title">앱 종료</string> <string name="exit_app_title">앱 종료</string>
+2
View File
@@ -191,6 +191,8 @@
<string name="align_left">← Slightly to the left</string> <string name="align_left">← Slightly to the left</string>
<string name="align_right">→ Slightly to the right</string> <string name="align_right">→ Slightly to the right</string>
<string name="align_probe_lr">↔ Move slowly side to side</string> <string name="align_probe_lr">↔ Move slowly side to side</string>
<string name="align_ch3_soft_hint">Verifying CH3 signal · hold or fine-tune</string>
<string name="align_ch3_relaxed_mode">CH3 weak — aligning with upper 3 channels</string>
<!-- Exit Dialog --> <!-- Exit Dialog -->
<string name="exit_app_title">Exit App</string> <string name="exit_app_title">Exit App</string>