refactor: simplify primary workflows

This commit is contained in:
Codex
2026-08-28 18:33:20 +09:00
parent 6e5bd512b9
commit 35c33e360e
5 changed files with 756 additions and 126 deletions
+76 -56
View File
@@ -15,11 +15,24 @@
<strong>micarrsim</strong>
<span>Dataset Builder</span>
</a>
<nav class="builder-workflow" id="builder-workflow" aria-label="Scene 제작 순서">
<ol>
<li data-workflow-step="input"><span>1</span>입력</li>
<li data-workflow-step="edit"><span>2</span>배치·편집</li>
<li data-workflow-step="validate"><span>3</span>검증</li>
<li data-workflow-step="save"><span>4</span>저장</li>
</ol>
</nav>
<div class="header-actions">
<button class="button button--ghost" id="preview-button" type="button">▶ 미리 듣기</button>
<button class="button button--ghost" id="wav-button" type="button" title="공간정보 없이 모든 음원을 합친 mono 확인용 WAV입니다.">Preview WAV 저장</button>
<button class="button button--primary" id="export-button" type="button">Scene JSON 저장</button>
<a class="button button--ghost" href="/apps/acoustic-camera/">Scene 저장 후 Camera 열기 →</a>
<details class="header-more">
<summary class="button button--ghost" aria-label="추가 작업">더보기</summary>
<div class="header-more__menu">
<button class="button button--ghost" id="wav-button" type="button" title="공간정보 없이 모든 음원을 합친 mono 확인용 WAV입니다.">Preview WAV 저장</button>
<a class="button button--ghost" href="/apps/acoustic-camera/">Acoustic Camera 열기 →</a>
</div>
</details>
</div>
</header>
@@ -27,39 +40,40 @@
<section class="scene-workspace" aria-label="장면 편집 화면">
<div class="workspace-heading">
<div>
<p class="eyebrow">Ground-truth canvas</p>
<p class="eyebrow">01 · 입력과 배치</p>
<h1 id="workspace-title">음향 장면</h1>
</div>
<div class="workspace-tools">
<label class="button button--ghost file-button">
Scene 불러오기
<input id="scene-input" type="file" accept="application/json,.json" />
</label>
<label class="button button--ghost file-button">
배경 이미지
<input id="image-input" type="file" accept="image/*" />
</label>
<button class="button button--danger is-hidden" id="remove-image-button" type="button">이미지 해제</button>
<label class="button button--ghost file-button">
원본 오디오 연결
<label class="button button--ghost file-button" title="Scene을 다시 열었을 때 원본 파일을 SHA-256으로 재연결합니다.">
원본 재연결
<input id="audio-input" type="file" accept="audio/*,.wav,.flac,.mp3,.ogg,.m4a" multiple />
</label>
<label class="button button--ghost file-button">
Scene 불러오기
<input id="scene-input" type="file" accept="application/json,.json" />
</label>
</div>
</div>
<div class="canvas-card">
<canvas id="scene-canvas" aria-label="이미지 평면과 음원 ground truth"></canvas>
<div class="canvas-empty" id="canvas-empty">
<strong>이미지를 추가하거나 grid를 그대로 사용하세요.</strong>
<span>선택한 음원을 옮기려면 canvas를 클릭하세요.</span>
<strong>음원을 선택하고 canvas를 클릭해 위치를 지정하세요.</strong>
<span>배경 이미지는 선택 사항입니다.</span>
</div>
<div class="canvas-badge" id="canvas-badge">x −0.60…0.60 m · y −0.45…0.45 m</div>
</div>
<div class="source-strip-heading">
<div>
<h2>음원 ground truth</h2>
<p>파일 음원은 선택 즉시 생성됩니다. 카드를 선택한 뒤 canvas를 클릭해 위치를 지정하세요.</p>
<p class="eyebrow">02 · 음원 편집</p>
<h2>음원과 이동 경로</h2>
<p>카드를 선택한 뒤 canvas를 클릭하면 현재 위치 또는 선택한 경로점이 이동합니다.</p>
</div>
<div class="source-actions">
<button class="button button--ghost" id="add-source-button" type="button">+ 합성 음원</button>
@@ -73,8 +87,11 @@
<section class="asset-shelf">
<div>
<h2>로컬 assets</h2>
<p>원본은 업로드되지 않고 브라우저에서만 처리됩니다. PCM WAV를 권장하며, 다른 codec 지원은 환경에 따라 다릅니다.<br />Scene을 다시 불러온 뒤에는 위의 <strong>원본 오디오 연결</strong>에서 같은 파일을 선택하세요.</p>
<h2>연결된 원본 파일</h2>
<details class="asset-help">
<summary>파일 연결 도움말</summary>
<p>원본은 업로드되지 않고 브라우저에서만 처리됩니다. Scene을 다시 열었다면 위의 <strong>원본 재연결</strong>에서 같은 파일을 선택하세요. PCM WAV를 권장합니다.</p>
</details>
</div>
<div class="asset-list" id="asset-list"></div>
</section>
@@ -86,40 +103,13 @@
<span class="save-state" id="save-state">기본 장면</span>
</div>
<section class="control-section">
<div class="section-title"><span>01</span><h2>장면</h2></div>
<label class="field field--wide">
<span>이름</span>
<input id="scene-name" type="text" maxlength="120" />
</label>
<label class="field field--wide">
<span>Scene ID</span>
<input id="scene-id" type="text" maxlength="120" pattern="[a-z0-9][a-z0-9._-]*" />
</label>
<div class="field-grid">
<label class="field"><span>길이 (s)</span><input id="scene-duration" type="number" min="0.05" max="300" step="0.05" /></label>
<label class="field"><span>Sample rate</span><input id="sample-rate" type="number" min="8000" max="192000" step="1" /></label>
<label class="field"><span>음속 (m/s)</span><input id="speed-of-sound" type="number" min="250" max="400" step="0.1" /></label>
<label class="field"><span>Seed</span><input id="scene-seed" type="number" min="0" max="4294967295" step="1" /></label>
<label class="field"><span>Mix 정규화</span><select id="mix-normalization"><option value="none">없음</option><option value="peak">Peak</option></select></label>
<label class="field"><span>Target peak</span><input id="target-peak" type="number" min="0.01" max="1" step="0.01" /></label>
</div>
<section class="validation-card" aria-live="polite">
<div class="validation-dot" id="validation-dot"></div>
<div><strong id="validation-title">검증 대기</strong><p id="validation-message">장면을 준비하고 있습니다.</p></div>
</section>
<section class="control-section">
<div class="section-title"><span>02</span><h2>표시 평면</h2></div>
<div class="field-grid field-grid--compact">
<label class="field"><span>x min</span><input id="plane-x-min" type="number" step="0.05" /></label>
<label class="field"><span>x max</span><input id="plane-x-max" type="number" step="0.05" /></label>
<label class="field"><span>y min</span><input id="plane-y-min" type="number" step="0.05" /></label>
<label class="field"><span>y max</span><input id="plane-y-max" type="number" step="0.05" /></label>
<label class="field"><span>plane z (m)</span><input id="plane-z" type="number" min="0.01" step="0.1" /></label>
</div>
<p class="section-note">x/y는 편집 화면 범위이며, 배경 이미지가 있을 때 image plane으로 JSON에 저장됩니다.</p>
</section>
<section class="control-section" id="source-editor-section">
<div class="section-title"><span>03</span><h2>선택 음원</h2></div>
<section class="control-section control-section--primary" id="source-editor-section">
<div class="section-title"><span>02</span><h2>선택 음원 편집</h2></div>
<div id="no-source-message" class="inline-note">음원을 선택하세요.</div>
<div id="source-fields">
<label class="field field--wide"><span>이름</span><input id="source-name" type="text" maxlength="120" /></label>
@@ -135,13 +125,13 @@
</div>
<section class="trajectory-editor" aria-label="음원 이동 경로">
<label class="check-field trajectory-toggle"><input id="source-motion" type="checkbox" /><span>시간에 따라 음원 이동</span></label>
<p class="section-note">경로 시간은 Scene 시작 기준입니다. 이동을 켜면 현재 위치에서 시작해 Scene 끝까지 이어지는 두 점 경로가 만들어집니다.</p>
<p class="section-note">켜면 현재 위치부터 Scene 끝까지의 기본 경로가 만들어집니다.</p>
<div id="trajectory-fields" class="is-hidden">
<label class="field field--wide trajectory-preview">
<span>경로 미리보기 · <output id="preview-time-value" for="preview-time">0.00 s</output></span>
<input id="preview-time" type="range" min="0" max="1" step="0.01" value="0" />
</label>
<p class="section-note">경로점을 선택하면 canvas 클릭으로 그 점의 x/y를 옮길 수 있습니다. 흰 테두리 점은 현재 선택한 경로점입니다.</p>
<p class="section-note">경로점을 선택하고 canvas를 클릭해 x/y를 옮기세요.</p>
<div class="trajectory-list" id="trajectory-list"></div>
<button class="button button--ghost button--full" id="add-keyframe-button" type="button">+ 경로점 추가</button>
</div>
@@ -161,7 +151,42 @@
</section>
<section class="control-section">
<div class="section-title"><span>04</span><h2>Ambient</h2></div>
<div class="section-title"><span>설정</span><h2>장면 기본값</h2></div>
<label class="field field--wide">
<span>이름</span>
<input id="scene-name" type="text" maxlength="120" />
</label>
<label class="field field--wide">
<span>Scene ID</span>
<input id="scene-id" type="text" maxlength="120" pattern="[a-z0-9][a-z0-9._-]*" />
</label>
<label class="field field--wide"><span>길이 (s)</span><input id="scene-duration" type="number" min="0.05" max="300" step="0.05" /></label>
<details class="inline-disclosure">
<summary>고급 음향·믹스 설정</summary>
<div class="field-grid">
<label class="field"><span>Sample rate</span><input id="sample-rate" type="number" min="8000" max="192000" step="1" /></label>
<label class="field"><span>음속 (m/s)</span><input id="speed-of-sound" type="number" min="250" max="400" step="0.1" /></label>
<label class="field"><span>Seed</span><input id="scene-seed" type="number" min="0" max="4294967295" step="1" /></label>
<label class="field"><span>Mix 정규화</span><select id="mix-normalization"><option value="none">없음</option><option value="peak">Peak</option></select></label>
<label class="field"><span>Target peak</span><input id="target-peak" type="number" min="0.01" max="1" step="0.01" /></label>
</div>
</details>
</section>
<details class="control-section disclosure-section">
<summary class="section-summary"><span>범위</span><strong>표시 평면</strong><small>고급</small></summary>
<div class="field-grid field-grid--compact">
<label class="field"><span>x min</span><input id="plane-x-min" type="number" step="0.05" /></label>
<label class="field"><span>x max</span><input id="plane-x-max" type="number" step="0.05" /></label>
<label class="field"><span>y min</span><input id="plane-y-min" type="number" step="0.05" /></label>
<label class="field"><span>y max</span><input id="plane-y-max" type="number" step="0.05" /></label>
<label class="field"><span>plane z (m)</span><input id="plane-z" type="number" min="0.01" step="0.1" /></label>
</div>
<p class="section-note">x/y는 canvas 범위이며 배경이 있으면 image plane으로 저장됩니다.</p>
</details>
<details class="control-section disclosure-section">
<summary class="section-summary"><span>배경음</span><strong>Ambient</strong><small>선택 사항</small></summary>
<label class="field field--wide"><span>종류</span><select id="ambient-type"><option value="none">없음</option><option value="white">White noise</option><option value="pink">Pink noise</option><option value="asset">Audio asset</option></select></label>
<label class="field field--wide" id="ambient-asset-field"><span>Audio asset</span><select id="ambient-asset"></select></label>
<div class="field-grid" id="ambient-asset-options">
@@ -169,12 +194,7 @@
<label class="check-field"><input id="ambient-loop" type="checkbox" /><span>짧은 파일 반복</span></label>
</div>
<label class="field field--wide"><span>Gain (dB)</span><input id="ambient-gain" type="number" min="-120" max="24" step="1" /></label>
</section>
<section class="validation-card" aria-live="polite">
<div class="validation-dot" id="validation-dot"></div>
<div><strong id="validation-title">검증 대기</strong><p id="validation-message">장면을 준비하고 있습니다.</p></div>
</section>
</details>
</aside>
</main>
</div>
+360
View File
@@ -643,3 +643,363 @@
grid-row: 2;
}
}
/* Task-first Builder navigation and progressive disclosure. */
.builder-header {
min-height: 58px;
padding-block: 9px;
gap: 16px;
}
.builder-workflow {
min-width: 0;
margin-left: auto;
}
.builder-workflow ol {
display: flex;
align-items: center;
margin: 0;
padding: 0;
list-style: none;
}
.builder-workflow li {
display: flex;
align-items: center;
gap: 6px;
color: var(--muted);
font-size: 0.68rem;
font-weight: 680;
white-space: nowrap;
}
.builder-workflow li + li::before {
width: 18px;
height: 1px;
margin: 0 7px;
background: var(--line);
content: "";
}
.builder-workflow li span {
display: grid;
width: 20px;
height: 20px;
border: 1px solid rgb(101 242 191 / 34%);
border-radius: 50%;
place-items: center;
color: var(--mint);
font: 750 0.6rem ui-monospace, SFMono-Regular, Consolas, monospace;
}
.header-more {
position: relative;
}
.header-more > summary {
list-style: none;
}
.header-more > summary::-webkit-details-marker,
.inline-disclosure > summary::-webkit-details-marker,
.section-summary::-webkit-details-marker,
.asset-help > summary::-webkit-details-marker {
display: none;
}
.header-more__menu {
position: absolute;
z-index: 30;
top: calc(100% + 8px);
right: 0;
display: grid;
width: max-content;
min-width: 210px;
gap: 8px;
padding: 10px;
border: 1px solid var(--line);
border-radius: 12px;
background: #0b1714;
box-shadow: 0 18px 48px rgb(0 0 0 / 35%);
}
.header-more__menu .button {
width: 100%;
}
.builder-layout {
grid-template-columns: minmax(0, 1fr) minmax(320px, 370px);
min-height: calc(100vh - 59px);
}
.scene-workspace {
padding: clamp(20px, 2.5vw, 34px);
}
.workspace-heading {
margin-bottom: 16px;
}
.workspace-heading h1 {
font-size: clamp(1.55rem, 3vw, 2.35rem);
}
.source-strip-heading .eyebrow {
margin-bottom: 5px;
font-size: 0.64rem;
}
.source-strip-heading {
margin-top: 24px;
}
.asset-shelf {
align-items: center;
margin-top: 20px;
padding: 14px 16px;
}
.asset-help {
color: var(--muted);
font-size: 0.7rem;
}
.asset-help summary {
width: fit-content;
margin-top: 4px;
color: var(--mint);
cursor: pointer;
}
.asset-help p {
max-width: 520px;
margin-top: 9px;
}
.control-panel {
top: 59px;
max-height: calc(100vh - 59px);
padding: 14px;
}
.control-section--primary {
border-color: rgb(101 242 191 / 28%);
background: rgb(101 242 191 / 3%);
}
.validation-card {
margin-top: 0;
margin-bottom: 12px;
padding: 13px 15px;
}
.control-section + .validation-card {
margin-top: 12px;
}
.inline-disclosure {
margin-top: 12px;
border-top: 1px solid var(--line);
}
.inline-disclosure > summary {
padding: 12px 0 0;
color: var(--muted);
cursor: pointer;
font-size: 0.7rem;
font-weight: 700;
}
.inline-disclosure > summary::after {
float: right;
color: var(--mint);
content: "+";
}
.inline-disclosure[open] > summary::after {
content: "−";
}
.disclosure-section {
padding: 0;
}
.section-summary {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 9px;
padding: 15px 17px;
cursor: pointer;
list-style: none;
}
.section-summary > span {
color: var(--mint);
font: 750 0.62rem ui-monospace, SFMono-Regular, Consolas, monospace;
}
.section-summary > strong {
font-size: 0.82rem;
}
.section-summary > small {
color: var(--muted);
font-size: 0.62rem;
}
.disclosure-section[open] {
padding: 0 17px 17px;
}
.disclosure-section[open] .section-summary {
margin: 0 -17px 14px;
border-bottom: 1px solid var(--line);
}
@media (max-width: 1180px) {
.builder-workflow li {
font-size: 0;
gap: 0;
}
.builder-workflow li + li::before {
width: 12px;
margin-inline: 5px;
}
}
@media (max-width: 1100px) {
.control-panel {
max-height: none;
}
}
@media (max-width: 760px) {
.builder-header {
gap: 10px;
}
.builder-workflow {
order: 3;
width: 100%;
margin: 0;
}
.builder-workflow ol {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.builder-workflow li {
display: grid;
grid-template-columns: 20px minmax(0, 1fr);
gap: 5px;
font-size: 0.64rem;
}
.builder-workflow li + li::before {
display: none;
}
.header-actions {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.header-actions > .button,
.header-actions > .header-more,
.header-more > summary {
width: 100%;
}
.header-more__menu {
top: calc(100% + 6px);
}
.asset-shelf {
align-items: stretch;
}
}
@media (max-width: 430px) {
.builder-header,
.scene-workspace,
.control-panel {
padding-inline: 14px;
}
.builder-workflow li {
grid-template-columns: 18px minmax(0, 1fr);
font-size: 0.59rem;
}
.builder-workflow li span {
width: 18px;
height: 18px;
}
.header-actions {
gap: 6px;
}
.header-actions .button {
min-height: 36px;
padding-inline: 7px;
font-size: 0.72rem;
}
.header-more__menu {
right: -2px;
min-width: 190px;
}
.canvas-card {
min-height: 360px;
}
#scene-canvas {
height: 360px;
}
.workspace-tools .button,
.source-actions .button {
font-size: 0.74rem;
}
.trajectory-row {
grid-template-columns: 34px repeat(2, minmax(0, 1fr)) 30px;
grid-template-rows: repeat(2, auto);
}
.trajectory-row__select,
.trajectory-row__remove {
grid-row: 1 / 3;
align-self: stretch;
}
.trajectory-row .field:nth-of-type(1) {
grid-column: 2;
grid-row: 1;
}
.trajectory-row .field:nth-of-type(2) {
grid-column: 3;
grid-row: 1;
}
.trajectory-row .field:nth-of-type(3) {
grid-column: 2;
grid-row: 2;
}
.trajectory-row .field:nth-of-type(4) {
grid-column: 3;
grid-row: 2;
}
.trajectory-row__remove {
grid-column: 4;
}
}