Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0289afa0e | ||
|
|
a9799e4920 |
Binary file not shown.
|
After Width: | Height: | Size: 285 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
+527
-225
@@ -1,343 +1,645 @@
|
|||||||
/* AIGen display tokens — Classic | Studio
|
/* Pixel grid tokens — Classic | Studio. Match TARGET-classic-layout.png */
|
||||||
Apply on [data-display] wrapper. Do not fork components.
|
|
||||||
Copied from studio2-display-ux/tokens/display.css */
|
|
||||||
|
|
||||||
:root,
|
:root,
|
||||||
[data-display='studio'] {
|
[data-display='studio'] {
|
||||||
--bg: #0e0e10;
|
--bg: #0b0b0d;
|
||||||
--bg-raise: #161618;
|
--panel: #141416;
|
||||||
--bg-slot: #1c1c1f;
|
--panel-raise: #1a1a1d;
|
||||||
--line: #2a2a2e;
|
--line: #2a2a2e;
|
||||||
--text: #f3f1ea;
|
--text: #f3f1ea;
|
||||||
--muted: #9a9588;
|
--muted: #9a9588;
|
||||||
--amber: #f5a524;
|
--amber: #f5a524;
|
||||||
--amber-dim: #c4841c;
|
--amber-on: #1a1204;
|
||||||
|
--chip-bg: #2a2418;
|
||||||
|
--chip-line: #5a4a28;
|
||||||
|
--chip-text: #e8c37a;
|
||||||
|
--success: #7dda6f;
|
||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
--chrome-gap: 16px;
|
--page-gap: 12px;
|
||||||
--rail-h: 40px;
|
--field-gap: 12px;
|
||||||
--card-pad: 14px;
|
--header-h: 48px;
|
||||||
--header-h: 52px;
|
--rail-task-h: 40px;
|
||||||
|
--rail-model-h: 36px;
|
||||||
|
--left-w: 260px;
|
||||||
|
--right-w: 280px;
|
||||||
--type: 14px;
|
--type: 14px;
|
||||||
--type-sm: 12px;
|
--type-sm: 12px;
|
||||||
|
--type-label: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display='classic'] {
|
[data-display='classic'] {
|
||||||
--chrome-gap: 8px;
|
--field-gap: 8px;
|
||||||
--rail-h: 32px;
|
|
||||||
--card-pad: 10px;
|
|
||||||
--header-h: 44px;
|
|
||||||
--type: 13px;
|
--type: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] {
|
[data-display] {
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||||
font-size: var(--type);
|
font-size: var(--type);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-app {
|
/* Skin toggles */
|
||||||
display: flex;
|
[data-display='classic'] .rail-task,
|
||||||
min-height: 100%;
|
[data-display='classic'] .rail-model,
|
||||||
flex-direction: column;
|
[data-display='classic'] .identity-slots,
|
||||||
gap: var(--chrome-gap);
|
[data-display='classic'] .eye-viewer,
|
||||||
padding: var(--chrome-gap);
|
[data-display='classic'] .eye-tile {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
[data-display='studio'] .classic-task-model,
|
||||||
|
[data-display='studio'] .viewer-lock-bar {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-top {
|
.bench-root {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
height: 100dvh;
|
||||||
|
max-height: 100dvh;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bench-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
align-items: center;
|
||||||
height: var(--header-h);
|
height: var(--header-h);
|
||||||
min-height: var(--header-h);
|
min-height: var(--header-h);
|
||||||
flex-wrap: wrap;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
border-bottom: 1px solid var(--line);
|
||||||
gap: 12px;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
background: var(--bg-raise);
|
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-brand {
|
.bench-brand {
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-grow {
|
.bench-crumb {
|
||||||
flex: 1;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--type-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-nav {
|
.bench-crumb select {
|
||||||
|
max-width: 14rem;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bench-header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bench-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
font-size: var(--type-sm);
|
font-size: var(--type-sm);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-nav a {
|
.bench-nav a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
outline: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-nav a:hover,
|
.bench-nav a:hover,
|
||||||
[data-display] .display-nav a.router-link-active {
|
.bench-nav a.router-link-active {
|
||||||
color: var(--text);
|
color: var(--amber);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-nav a:focus-visible {
|
.gpu-chip {
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
display: inline-flex;
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .identity-header,
|
|
||||||
[data-display] .classic-hide,
|
|
||||||
[data-display] .identity-slots {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--panel-raise);
|
||||||
|
padding: 8px 12px;
|
||||||
font-size: var(--type-sm);
|
font-size: var(--type-sm);
|
||||||
color: var(--muted);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .identity-slots {
|
.gpu-chip .dot {
|
||||||
flex-direction: column;
|
width: 8px;
|
||||||
align-items: flex-start;
|
height: 8px;
|
||||||
margin-top: 8px;
|
border-radius: 999px;
|
||||||
|
background: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .identity-slots .hint {
|
.gpu-chip[data-tone='ready'] .dot { background: var(--success); }
|
||||||
margin: 6px 0 0;
|
.gpu-chip[data-tone='busy'] .dot { background: var(--amber); animation: pulse 1.2s infinite; }
|
||||||
font-size: 11px;
|
|
||||||
color: var(--muted);
|
@keyframes pulse {
|
||||||
|
50% { opacity: 0.45; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .rail {
|
.rail {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .seg {
|
.rail-task { min-height: var(--rail-task-h); align-items: center; padding-top: 8px; }
|
||||||
height: var(--rail-h);
|
.rail-model { min-height: var(--rail-model-h); align-items: center; padding-bottom: 8px; }
|
||||||
cursor: pointer;
|
|
||||||
|
.seg {
|
||||||
|
height: 36px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--bg-slot);
|
background: var(--panel);
|
||||||
padding: 0 12px;
|
padding: 0 14px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: var(--type);
|
font-size: var(--type);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: capitalize;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .seg:focus-visible {
|
.rail-model .seg { height: 32px; font-weight: 500; }
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .seg[aria-pressed='true'] {
|
.seg[aria-pressed='true'] {
|
||||||
border-color: var(--amber);
|
border-color: var(--amber);
|
||||||
background: #2a1f0c;
|
background: var(--amber);
|
||||||
color: var(--amber);
|
color: var(--amber-on);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .seg:disabled,
|
.seg:disabled {
|
||||||
[data-display] .seg[aria-disabled='true'] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .classic-selects {
|
.seg:focus-visible {
|
||||||
display: flex;
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 55%, transparent);
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .classic-selects select {
|
.bench-grid {
|
||||||
height: var(--rail-h);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
background: var(--bg-slot);
|
|
||||||
padding: 0 8px;
|
|
||||||
color: var(--text);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .classic-selects select:focus-visible {
|
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .display-grid {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
gap: var(--chrome-gap);
|
min-height: 0;
|
||||||
min-width: 0;
|
grid-template-columns: var(--left-w) minmax(0, 1fr) var(--right-w);
|
||||||
|
gap: var(--page-gap);
|
||||||
|
padding: var(--page-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
.bench-panel {
|
||||||
[data-display] .display-grid {
|
|
||||||
grid-template-columns: minmax(14rem, 15rem) minmax(0, 1fr) minmax(16rem, 18rem);
|
|
||||||
}
|
|
||||||
[data-display] .display-grid--music {
|
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(16rem, 18rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .display-col {
|
|
||||||
min-width: 0;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
background: var(--bg-raise);
|
|
||||||
padding: var(--card-pad);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .display-viewer {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
min-height: 320px;
|
|
||||||
place-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
background: #111;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .eye-viewer {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
z-index: 10;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #000a;
|
|
||||||
padding: 4px;
|
|
||||||
color: var(--text);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .eye-viewer:focus-visible,
|
|
||||||
[data-display] .eye-tile:focus-visible {
|
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .display-strip {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
min-width: 0;
|
||||||
margin-top: 10px;
|
min-height: 0;
|
||||||
max-width: 100%;
|
flex-direction: column;
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .display-tile {
|
|
||||||
position: relative;
|
|
||||||
width: 72px;
|
|
||||||
height: 48px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 6px;
|
|
||||||
background: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .display-tile[data-active='true'] {
|
|
||||||
border-color: var(--amber);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .eye-tile {
|
|
||||||
position: absolute;
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
display: grid;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
place-items: center;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: #000a;
|
|
||||||
padding: 0;
|
|
||||||
color: var(--text);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display] .display-cta {
|
|
||||||
width: 100%;
|
|
||||||
height: 44px;
|
|
||||||
margin-top: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--amber);
|
background: var(--panel);
|
||||||
color: #1a1204;
|
|
||||||
font-weight: 700;
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-cta:focus-visible {
|
.bench-scroll {
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-cta:disabled {
|
.inspector {
|
||||||
cursor: not-allowed;
|
display: flex;
|
||||||
opacity: 0.5;
|
flex-direction: column;
|
||||||
|
gap: var(--field-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-field {
|
.field-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--type-label);
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-control {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--bg-slot);
|
background: var(--panel-raise);
|
||||||
padding: 8px;
|
padding: 0 10px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-field:focus-visible {
|
.field-control:focus-visible,
|
||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 50%, transparent);
|
.field-area:focus-visible {
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--amber) 55%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-gpu-pill {
|
.field-area {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 88px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--panel-raise);
|
||||||
|
padding: 8px 10px;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
resize: vertical;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neg-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.neg-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border: 1px solid var(--chip-line);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--chip-bg);
|
||||||
|
padding: 4px 10px;
|
||||||
|
color: var(--chip-text);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.neg-chip button {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seed-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seed-row .field-control { flex: 1; }
|
||||||
|
|
||||||
|
.dice-btn {
|
||||||
|
display: grid;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
place-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--panel-raise);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-lock-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-stack {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-frame {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 360px;
|
||||||
|
place-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #0a0a0c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-frame img,
|
||||||
|
.viewer-frame video {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye-viewer {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 5;
|
||||||
|
display: grid;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #000a;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filmstrip {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 8px;
|
||||||
|
height: 80px;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: auto;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
position: relative;
|
||||||
|
width: 96px;
|
||||||
|
height: 64px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1440px) {
|
||||||
|
.tile { width: 120px; height: 80px; }
|
||||||
|
.filmstrip { height: 96px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile[data-active='true'] {
|
||||||
|
border-color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile button.pick {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile .eye-tile,
|
||||||
|
.tile .tile-trash {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
|
display: grid;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #000a;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile .tile-trash { right: 22px; }
|
||||||
|
|
||||||
|
.transport {
|
||||||
|
display: flex;
|
||||||
|
height: 28px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding: 0 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transport .ops {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transport button {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-col {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-head {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 12px 8px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-badge {
|
||||||
|
display: inline-grid;
|
||||||
|
min-width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--amber);
|
||||||
|
color: var(--amber-on);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-card {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--panel-raise);
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-card[data-active='true'] {
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-id {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-title {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-meta {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-status {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-status .left {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-status .dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid #2d4a2a;
|
background: var(--success);
|
||||||
background: #142016;
|
|
||||||
padding: 4px 10px;
|
|
||||||
font-size: var(--type-sm);
|
|
||||||
color: #b8e0a8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-gpu-pill[data-tone='busy'] {
|
.job-status[data-state='queued'] .dot,
|
||||||
border-color: color-mix(in srgb, var(--amber) 40%, transparent);
|
.job-status[data-state='waiting'] .dot {
|
||||||
background: #2a1f0c;
|
background: var(--muted);
|
||||||
color: var(--amber);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-display] .display-gpu-pill[data-tone='idle'],
|
.kebab {
|
||||||
[data-display] .display-gpu-pill[data-tone='off'] {
|
border: 0;
|
||||||
border-color: var(--line);
|
background: transparent;
|
||||||
background: var(--bg-slot);
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Skin toggles — keep after base rules so display:none wins */
|
.queue-foot {
|
||||||
[data-display='classic'] .rail-task,
|
flex-shrink: 0;
|
||||||
[data-display='classic'] .rail-model {
|
border-top: 1px solid var(--line);
|
||||||
display: none !important;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
[data-display='studio'] .classic-selects {
|
|
||||||
display: none !important;
|
.generate-cta {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--amber);
|
||||||
|
color: var(--amber-on);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
[data-display='classic'] .identity-slots {
|
|
||||||
display: none !important;
|
.generate-cta:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
[data-display='studio'] .identity-header {
|
|
||||||
display: none !important;
|
.queue-foot .tiny {
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
[data-display='classic'] .eye-tile,
|
|
||||||
[data-display='classic'] .eye-viewer {
|
.queue-foot .tiny button {
|
||||||
display: none !important;
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
[data-display='studio'] .classic-hide {
|
|
||||||
display: none !important;
|
.slot-block { width: 100%; }
|
||||||
|
|
||||||
|
.slot-thumb {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: #0a0a0c;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-thumb--hero { height: 148px; }
|
||||||
|
.slot-thumb--slot { height: 96px; }
|
||||||
|
.slot-thumb--empty {
|
||||||
|
height: 72px;
|
||||||
|
border-style: dashed;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-thumb img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-caption {
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-upload {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--amber);
|
||||||
|
font-size: var(--type-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-still {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--type-sm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-36 shrink-0 space-y-2"><span class="text-xs text-zinc-400">{{ label }}</span><button class="flex h-24 w-full items-center justify-center overflow-hidden rounded-xl border border-white/15 bg-black" @click="$emit('pick')"><span v-if="asset && hidden" class="p-2 text-xs text-zinc-400">Still loaded · preview hidden</span><img v-else-if="asset" :src="asset.kind==='clip'?`/api/library/clips/${asset.id}/thumb`:`/api/library/stills/${asset.id}`" class="h-full w-full object-contain" :alt="label"><span v-else class="text-xs text-zinc-400">Choose {{ label.toLowerCase() }}</span></button><p v-if="asset" class="truncate text-xs" :title="assetLabel(asset)">{{ assetLabel(asset) }}</p><label v-if="upload" class="block cursor-pointer text-xs text-amber-200">Upload {{ label.toLowerCase() }}<input type="file" accept="image/*" class="sr-only" :disabled="disabled" @change="$emit('upload',$event)"></label></div>
|
<div class="slot-block">
|
||||||
|
<span class="field-label">{{ label }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="slot-thumb"
|
||||||
|
:class="asset ? (size==='hero' ? 'slot-thumb--hero' : 'slot-thumb--slot') : 'slot-thumb--empty'"
|
||||||
|
@click="$emit('pick')"
|
||||||
|
>
|
||||||
|
<span v-if="asset && hidden" class="p-2 text-center text-[var(--type-sm)] text-[var(--muted)]">Still loaded · preview hidden</span>
|
||||||
|
<img v-else-if="asset" :src="asset.kind==='clip' ? `/api/library/clips/${asset.id}/thumb` : `/api/library/stills/${asset.id}`" :alt="label">
|
||||||
|
<span v-else class="inline-flex items-center gap-1">Select <span aria-hidden="true">▾</span></span>
|
||||||
|
</button>
|
||||||
|
<p v-if="asset" class="slot-caption" :title="assetLabel(asset)">{{ assetLabel(asset) }}</p>
|
||||||
|
<label v-if="upload" class="slot-upload">Upload {{ label.toLowerCase() }}
|
||||||
|
<input type="file" accept="image/*" class="sr-only" :disabled="disabled" @change="$emit('upload',$event)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {assetLabel} from '~/shared/studio2/viewer.mjs'
|
import {assetLabel} from '~/shared/studio2/viewer.mjs'
|
||||||
defineProps<{label:string;asset?:any;hidden?:boolean;upload?:boolean;disabled?:boolean}>()
|
withDefaults(defineProps<{label:string;asset?:any;hidden?:boolean;upload?:boolean;disabled?:boolean;size?:'hero'|'slot'}>(),{size:'slot'})
|
||||||
defineEmits(['pick','upload'])
|
defineEmits(['pick','upload'])
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+335
-191
@@ -1,27 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="display-app w-full min-w-0" :data-display="displayMode">
|
<div class="bench-root" :data-display="displayMode">
|
||||||
<header class="display-top">
|
<header class="bench-header">
|
||||||
<span class="display-brand">{{ instanceName }}</span>
|
<div class="bench-brand">{{ instanceName }}</div>
|
||||||
<label class="text-[var(--type-sm)] text-[var(--muted)]">Folder
|
<div class="bench-crumb" aria-label="Folder">
|
||||||
<select v-model="form.folderId" class="ml-2 h-[var(--rail-h)] rounded-[var(--radius)] border border-[var(--line)] bg-[var(--bg-slot)] px-2 text-[var(--text)] outline-none focus-visible:ring-2 focus-visible:ring-[color-mix(in_srgb,var(--amber)_50%,transparent)]">
|
<span aria-hidden="true">📁</span>
|
||||||
|
<span>/ Projects /</span>
|
||||||
|
<select v-model="form.folderId" :aria-label="'Folder'">
|
||||||
<option v-for="f in library.folders" :key="f.id" :value="f.id">{{ f.name }}</option>
|
<option v-for="f in library.folders" :key="f.id" :value="f.id">{{ f.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
|
||||||
<div class="display-grow" />
|
|
||||||
<div v-if="form.mode!=='music'" class="identity-header" aria-label="Identity locks">
|
|
||||||
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockFace" type="checkbox" class="accent-[var(--amber)]">Lock face</label>
|
|
||||||
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockOutfit" type="checkbox" class="accent-[var(--amber)]">Lock outfit</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="xaigen" class="classic-hide" aria-label="Preview hide">
|
<div class="bench-header-right">
|
||||||
<label class="inline-flex items-center gap-1.5"><input v-model="classicHideInput" type="checkbox" class="accent-[var(--amber)]">Hide input</label>
|
<nav class="bench-nav" aria-label="Studio links">
|
||||||
<label class="inline-flex items-center gap-1.5"><input v-model="classicHideOutput" type="checkbox" class="accent-[var(--amber)]">Hide output</label>
|
<NuxtLink to="/library">Library</NuxtLink>
|
||||||
|
<NuxtLink to="/queue">Queue</NuxtLink>
|
||||||
|
<NuxtLink to="/settings">Settings</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
<span class="gpu-chip" :data-tone="gpu.tone" role="status" :title="gpu.detail">
|
||||||
|
<span class="dot" aria-hidden="true" />
|
||||||
|
{{ gpuChipLabel }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<nav class="display-nav" aria-label="Studio links">
|
|
||||||
<NuxtLink to="/library">Library</NuxtLink>
|
|
||||||
<NuxtLink to="/queue">Queue</NuxtLink>
|
|
||||||
<NuxtLink to="/settings">Settings</NuxtLink>
|
|
||||||
</nav>
|
|
||||||
<span class="display-gpu-pill" :data-tone="gpu.tone" role="status" :title="gpu.detail">{{ gpu.label }}</span>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="rail rail-task" aria-label="Tasks">
|
<nav class="rail rail-task" aria-label="Tasks">
|
||||||
@@ -29,12 +27,11 @@
|
|||||||
v-for="task in tasks"
|
v-for="task in tasks"
|
||||||
:key="task"
|
:key="task"
|
||||||
type="button"
|
type="button"
|
||||||
class="seg capitalize"
|
class="seg"
|
||||||
:aria-pressed="form.mode===task"
|
:aria-pressed="form.mode===task"
|
||||||
@click="chooseTask(task)"
|
@click="chooseTask(task)"
|
||||||
>{{ task }}</button>
|
>{{ task }}</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div v-if="form.mode!=='music'" class="rail rail-model" role="group" aria-label="Model">
|
<div v-if="form.mode!=='music'" class="rail rail-model" role="group" aria-label="Model">
|
||||||
<button
|
<button
|
||||||
v-for="opt in engineOptions"
|
v-for="opt in engineOptions"
|
||||||
@@ -42,193 +39,246 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="seg"
|
class="seg"
|
||||||
:aria-pressed="form.engine===opt.value"
|
:aria-pressed="form.engine===opt.value"
|
||||||
:aria-disabled="opt.disabled || undefined"
|
|
||||||
:disabled="opt.disabled"
|
:disabled="opt.disabled"
|
||||||
:title="opt.hint || undefined"
|
:title="opt.hint || undefined"
|
||||||
@click="!opt.disabled && (form.engine=opt.value)"
|
@click="!opt.disabled && (form.engine=opt.value)"
|
||||||
>
|
>{{ opt.label }}</button>
|
||||||
{{ opt.label }}<span v-if="opt.hint" class="ml-1 text-[11px] opacity-80">· {{ opt.hint }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="rail-model text-[var(--type-sm)] text-[var(--muted)]">Model · YuE2</p>
|
<p v-else class="rail rail-model text-[var(--muted)]">YuE2</p>
|
||||||
|
|
||||||
<div class="classic-selects">
|
<div class="bench-grid">
|
||||||
<select :value="form.mode" class="capitalize" aria-label="Task" @change="onTaskSelect">
|
<!-- LEFT inspector -->
|
||||||
<option v-for="task in tasks" :key="task" :value="task" class="capitalize">{{ task }}</option>
|
<aside class="bench-panel">
|
||||||
</select>
|
<div class="bench-scroll inspector">
|
||||||
<select v-if="form.mode!=='music'" v-model="form.engine" aria-label="Model">
|
<div class="classic-task-model space-y-2">
|
||||||
<option v-for="opt in engineOptions" :key="opt.value" :value="opt.value" :disabled="opt.disabled">{{ opt.label }}{{ opt.hint ? ' — '+opt.hint : '' }}</option>
|
<label>
|
||||||
</select>
|
<span class="field-label">Task</span>
|
||||||
<span v-else class="flex h-[var(--rail-h)] items-center text-[var(--type-sm)] text-[var(--muted)]">YuE2</span>
|
<select :value="form.mode" class="field-control capitalize" @change="onTaskSelect">
|
||||||
</div>
|
<option v-for="task in tasks" :key="task" :value="task" class="capitalize">{{ task }}</option>
|
||||||
|
</select>
|
||||||
<div class="display-grid" :class="form.mode==='music' ? 'display-grid--music' : ''">
|
</label>
|
||||||
<aside v-if="form.mode!=='music'" class="display-col space-y-2">
|
<label v-if="form.mode!=='music'">
|
||||||
<Studio2AssetSlot label="Hero" :asset="stills.find((a:any)=>a.id===form.identityStillId)" :hidden="slotHidden(form.identityStillId)" upload :disabled="uploading" @pick="picker='identityStillId'" @upload="upload($event,'hero')" />
|
<span class="field-label">Model</span>
|
||||||
<Studio2AssetSlot :label="form.mode==='extend'?'Previous clip':'Start still'" :asset="sourceSlot" :hidden="slotHidden(form.mode==='extend'?form.startClipId:form.imageAId)" upload :disabled="uploading" @pick="picker=form.mode==='extend'?'startClipId':'imageAId'" @upload="upload($event,'source')" />
|
<select v-model="form.engine" class="field-control">
|
||||||
<Studio2AssetSlot v-if="form.mode==='compose'" label="Second still" :asset="stills.find((a:any)=>a.id===form.imageBId)" :hidden="slotHidden(form.imageBId)" @pick="picker='imageBId'" />
|
<option v-for="opt in engineOptions" :key="opt.value" :value="opt.value" :disabled="opt.disabled">{{ opt.label }}{{ opt.hint ? ' — '+opt.hint : '' }}</option>
|
||||||
<Studio2AssetSlot v-if="form.mode==='refine'" label="Mask" :asset="stills.find((a:any)=>a.id===form.maskId)" :hidden="slotHidden(form.maskId)" @pick="picker='maskId'" />
|
</select>
|
||||||
<Studio2AssetSlot v-if="video" label="End still" :asset="stills.find((a:any)=>a.id===form.endStillId)" :hidden="slotHidden(form.endStillId)" @pick="picker='endStillId'" />
|
</label>
|
||||||
<div class="identity-slots" aria-label="Identity locks">
|
<p v-else class="text-[var(--type-sm)] text-[var(--muted)]">Model · YuE2</p>
|
||||||
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockFace" type="checkbox" class="accent-[var(--amber)]">Lock face</label>
|
|
||||||
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockOutfit" type="checkbox" class="accent-[var(--amber)]">Lock outfit</label>
|
|
||||||
<p class="hint">hero encode + lock text — not a MiniMax face/clothes split on stills</p>
|
|
||||||
</div>
|
|
||||||
<label v-if="form.mode==='refine'" class="block text-[var(--type-sm)]">Mask still (white = change). On-canvas mask comes later.
|
|
||||||
<select v-model="form.maskId" class="display-field mt-1"><option value="">Choose mask still</option><option v-for="a in stills" :key="a.id" :value="a.id">{{ assetLabel(a) }}</option></select>
|
|
||||||
</label>
|
|
||||||
<p v-if="uploading || uploadMessage" role="status" class="text-[var(--type-sm)] text-[var(--amber)]">{{ uploading?'Uploading image…':uploadMessage }}</p>
|
|
||||||
<details v-if="video" class="rounded-[var(--radius)] border border-[var(--line)] p-2"><summary class="text-[var(--type-sm)]">Add Guide stills</summary>
|
|
||||||
<div v-for="(g,i) in form.guides" :key="i" class="mt-2 flex gap-2">
|
|
||||||
<Studio2AssetSlot label="Guide" :asset="stills.find((a:any)=>a.id===g.stillId)" :hidden="slotHidden(g.stillId)" @pick="picker='guide:'+i" />
|
|
||||||
<input v-model.number="g.frame" type="number" min="1" aria-label="Guide frame" class="display-field w-20">
|
|
||||||
<button type="button" @click="form.guides.splice(i,1)">Remove</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="mt-3 text-[var(--amber)]" @click="form.guides.push({stillId:'',frame:24})">Add guide</button>
|
|
||||||
</details>
|
<template v-if="form.mode==='music'">
|
||||||
|
<label><span class="field-label">Genre / style tags</span><textarea v-model="music.tags" rows="3" class="field-area" :placeholder="tagsPlaceholder" /></label>
|
||||||
|
<label><span class="field-label">Lyrics</span><textarea v-model="music.lyrics" rows="8" class="field-area" :placeholder="lyricsPlaceholder" /></label>
|
||||||
|
<label><span class="field-label">Target length (sec)</span><input v-model.number="music.duration" type="number" min="15" max="150" class="field-control"></label>
|
||||||
|
<label><span class="field-label">Seed</span><input v-model="music.seed" placeholder="random" class="field-control"></label>
|
||||||
|
<label><span class="field-label">Track name</span><input v-model="music.name" class="field-control" placeholder="Optional"></label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<Studio2AssetSlot size="hero" label="Hero still" :asset="stills.find((a:any)=>a.id===form.identityStillId)" :hidden="slotHidden(form.identityStillId)" upload :disabled="uploading" @pick="picker='identityStillId'" @upload="upload($event,'hero')" />
|
||||||
|
<Studio2AssetSlot :label="form.mode==='extend'?'Previous clip':'Start still'" :asset="sourceSlot" :hidden="slotHidden(form.mode==='extend'?form.startClipId:form.imageAId)" upload :disabled="uploading" @pick="picker=form.mode==='extend'?'startClipId':'imageAId'" @upload="upload($event,'source')" />
|
||||||
|
<Studio2AssetSlot v-if="form.mode==='compose'" label="Second still" :asset="stills.find((a:any)=>a.id===form.imageBId)" :hidden="slotHidden(form.imageBId)" @pick="picker='imageBId'" />
|
||||||
|
<Studio2AssetSlot v-if="form.mode==='refine'" label="Mask" :asset="stills.find((a:any)=>a.id===form.maskId)" :hidden="slotHidden(form.maskId)" @pick="picker='maskId'" />
|
||||||
|
<Studio2AssetSlot v-if="video" label="End still" :asset="stills.find((a:any)=>a.id===form.endStillId)" :hidden="slotHidden(form.endStillId)" @pick="picker='endStillId'" />
|
||||||
|
|
||||||
|
<div class="identity-slots flex flex-col gap-2 text-[var(--type-sm)] text-[var(--muted)]" aria-label="Identity locks">
|
||||||
|
<label class="inline-flex items-center gap-2"><input v-model="form.lockFace" type="checkbox" class="accent-[var(--amber)]">Lock face</label>
|
||||||
|
<label class="inline-flex items-center gap-2"><input v-model="form.lockOutfit" type="checkbox" class="accent-[var(--amber)]">Lock outfit</label>
|
||||||
|
<p class="text-[11px]">hero encode + lock text — not a MiniMax face/clothes split on stills</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span class="field-label">Prompt</span>
|
||||||
|
<textarea v-model="form.promptSections.action" class="field-area" placeholder="Describe the image or the shot." />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="field-label">Negative prompt</span>
|
||||||
|
<div class="neg-row">
|
||||||
|
<span v-for="chip in negativeChips" :key="chip" class="neg-chip">
|
||||||
|
{{ chip }}
|
||||||
|
<button type="button" :aria-label="'Remove '+chip" @click="removeNegative(chip)">×</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="field-label">LoRA</span>
|
||||||
|
<LoraSelect v-model="form.settings.loraStack" compact :names="pickerLoras" :use="currentLoraUse" :clip-strength="true" filename-subtitle :aliases="{...loraNames.aliases,...library.loraAliases}" :triggers="{...loraNames.triggers,...library.loraTriggers}" missing-label="None" @insert-trigger="form.promptSections.action += ' '+$event" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="field-label">Seed</span>
|
||||||
|
<div class="seed-row">
|
||||||
|
<input
|
||||||
|
:value="form.settings.seed ?? ''"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="2147483647"
|
||||||
|
class="field-control"
|
||||||
|
@input="form.settings.seed=Number(($event.target as HTMLInputElement).value);form.settings.seedMode='locked'"
|
||||||
|
>
|
||||||
|
<button type="button" class="dice-btn" title="Random seed" aria-label="Random seed" @click="form.settings.seedMode='random';form.settings.seed=randomSeed()">🎲</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span class="field-label">Aspect ratio</span>
|
||||||
|
<select v-model="form.settings.aspect" class="field-control">
|
||||||
|
<option v-for="opt in aspectOptions" :key="opt.id" :value="opt.id">{{ opt.label }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button type="button" class="text-left text-[var(--type-sm)] text-[var(--amber)]" @click="settingsOpen=true">More settings (steps / CFG)…</button>
|
||||||
|
<p v-if="uploading || uploadMessage" role="status" class="text-[var(--type-sm)] text-[var(--amber)]">{{ uploading?'Uploading…':uploadMessage }}</p>
|
||||||
|
<p v-if="error" role="alert" class="text-[var(--type-sm)] text-red-300">{{ error }}</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="display-col min-w-0">
|
<!-- CENTER viewer -->
|
||||||
<div class="display-viewer">
|
<section class="bench-panel">
|
||||||
<button
|
<div v-if="form.mode!=='music'" class="viewer-lock-bar" aria-label="Locks and hide">
|
||||||
v-if="selected && xaigen"
|
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockFace" type="checkbox" class="accent-[var(--amber)]">Lock face</label>
|
||||||
type="button"
|
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockOutfit" type="checkbox" class="accent-[var(--amber)]">Lock outfit</label>
|
||||||
class="eye-viewer"
|
<label v-if="xaigen" class="inline-flex items-center gap-1.5"><input v-model="classicHideInput" type="checkbox" class="accent-[var(--amber)]">Hide input</label>
|
||||||
:aria-pressed="!isHidden(selected)"
|
<label v-if="xaigen" class="inline-flex items-center gap-1.5"><input v-model="classicHideOutput" type="checkbox" class="accent-[var(--amber)]">Hide output</label>
|
||||||
:aria-label="isHidden(selected) ? 'Show preview' : 'Hide preview'"
|
|
||||||
@click.stop="toggleReveal(selected.id)"
|
|
||||||
>
|
|
||||||
<img :src="isHidden(selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="20" height="20">
|
|
||||||
</button>
|
|
||||||
<p v-if="selected && isHidden(selected)">{{ selected.kind==='track' ? 'Track loaded · preview hidden' : selected.role==='input' ? 'Still loaded · preview hidden' : 'Output hidden' }}</p>
|
|
||||||
<div v-else-if="selected?.kind==='track'" class="w-full max-w-xl px-6 py-4">
|
|
||||||
<AudioPlayer :src="`/api/library/tracks/${selected.id}/audio`" :download-name="`${selected.name || 'track'}.${selected.ext || 'wav'}`" :label="selected.name || 'Track'" />
|
|
||||||
</div>
|
|
||||||
<video v-else-if="selected?.kind==='clip'" :key="selected.id" :src="`/api/library/clips/${selected.id}/video`" controls class="max-h-[65vh] w-full object-contain" />
|
|
||||||
<img v-else-if="selected" :src="stillUrl(selected.id)" class="max-h-[65vh] max-w-full object-contain" alt="Current output">
|
|
||||||
<p v-else>Select a result to view it here</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selected" class="mt-3 flex flex-wrap items-start gap-2">
|
<div class="center-stack">
|
||||||
<button v-if="selected.kind!=='track'" type="button" class="seg h-8 text-[var(--type-sm)]" @click="restore(selected)">Use as input</button>
|
<div class="viewer-frame">
|
||||||
<button v-if="selected.kind==='still'" type="button" class="seg h-8 text-[var(--type-sm)]" @click="Object.assign(form,useAsSource(form,selected))">Use as source still</button>
|
|
||||||
<button v-if="selected.kind==='still'" type="button" class="seg h-8 text-[var(--type-sm)]" @click="Object.assign(form,useAsHero(form,selected))">Use as hero</button>
|
|
||||||
<button v-if="selected.kind!=='track'" type="button" class="seg h-8 text-[var(--type-sm)]" @click="detailsOpen=true">Details</button>
|
|
||||||
<button type="button" class="seg h-8 text-[var(--type-sm)] text-red-200" @click="deleteOutput">Delete</button>
|
|
||||||
<VideoUpscaleControl v-if="selected.kind==='clip'" :clip-id="selected.id" queue-only @complete="refresh" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="display-strip" aria-label="Current family outputs">
|
|
||||||
<div
|
|
||||||
v-for="o in family"
|
|
||||||
:key="o.id"
|
|
||||||
class="display-tile"
|
|
||||||
:data-active="selected?.id===o.id ? 'true' : undefined"
|
|
||||||
>
|
|
||||||
<button type="button" class="block h-full w-full outline-none" @click="selectOutput(o)">
|
|
||||||
<span v-if="isHidden(o)" class="flex h-full items-center justify-center p-1 text-[10px] text-[var(--muted)]">{{ o.kind==='track' ? 'Audio hidden' : o.role==='input' ? 'Hidden' : 'Hidden' }}</span>
|
|
||||||
<span v-else-if="o.kind==='track'" class="flex h-full items-center justify-center text-[10px] text-[var(--amber)]">Audio</span>
|
|
||||||
<img v-else :src="o.kind==='clip' ? `/api/library/clips/${o.id}/thumb` : stillUrl(o.id)" class="h-full w-full object-cover" alt="Family output">
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
v-if="xaigen"
|
v-if="selected && xaigen"
|
||||||
type="button"
|
type="button"
|
||||||
class="eye-tile"
|
class="eye-viewer"
|
||||||
:aria-label="isHidden(o) ? 'Show tile' : 'Hide tile'"
|
:aria-pressed="!isHidden(selected)"
|
||||||
:aria-pressed="!isHidden(o)"
|
:aria-label="isHidden(selected) ? 'Show preview' : 'Hide preview'"
|
||||||
@click.stop="toggleReveal(o.id)"
|
@click.stop="toggleReveal(selected.id)"
|
||||||
>
|
>
|
||||||
<img :src="isHidden(o) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="12" height="12">
|
<img :src="isHidden(selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="16" height="16">
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<template v-if="selected && isHidden(selected)">
|
||||||
|
<p class="placeholder-still">{{ selected.kind==='track' ? 'Track loaded · preview hidden' : selected.role==='input' ? 'Still loaded · preview hidden' : 'Output hidden' }}</p>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="selected?.kind==='track'">
|
||||||
|
<div class="w-full max-w-xl px-6 py-4">
|
||||||
|
<AudioPlayer :src="`/api/library/tracks/${selected.id}/audio`" :download-name="`${selected.name || 'track'}.${selected.ext || 'wav'}`" :label="selected.name || 'Track'" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="selected?.kind==='clip'">
|
||||||
|
<video
|
||||||
|
ref="viewerVideo"
|
||||||
|
:key="selected.id"
|
||||||
|
:src="`/api/library/clips/${selected.id}/video`"
|
||||||
|
class="h-full w-full"
|
||||||
|
:muted="viewerMuted"
|
||||||
|
@timeupdate="onVideoTime"
|
||||||
|
@loadedmetadata="onVideoTime"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="selected">
|
||||||
|
<img :src="stillUrl(selected.id)" alt="Selected still">
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p class="placeholder-still">Select a still or clip in the strip</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filmstrip" aria-label="Current family outputs">
|
||||||
|
<div
|
||||||
|
v-for="o in family"
|
||||||
|
:key="o.id"
|
||||||
|
class="tile"
|
||||||
|
:data-active="selected?.id===o.id ? 'true' : undefined"
|
||||||
|
>
|
||||||
|
<button type="button" class="pick" @click="selectOutput(o)">
|
||||||
|
<span v-if="isHidden(o)" class="flex h-full items-center justify-center p-1 text-[10px] text-[var(--muted)]">Hidden</span>
|
||||||
|
<span v-else-if="o.kind==='track'" class="flex h-full items-center justify-center text-[10px] text-[var(--amber)]">Audio</span>
|
||||||
|
<img v-else :src="o.kind==='clip' ? `/api/library/clips/${o.id}/thumb` : stillUrl(o.id)" alt="">
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="xaigen"
|
||||||
|
type="button"
|
||||||
|
class="eye-tile"
|
||||||
|
:aria-label="isHidden(o) ? 'Show tile' : 'Hide tile'"
|
||||||
|
@click.stop="toggleReveal(o.id)"
|
||||||
|
>
|
||||||
|
<img :src="isHidden(o) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt="" width="12" height="12">
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="selected?.id===o.id"
|
||||||
|
type="button"
|
||||||
|
class="tile-trash"
|
||||||
|
aria-label="Delete selected"
|
||||||
|
title="Delete"
|
||||||
|
@click.stop="deleteOutput"
|
||||||
|
>🗑</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="transport">
|
||||||
|
<span>{{ transportLabel }}</span>
|
||||||
|
<div class="ops">
|
||||||
|
<button type="button" :aria-pressed="viewerMuted" :aria-label="viewerMuted ? 'Unmute' : 'Mute'" @click="viewerMuted=!viewerMuted">{{ viewerMuted ? '🔇' : '🔊' }}</button>
|
||||||
|
<button type="button" aria-label="Fullscreen" @click="toggleFullscreen">⛶</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="display-col min-w-0 space-y-3">
|
<!-- RIGHT queue -->
|
||||||
<template v-if="form.mode==='music'">
|
<aside class="bench-panel queue-col">
|
||||||
<p class="text-[var(--type-sm)] text-[var(--muted)]">YuE2 · lyrics-to-song · plan → synthesize → decode</p>
|
<div class="queue-head">
|
||||||
<label class="block text-[var(--type-sm)]">Genre / style tags
|
Queue
|
||||||
<textarea v-model="music.tags" rows="3" class="display-field mt-1" :placeholder="tagsPlaceholder" />
|
<span class="queue-badge">{{ queueCount }}</span>
|
||||||
</label>
|
|
||||||
<label class="block text-[var(--type-sm)]">Lyrics
|
|
||||||
<textarea v-model="music.lyrics" rows="8" class="display-field mt-1" :placeholder="lyricsPlaceholder" />
|
|
||||||
</label>
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<label class="block text-[var(--type-sm)]">Target length (sec)
|
|
||||||
<input v-model.number="music.duration" type="number" min="15" max="150" class="display-field mt-1">
|
|
||||||
<span class="mt-1 block text-[11px] text-[var(--muted)]">Caps semantic tokens (~25/s). Actual length follows the model end token — not an FFmpeg trim.</span>
|
|
||||||
</label>
|
|
||||||
<label class="block text-[var(--type-sm)]">Seed
|
|
||||||
<input v-model="music.seed" placeholder="random" class="display-field mt-1">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label class="block text-[var(--type-sm)]">Track name (optional)
|
|
||||||
<input v-model="music.name" class="display-field mt-1" placeholder="Optional track name">
|
|
||||||
</label>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<label class="block text-[var(--type-sm)]">Prompt<textarea v-model="form.promptSections.action" rows="5" class="display-field mt-1" placeholder="Describe the image or the shot." /></label>
|
|
||||||
<Studio2PromptControls v-if="video" v-model="form.promptSections" />
|
|
||||||
<Studio2StyleChips v-else v-model="form.imageStyles" />
|
|
||||||
<label v-if="video" class="block text-[var(--type-sm)]">Extra constraints (optional) — do not repeat the Prompt. Example: keep the black glasses, no extra people.
|
|
||||||
<textarea v-model="form.promptSections.locks" rows="2" class="display-field mt-1" />
|
|
||||||
</label>
|
|
||||||
<details v-if="video || form.mode==='edit'" class="rounded-[var(--radius)] border border-[var(--line)] p-2">
|
|
||||||
<summary class="text-[var(--type-sm)]">{{ video?'Queue more shots':'Queue extra edit passes' }}</summary>
|
|
||||||
<p class="mt-2 text-[11px] text-[var(--muted)]">Each blank-line-separated block is another job. Hero and settings stay. Press Enter twice between shots. A wrapped sentence is still one shot.{{ video?'':' Each extra pass edits the previous result with Klein.' }}</p>
|
|
||||||
<textarea v-model="batchText" rows="5" class="display-field mt-2" placeholder="First additional prompt Next additional prompt" />
|
|
||||||
</details>
|
|
||||||
<button type="button" class="text-[var(--type-sm)] text-[var(--amber)] outline-none focus-visible:underline" @click="settingsOpen=true">Generation settings</button>
|
|
||||||
<p v-if="!video && form.engine==='krea'" class="text-[var(--type-sm)] text-[var(--muted)]">Krea does not attach the hero identity reference.</p>
|
|
||||||
<p v-if="form.mode==='extend'" class="text-[var(--type-sm)] text-[var(--amber)]">{{ form.startClipId ? 'Extend · '+(clips.find((c:any)=>c.id===form.startClipId)?.name || form.startClipId)+' · last frame' : 'Select the clip in the family strip or Previous clip.' }}</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<p v-if="error" role="alert" class="text-[var(--type-sm)] text-red-300">{{ error }}</p>
|
|
||||||
<button type="button" :disabled="submitting" class="display-cta" @click="queue">{{ submitting?'Queuing…':'Queue' }}</button>
|
|
||||||
|
|
||||||
<h2 class="text-[var(--type-sm)] font-semibold text-[var(--muted)]">Job queue</h2>
|
|
||||||
<div v-for="job in upscales.filter(j=>j.folderId===form.folderId && ['queued','running'].includes(j.status))" :key="job.id" class="space-y-2 rounded-[var(--radius)] border border-[color-mix(in_srgb,var(--amber)_25%,transparent)] bg-[var(--bg-slot)] p-3 text-[var(--type-sm)]" role="status">
|
|
||||||
<div class="flex justify-between gap-3"><strong>Video upscale</strong><span class="capitalize text-[var(--amber)]">{{ job.status }}</span></div>
|
|
||||||
<p class="truncate">{{ job.name }}</p>
|
|
||||||
<p class="text-[11px] text-[var(--muted)]">{{ job.width?job.width+' × '+job.height+' · ':'' }}{{ Math.round((job.elapsedMs||0)/1000) }}s elapsed</p>
|
|
||||||
<progress class="h-2 w-full accent-[var(--amber)]" max="100" :value="job.progress || 0" />
|
|
||||||
<p class="text-[11px] text-[var(--muted)]">{{ job.message }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-for="job in musicJobs" :key="job.id" class="space-y-2 rounded-[var(--radius)] border border-[color-mix(in_srgb,var(--amber)_25%,transparent)] bg-[var(--bg-slot)] p-3 text-[var(--type-sm)]" aria-live="polite" role="status">
|
<div class="queue-list">
|
||||||
<div class="flex justify-between gap-3">
|
<div
|
||||||
<strong class="truncate">{{ job.name || 'YuE2' }}</strong>
|
v-for="card in queueCards"
|
||||||
<span class="capitalize text-[var(--amber)]">{{ job.live?.status || job.status }}</span>
|
:key="card.id"
|
||||||
|
class="job-card"
|
||||||
|
:data-active="focusedJobId===card.id ? 'true' : undefined"
|
||||||
|
@click="focusQueueCard(card)"
|
||||||
|
>
|
||||||
|
<div class="job-id">{{ card.code }}</div>
|
||||||
|
<div class="job-title">{{ card.title }}</div>
|
||||||
|
<div class="job-meta">{{ card.meta }}</div>
|
||||||
|
<div class="job-status" :data-state="card.state">
|
||||||
|
<span class="left"><span class="dot" aria-hidden="true" />{{ card.statusLabel }}</span>
|
||||||
|
<button v-if="card.cancelable" type="button" class="kebab" aria-label="Cancel job" @click.stop="cancel(card.job)">⋮</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-[11px] text-[var(--muted)]">YuE2 · target {{ job.duration || '—' }}s<span v-if="job.live?.message"> · {{ job.live.message }}</span></p>
|
<p v-if="!queueCards.length" class="px-1 text-[var(--type-sm)] text-[var(--muted)]">No active jobs</p>
|
||||||
<progress v-if="job.live?.progress" class="h-2 w-full accent-[var(--amber)]" max="100" :value="job.live.progress || 0" />
|
|
||||||
<p v-if="job.lastError || job.live?.error" role="alert" class="text-red-300">{{ job.lastError || job.live?.error }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Studio2JobProgress v-for="job in benchJobs" :key="job.id" :job="job" :now="now" @select="focusJob(job)" @cancel="cancel(job)" />
|
<div class="queue-foot">
|
||||||
<button v-if="folderJobs.some(j=>['complete','failed','cancelled'].includes(j.state)) || musicJobs.some(j=>['complete','done','error','cancelled'].includes(j.status))" type="button" class="text-[var(--type-sm)] text-[var(--muted)] outline-none focus-visible:underline" @click="clearFinished">Clear finished</button>
|
<button type="button" class="generate-cta" :disabled="submitting" @click="queue">
|
||||||
</section>
|
<span aria-hidden="true">▶</span>
|
||||||
|
{{ submitting ? 'Queuing…' : 'Generate' }}
|
||||||
|
</button>
|
||||||
|
<p class="tiny">
|
||||||
|
Job queue
|
||||||
|
<button v-if="canClearFinished" type="button" @click="clearFinished"> · Clear finished</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="settingsOpen || detailsOpen" class="fixed inset-0 z-50 bg-black/70" @click.self="settingsOpen=false;detailsOpen=false">
|
<div v-if="settingsOpen || detailsOpen" class="fixed inset-0 z-50 bg-black/70" @click.self="settingsOpen=false;detailsOpen=false">
|
||||||
<aside class="absolute inset-y-0 right-0 w-full max-w-md overflow-auto border-l border-[var(--line)] bg-[var(--bg)] p-6">
|
<aside class="absolute inset-y-0 right-0 w-full max-w-md overflow-auto border-l border-[var(--line)] bg-[var(--bg)] p-6">
|
||||||
<button type="button" class="float-right outline-none focus-visible:underline" @click="settingsOpen=false;detailsOpen=false">Close</button>
|
<button type="button" class="float-right" @click="settingsOpen=false;detailsOpen=false">Close</button>
|
||||||
<template v-if="settingsOpen">
|
<template v-if="settingsOpen">
|
||||||
<h2 class="mb-6 text-xl font-semibold">Generation settings</h2>
|
<h2 class="mb-6 text-xl font-semibold">More settings</h2>
|
||||||
<AspectRatioPick v-model="form.settings.aspect" heading="Aspect" :options="aspectOptions" />
|
<p class="mb-3 text-[var(--type-sm)] text-[var(--muted)]">{{ resolvedSize.width }} × {{ resolvedSize.height }} · fit with padding</p>
|
||||||
<p class="my-3 text-[var(--type-sm)] text-[var(--muted)]">{{ resolvedSize.width }} × {{ resolvedSize.height }} · fit with padding, never stretch</p>
|
|
||||||
<label v-for="key in ['steps','cfg',...(video?['duration','fps']:[])]" :key="key" class="mb-4 block capitalize">{{ key }}
|
<label v-for="key in ['steps','cfg',...(video?['duration','fps']:[])]" :key="key" class="mb-4 block capitalize">{{ key }}
|
||||||
<input v-model.number="form.settings[key]" type="number" class="display-field mt-1">
|
<input v-model.number="form.settings[key]" type="number" class="field-control mt-1">
|
||||||
</label>
|
</label>
|
||||||
<label class="mb-4 block">Seed · {{ form.settings.seedMode==='locked'?'Locked':'Random each job' }}
|
<label v-if="video" class="mb-4 flex items-center gap-2"><input v-model="form.settings.turbo" type="checkbox" class="accent-[var(--amber)]"> Turbo</label>
|
||||||
<input :value="form.settings.seed" type="number" min="0" max="2147483647" class="display-field mt-1" @input="form.settings.seed=Number(($event.target as HTMLInputElement).value);form.settings.seedMode='locked'">
|
<details v-if="video || form.mode==='edit'" class="mt-4">
|
||||||
</label>
|
<summary>{{ video?'Queue more shots':'Queue extra edit passes' }}</summary>
|
||||||
<button type="button" class="mb-4 text-[var(--type-sm)] text-[var(--amber)] outline-none focus-visible:underline" @click="form.settings.seedMode='random';form.settings.seed=randomSeed()">Random</button>
|
<textarea v-model="batchText" rows="5" class="field-area mt-2" placeholder="First additional prompt Next additional prompt" />
|
||||||
<label v-if="video"><input v-model="form.settings.turbo" type="checkbox" class="accent-[var(--amber)]"> Turbo</label>
|
|
||||||
<LoraSelect v-model="form.settings.loraStack" class="mt-5" :names="pickerLoras" :use="currentLoraUse" :clip-strength="true" filename-subtitle :aliases="{...loraNames.aliases,...library.loraAliases}" :triggers="{...loraNames.triggers,...library.loraTriggers}" missing-label="No cached LoRAs yet — Refresh to check Comfy" @insert-trigger="form.promptSections.action += ' '+$event" />
|
|
||||||
<button type="button" class="mt-2 text-[var(--type-sm)] text-[var(--amber)] outline-none focus-visible:underline" @click="loadLoras(true)">Refresh LoRAs</button>
|
|
||||||
<details class="mt-6"><summary>Compiled prompt</summary>
|
|
||||||
<pre class="whitespace-pre-wrap text-xs text-[var(--muted)]">{{ compiled }}</pre>
|
|
||||||
<p v-if="!video" class="mt-3 text-xs text-[var(--muted)]">Negative: {{ stylePrompt(form.imageStyles,true) || 'None' }}</p>
|
|
||||||
</details>
|
</details>
|
||||||
|
<button type="button" class="mt-4 text-[var(--type-sm)] text-[var(--amber)]" @click="loadLoras(true)">Refresh LoRAs</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h2 class="mb-6 text-xl font-semibold">Output details</h2>
|
<h2 class="mb-6 text-xl font-semibold">Output details</h2>
|
||||||
@@ -238,31 +288,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="picker" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-6" @click.self="picker=''">
|
<div v-if="picker" class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-6" @click.self="picker=''">
|
||||||
<section role="dialog" aria-modal="true" aria-label="Choose source asset" class="max-h-[80vh] w-full max-w-4xl overflow-auto rounded-[var(--radius)] border border-[var(--line)] bg-[var(--bg)] p-5">
|
<section role="dialog" aria-modal="true" aria-label="Choose source asset" class="max-h-[80vh] w-full max-w-4xl overflow-auto rounded-[var(--radius)] border border-[var(--line)] bg-[var(--panel)] p-5">
|
||||||
<div class="mb-4 flex justify-between"><h2>Choose {{ picker==='identityStillId'?'hero':'source' }}</h2><button type="button" @click="picker=''">Close</button></div>
|
<div class="mb-4 flex justify-between"><h2>Choose {{ picker==='identityStillId'?'hero':'source' }}</h2><button type="button" @click="picker=''">Close</button></div>
|
||||||
<button type="button" class="mb-4 text-[var(--type-sm)] text-[var(--muted)]" @click="pickAsset(null)">Clear slot</button>
|
<button type="button" class="mb-4 text-[var(--type-sm)] text-[var(--muted)]" @click="pickAsset(null)">Clear slot</button>
|
||||||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-5">
|
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-5">
|
||||||
<button v-for="a in pickerAssets" :key="a.id" type="button" class="rounded-[var(--radius)] border border-[var(--line)] p-2 text-left outline-none focus-visible:ring-2 focus-visible:ring-[color-mix(in_srgb,var(--amber)_50%,transparent)]" @click="pickAsset(a)">
|
<button v-for="a in pickerAssets" :key="a.id" type="button" class="rounded-[var(--radius)] border border-[var(--line)] p-2 text-left" @click="pickAsset(a)">
|
||||||
<span v-if="isHidden(a)" class="flex h-24 items-center text-xs text-[var(--muted)]">Still loaded · preview hidden</span>
|
<span v-if="isHidden(a)" class="flex h-24 items-center text-xs text-[var(--muted)]">Still loaded · preview hidden</span>
|
||||||
<img v-else :src="a.kind==='clip'?'/api/library/clips/'+a.id+'/thumb':stillUrl(a.id)" class="h-24 w-full object-contain" alt="Source option">
|
<img v-else :src="a.kind==='clip'?'/api/library/clips/'+a.id+'/thumb':stillUrl(a.id)" class="h-24 w-full object-contain" alt="">
|
||||||
<span class="mt-2 block truncate text-xs">{{ assetLabel(a) }}</span>
|
<span class="mt-2 block truncate text-xs">{{ assetLabel(a) }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {createLoraMemory,loraUse} from '~/shared/studio2/lora-memory.mjs'
|
import {createLoraMemory,loraUse} from '~/shared/studio2/lora-memory.mjs'
|
||||||
import {filterLorasForStudio,filterLorasForUse} from '~/utils/loras'
|
import {filterLorasForStudio,filterLorasForUse} from '~/utils/loras'
|
||||||
import {compilePrompt,sectionsFrom} from '~/shared/studio2/contracts.mjs'
|
import {sectionsFrom} from '~/shared/studio2/contracts.mjs'
|
||||||
import {aspectOptions,resolveSize,bindClip,changeSource,restoreAll,useAsHero,useAsSource} from '~/shared/studio2/input.mjs'
|
import {aspectOptions,resolveSize,bindClip,restoreAll,useAsHero,useAsSource} from '~/shared/studio2/input.mjs'
|
||||||
import {folderAssets,newestOutput,assetLabel,reconcileSelection,previewHidden} from '~/shared/studio2/viewer.mjs'
|
import {folderAssets,newestOutput,assetLabel,reconcileSelection,previewHidden} from '~/shared/studio2/viewer.mjs'
|
||||||
import {queueJobs} from '~/shared/studio2/queue-view.mjs'
|
import {queueJobs} from '~/shared/studio2/queue-view.mjs'
|
||||||
import {gpuStatus} from '~/shared/studio2/status.mjs'
|
import {gpuStatus} from '~/shared/studio2/status.mjs'
|
||||||
import {applyUploadedStill} from '~/shared/studio2/upload.mjs'
|
import {applyUploadedStill} from '~/shared/studio2/upload.mjs'
|
||||||
import {parseBatch,stylePrompt} from '~/shared/studio2/styles.mjs'
|
import {defaultNegatives,parseBatch} from '~/shared/studio2/styles.mjs'
|
||||||
import {randomSeed} from '~/shared/studio2/seed.mjs'
|
import {randomSeed} from '~/shared/studio2/seed.mjs'
|
||||||
import {DEFAULT_MUSIC_LYRICS,DEFAULT_MUSIC_TAGS} from '~/utils/music'
|
import {DEFAULT_MUSIC_LYRICS,DEFAULT_MUSIC_TAGS} from '~/utils/music'
|
||||||
|
|
||||||
@@ -279,7 +329,7 @@ const loraNames=ref<any>({image:[],video:[]}),now=ref(Date.now()),healthState=re
|
|||||||
const library=ref<any>({folders:[],stills:[],clips:[],tracks:[]}),jobs=ref<any[]>([]),selected=ref<any>(null)
|
const library=ref<any>({folders:[],stills:[],clips:[],tracks:[]}),jobs=ref<any[]>([]),selected=ref<any>(null)
|
||||||
const studioQueue=ref<any[]>([])
|
const studioQueue=ref<any[]>([])
|
||||||
const liveMusic=ref<Record<string,any>>({})
|
const liveMusic=ref<Record<string,any>>({})
|
||||||
const form=reactive<any>({mode:'generate',engine:'flux',imageStyles:{positive:[],negative:[]},lockFace:true,lockOutfit:true,hideInput:xaigen,folderId:'',identityStillId:'',imageAId:'',imageBId:'',maskId:'',startClipId:'',endStillId:'',guides:[],promptSections:sectionsFrom({}),settings:{aspect:'auto',fps:24,steps:24,cfg:4,seed:null,seedMode:'random',duration:5,turbo:true,loraStack:[]}})
|
const form=reactive<any>({mode:'generate',engine:'flux',imageStyles:{positive:[],negative:[...defaultNegatives]},lockFace:true,lockOutfit:true,hideInput:xaigen,folderId:'',identityStillId:'',imageAId:'',imageBId:'',maskId:'',startClipId:'',endStillId:'',guides:[],promptSections:sectionsFrom({}),settings:{aspect:'16:9',fps:24,steps:24,cfg:4,seed:null,seedMode:'random',duration:5,turbo:true,loraStack:[]}})
|
||||||
const music=reactive({tags:DEFAULT_MUSIC_TAGS,lyrics:DEFAULT_MUSIC_LYRICS,duration:60,seed:'',name:''})
|
const music=reactive({tags:DEFAULT_MUSIC_TAGS,lyrics:DEFAULT_MUSIC_LYRICS,duration:60,seed:'',name:''})
|
||||||
const tagsPlaceholder=DEFAULT_MUSIC_TAGS,lyricsPlaceholder=DEFAULT_MUSIC_LYRICS
|
const tagsPlaceholder=DEFAULT_MUSIC_TAGS,lyricsPlaceholder=DEFAULT_MUSIC_LYRICS
|
||||||
let loraMemory=createLoraMemory(xaigen)
|
let loraMemory=createLoraMemory(xaigen)
|
||||||
@@ -289,6 +339,11 @@ watch(()=>[currentLoraUse.value,form.settings.loraStack],()=>loraMemory.sync(for
|
|||||||
const route=useRoute()
|
const route=useRoute()
|
||||||
const tasks=['generate','edit','compose','refine','video','extend','music']
|
const tasks=['generate','edit','compose','refine','video','extend','music']
|
||||||
const settingsOpen=ref(false),detailsOpen=ref(false),submitting=ref(false),error=ref(''),batchText=ref('')
|
const settingsOpen=ref(false),detailsOpen=ref(false),submitting=ref(false),error=ref(''),batchText=ref('')
|
||||||
|
const viewerVideo=ref<HTMLVideoElement|null>(null)
|
||||||
|
const viewerMuted=ref(false)
|
||||||
|
const videoTime=ref(0)
|
||||||
|
const videoDuration=ref(0)
|
||||||
|
const focusedJobId=ref('')
|
||||||
const video=computed(()=>['video','extend'].includes(form.mode))
|
const video=computed(()=>['video','extend'].includes(form.mode))
|
||||||
const kreaSticky=computed(()=>['edit','compose','iterate'].includes(form.mode))
|
const kreaSticky=computed(()=>['edit','compose','iterate'].includes(form.mode))
|
||||||
const engineOptions=computed(()=>{
|
const engineOptions=computed(()=>{
|
||||||
@@ -309,7 +364,6 @@ watch(engineOptions,opts=>{
|
|||||||
})
|
})
|
||||||
const stills=computed(()=>library.value.stills.filter((s:any)=>s.folderId===form.folderId))
|
const stills=computed(()=>library.value.stills.filter((s:any)=>s.folderId===form.folderId))
|
||||||
const clips=computed(()=>library.value.clips.filter((s:any)=>s.folderId===form.folderId))
|
const clips=computed(()=>library.value.clips.filter((s:any)=>s.folderId===form.folderId))
|
||||||
const compiled=computed(()=>compilePrompt(form.promptSections,video.value,form))
|
|
||||||
const stillUrl=(id:string)=>`/api/library/stills/${id}`
|
const stillUrl=(id:string)=>`/api/library/stills/${id}`
|
||||||
const folderJobs=computed(()=>jobs.value.filter(j=>j.request.folderId===form.folderId))
|
const folderJobs=computed(()=>jobs.value.filter(j=>j.request.folderId===form.folderId))
|
||||||
const benchJobs=computed(()=>queueJobs(folderJobs.value))
|
const benchJobs=computed(()=>queueJobs(folderJobs.value))
|
||||||
@@ -317,6 +371,12 @@ const musicJobs=computed(()=>studioQueue.value
|
|||||||
.filter((j:any)=>j.kind==='music' && j.folderId===form.folderId && !['complete','done'].includes(j.status))
|
.filter((j:any)=>j.kind==='music' && j.folderId===form.folderId && !['complete','done'].includes(j.status))
|
||||||
.map((j:any)=>({...j,live:j.liveJobId?liveMusic.value[j.liveJobId]:null})))
|
.map((j:any)=>({...j,live:j.liveJobId?liveMusic.value[j.liveJobId]:null})))
|
||||||
const gpu=computed(()=>gpuStatus(jobs.value,healthState.value,upscales.value,musicJobs.value))
|
const gpu=computed(()=>gpuStatus(jobs.value,healthState.value,upscales.value,musicJobs.value))
|
||||||
|
const gpuChipLabel=computed(()=>{
|
||||||
|
const raw=String(healthState.value?.comfy?.gpu || healthState.value?.gpuName || healthState.value?.comfy?.device || '').trim()
|
||||||
|
const short=raw.match(/(\d{3,4})\b/)?.[1]
|
||||||
|
if(gpu.value.tone==='ready') return short ? `${short} Ready` : 'Ready'
|
||||||
|
return gpu.value.label
|
||||||
|
})
|
||||||
const sourceSlot=computed(()=>{const clip=clips.value.find((a:any)=>a.id===form.startClipId);return form.mode==='extend'?(clip?{...clip,kind:'clip'}:null):inputStill.value})
|
const sourceSlot=computed(()=>{const clip=clips.value.find((a:any)=>a.id===form.startClipId);return form.mode==='extend'?(clip?{...clip,kind:'clip'}:null):inputStill.value})
|
||||||
const pickerAssets=computed(()=>picker.value==='startClipId'?clips.value.map((a:any)=>({...a,kind:'clip'})):stills.value.map((a:any)=>({...a,kind:'still'})))
|
const pickerAssets=computed(()=>picker.value==='startClipId'?clips.value.map((a:any)=>({...a,kind:'clip'})):stills.value.map((a:any)=>({...a,kind:'still'})))
|
||||||
const inputStill=computed(()=>stills.value.find((s:any)=>s.id===form.imageAId))
|
const inputStill=computed(()=>stills.value.find((s:any)=>s.id===form.imageAId))
|
||||||
@@ -324,7 +384,85 @@ const family=computed(()=>folderAssets(library.value,jobs.value,form.folderId))
|
|||||||
const sourceAsset=computed(()=>form.startClipId?clips.value.find((c:any)=>c.id===form.startClipId):stills.value.find((s:any)=>s.id===(form.imageAId||form.identityStillId)))
|
const sourceAsset=computed(()=>form.startClipId?clips.value.find((c:any)=>c.id===form.startClipId):stills.value.find((s:any)=>s.id===(form.imageAId||form.identityStillId)))
|
||||||
const sourceSize=computed(()=>measured[sourceAsset.value?.id] || sourceAsset.value)
|
const sourceSize=computed(()=>measured[sourceAsset.value?.id] || sourceAsset.value)
|
||||||
const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.value))
|
const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.value))
|
||||||
|
const negativeChips=computed(()=>form.imageStyles.negative || [])
|
||||||
|
const queueCards=computed(()=>{
|
||||||
|
const cards:any[]=[]
|
||||||
|
for(const job of benchJobs.value){
|
||||||
|
const action=job.request?.promptSections?.action || 'Job'
|
||||||
|
const eng=job.request?.engine==='ltx'?'LTX':job.request?.engine==='krea'?'Krea 2':job.request?.engine==='minimax'?'MiniMax H3':'Klein'
|
||||||
|
const mode=job.request?.mode || 'generate'
|
||||||
|
const modeLabel=mode.charAt(0).toUpperCase()+mode.slice(1)
|
||||||
|
const aspect=job.request?.settings?.aspect || 'auto'
|
||||||
|
const state=job.state==='waiting'?'queued':job.state
|
||||||
|
cards.push({
|
||||||
|
id:job.id,
|
||||||
|
job,
|
||||||
|
code:'#'+String(job.id).replace(/\D/g,'').slice(-3).padStart(3,'0'),
|
||||||
|
title:action.slice(0,72),
|
||||||
|
meta:`${modeLabel} · ${eng} · ${aspect}`,
|
||||||
|
state,
|
||||||
|
statusLabel:state==='queued'?'Queued':state==='rendering'||state==='submitting'||state==='waking'||state==='saving'?'Running':state==='complete'?'Ready':state,
|
||||||
|
cancelable:!['complete','failed','cancelled'].includes(job.state)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for(const job of musicJobs.value){
|
||||||
|
cards.push({
|
||||||
|
id:job.id,
|
||||||
|
job,
|
||||||
|
code:'#'+String(job.id).replace(/\D/g,'').slice(-3).padStart(3,'0'),
|
||||||
|
title:job.name || 'YuE2',
|
||||||
|
meta:`Music · YuE2 · ${job.duration || '—'}s`,
|
||||||
|
state:job.live?.status || job.status,
|
||||||
|
statusLabel:job.live?.status || job.status,
|
||||||
|
cancelable:!['complete','done','error','cancelled'].includes(job.status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for(const job of upscales.value.filter((j:any)=>j.folderId===form.folderId && ['queued','running'].includes(j.status))){
|
||||||
|
cards.push({
|
||||||
|
id:job.id,
|
||||||
|
job,
|
||||||
|
code:'#'+String(job.id).replace(/\D/g,'').slice(-3).padStart(3,'0'),
|
||||||
|
title:job.name || 'Video upscale',
|
||||||
|
meta:'Upscale',
|
||||||
|
state:job.status,
|
||||||
|
statusLabel:job.status==='running'?'Running':'Queued',
|
||||||
|
cancelable:false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cards
|
||||||
|
})
|
||||||
|
const queueCount=computed(()=>queueCards.value.length)
|
||||||
|
const canClearFinished=computed(()=>folderJobs.value.some(j=>['complete','failed','cancelled'].includes(j.state)) || musicJobs.value.some(j=>['complete','done','error','cancelled'].includes(j.status)))
|
||||||
|
const transportLabel=computed(()=>{
|
||||||
|
if(selected.value?.kind==='clip') return `${formatTime(videoTime.value)} / ${formatTime(videoDuration.value)}`
|
||||||
|
if(selected.value?.kind==='track') return 'Audio'
|
||||||
|
if(selected.value) return 'Still'
|
||||||
|
return '—'
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatTime(sec:number){
|
||||||
|
const s=Math.max(0,Number(sec)||0)
|
||||||
|
const m=Math.floor(s/60)
|
||||||
|
const r=s-m*60
|
||||||
|
const whole=Math.floor(r)
|
||||||
|
const frac=Math.floor((r-whole)*100)
|
||||||
|
return `${String(m).padStart(2,'0')}:${String(whole).padStart(2,'0')}.${String(frac).padStart(2,'0')}`
|
||||||
|
}
|
||||||
|
function onVideoTime(){
|
||||||
|
const el=viewerVideo.value
|
||||||
|
if(!el)return
|
||||||
|
videoTime.value=el.currentTime||0
|
||||||
|
videoDuration.value=el.duration||0
|
||||||
|
}
|
||||||
|
function toggleFullscreen(){
|
||||||
|
const el=viewerVideo.value || document.querySelector('.viewer-frame')
|
||||||
|
if(el && 'requestFullscreen' in el) void (el as HTMLElement).requestFullscreen()
|
||||||
|
}
|
||||||
|
function removeNegative(chip:string){form.imageStyles={...form.imageStyles,negative:(form.imageStyles.negative||[]).filter((c:string)=>c!==chip)}}
|
||||||
|
function focusQueueCard(card:any){
|
||||||
|
focusedJobId.value=card.id
|
||||||
|
if(card.job?.outputs?.length)focusJob(card.job)
|
||||||
|
}
|
||||||
function isHidden(asset:any){
|
function isHidden(asset:any){
|
||||||
if(!xaigen || !asset?.id)return false
|
if(!xaigen || !asset?.id)return false
|
||||||
if(isClassic.value){
|
if(isClassic.value){
|
||||||
@@ -352,6 +490,7 @@ function loadRevealed(folderId:string){
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(()=>sourceAsset.value?.id,id=>{const asset=sourceAsset.value;if(!import.meta.client||!asset||measured[id])return;const media=asset.id===form.startClipId?document.createElement('video'):new Image();if(media.tagName==='VIDEO')(media as HTMLVideoElement).preload='metadata';const done=()=>{measured[asset.id]={width:(media as HTMLVideoElement).videoWidth||(media as HTMLImageElement).naturalWidth,height:(media as HTMLVideoElement).videoHeight||(media as HTMLImageElement).naturalHeight}};media.addEventListener(media.tagName==='VIDEO'?'loadedmetadata':'load',done,{once:true});media.src=media.tagName==='VIDEO'?`/api/library/clips/${asset.id}/video`:stillUrl(asset.id)})
|
watch(()=>sourceAsset.value?.id,id=>{const asset=sourceAsset.value;if(!import.meta.client||!asset||measured[id])return;const media=asset.id===form.startClipId?document.createElement('video'):new Image();if(media.tagName==='VIDEO')(media as HTMLVideoElement).preload='metadata';const done=()=>{measured[asset.id]={width:(media as HTMLVideoElement).videoWidth||(media as HTMLImageElement).naturalWidth,height:(media as HTMLVideoElement).videoHeight||(media as HTMLImageElement).naturalHeight}};media.addEventListener(media.tagName==='VIDEO'?'loadedmetadata':'load',done,{once:true});media.src=media.tagName==='VIDEO'?`/api/library/clips/${asset.id}/video`:stillUrl(asset.id)})
|
||||||
|
watch(()=>selected.value?.id,()=>{videoTime.value=0;videoDuration.value=0})
|
||||||
|
|
||||||
function chooseTask(task:string){
|
function chooseTask(task:string){
|
||||||
if(task==='music'){form.mode='music';return}
|
if(task==='music'){form.mode='music';return}
|
||||||
@@ -366,12 +505,13 @@ function chooseTask(task:string){
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const was=video.value;form.mode=task;form.startClipId='';if(video.value!==was){form.engine=video.value?(xaigen && form.engine==='ltx'?'ltx':'minimax'):'flux';form.settings.steps=video.value?8:24;form.settings.cfg=video.value?1.5:4}
|
const was=video.value;form.mode=task;form.startClipId='';if(video.value!==was){form.engine=video.value?(xaigen && form.engine==='ltx'?'ltx':'minimax'):'flux';form.settings.steps=video.value?8:24;form.settings.cfg=video.value?1.5:4}
|
||||||
|
if(selected.value?.kind==='track' && task!=='music') selected.value=family.value.find((a:any)=>a.kind!=='track') || null
|
||||||
}
|
}
|
||||||
function onTaskSelect(event:Event){chooseTask((event.target as HTMLSelectElement).value)}
|
function onTaskSelect(event:Event){chooseTask((event.target as HTMLSelectElement).value)}
|
||||||
function chooseClip(id:string){if(!id){form.startClipId='';return}const clip=clips.value.find((c:any)=>c.id===id);if(clip){try{Object.assign(form,bindClip(form,clip,xaigen,loraMemory))}catch(e:any){error.value=e?.message||'Could not copy clip settings.';form.startClipId=clip.id;form.mode='extend';form.engine=xaigen && form.engine==='ltx'?'ltx':'minimax'}}}
|
function chooseClip(id:string){if(!id){form.startClipId='';return}const clip=clips.value.find((c:any)=>c.id===id);if(clip){try{Object.assign(form,bindClip(form,clip,xaigen,loraMemory))}catch(e:any){error.value=e?.message||'Could not copy clip settings.';form.startClipId=clip.id;form.mode='extend';form.engine=xaigen && form.engine==='ltx'?'ltx':'minimax'}}}
|
||||||
function selectOutput(item:any){selected.value=item;if(form.mode==='extend' && item?.kind==='clip')chooseClip(item.id)}
|
function selectOutput(item:any){selected.value=item;if(form.mode==='extend' && item?.kind==='clip')chooseClip(item.id)}
|
||||||
function restore(item:any){Object.assign(form,restoreAll(form,item,xaigen,loraMemory));form.guides=form.guides||[];batchText.value=''}
|
function restore(item:any){Object.assign(form,restoreAll(form,item,xaigen,loraMemory));form.guides=form.guides||[];batchText.value=''}
|
||||||
function focusJob(job:any){selectOutput(job.outputs?.at(-1)||null)}
|
function focusJob(job:any){selectOutput(job.outputs?.at(-1)||null);focusedJobId.value=job.id}
|
||||||
async function loadLoras(fresh=false){try{loraNames.value=await $fetch('/api/studio-2/loras'+(fresh?'?fresh=1':''))}catch{error.value='Could not refresh LoRAs. The last loaded list is still available.'}}
|
async function loadLoras(fresh=false){try{loraNames.value=await $fetch('/api/studio-2/loras'+(fresh?'?fresh=1':''))}catch{error.value='Could not refresh LoRAs. The last loaded list is still available.'}}
|
||||||
function pickAsset(asset:any){const key=picker.value;if(key.startsWith('guide:'))form.guides[Number(key.split(':')[1])].stillId=asset?.id||'';else if(key==='startClipId')chooseClip(asset?.id||'');else form[key]=asset?.id||'';picker.value=''}
|
function pickAsset(asset:any){const key=picker.value;if(key.startsWith('guide:'))form.guides[Number(key.split(':')[1])].stillId=asset?.id||'';else if(key==='startClipId')chooseClip(asset?.id||'');else form[key]=asset?.id||'';picker.value=''}
|
||||||
async function clearFinished(){await $fetch('/api/studio-2/clear-finished',{method:'POST'});await refresh()}
|
async function clearFinished(){await $fetch('/api/studio-2/clear-finished',{method:'POST'});await refresh()}
|
||||||
@@ -429,8 +569,12 @@ async function refresh(){
|
|||||||
for(const job of nextJobs)for(const output of job.outputs||[])seenOutputs.add(output.id)
|
for(const job of nextJobs)for(const output of job.outputs||[])seenOutputs.add(output.id)
|
||||||
const existingFresh=family.value.find(a=>a.id===fresh?.id)
|
const existingFresh=family.value.find(a=>a.id===fresh?.id)
|
||||||
const newestTrack=(library.value.tracks||[]).filter((t:any)=>t.folderId===form.folderId).sort((a:any,b:any)=>(b.createdAt||0)-(a.createdAt||0))[0]
|
const newestTrack=(library.value.tracks||[]).filter((t:any)=>t.folderId===form.folderId).sort((a:any,b:any)=>(b.createdAt||0)-(a.createdAt||0))[0]
|
||||||
if(newestTrack && !seenOutputs.has(newestTrack.id)){seenOutputs.add(newestTrack.id);selected.value={...newestTrack,kind:'track'};return}
|
// Only auto-select new tracks on Music — never leave a waveform mounted over Generate/Klein/Video.
|
||||||
selected.value=existingFresh || reconcileSelection(selected.value,family.value)
|
if(form.mode==='music' && newestTrack && !seenOutputs.has(newestTrack.id)){seenOutputs.add(newestTrack.id);selected.value={...newestTrack,kind:'track'};return}
|
||||||
|
if(newestTrack) seenOutputs.add(newestTrack.id)
|
||||||
|
let next=existingFresh || reconcileSelection(selected.value,family.value)
|
||||||
|
if(form.mode!=='music' && next?.kind==='track') next=family.value.find((a:any)=>a.kind!=='track') || null
|
||||||
|
selected.value=next
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(()=>form.folderId,id=>{selected.value=null;form.imageAId='';form.startClipId='';loadRevealed(id);if(import.meta.client)form.identityStillId=localStorage.getItem(`studio2-hero:${id}`)||''},{flush:'sync'})
|
watch(()=>form.folderId,id=>{selected.value=null;form.imageAId='';form.startClipId='';loadRevealed(id);if(import.meta.client)form.identityStillId=localStorage.getItem(`studio2-hero:${id}`)||''},{flush:'sync'})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#f3f1ea" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<rect x="4" y="11" width="16" height="10" rx="2"/>
|
<rect x="4" y="11" width="16" height="10" rx="2"/>
|
||||||
<path d="M8 11V8a4 4 0 0 1 8 0v3"/>
|
<path d="M8 11V8a4 4 0 0 1 8 0v3"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 297 B After Width: | Height: | Size: 302 B |
@@ -1,5 +1,7 @@
|
|||||||
export const styleGroups={Render:['Photorealistic','Pixar','3D Disney','Comic','Watercolor','Oil painting'],Quality:['Realistic skin','Skin pores','Fine detail','Natural texture'],Lighting:['Golden hour','Soft studio','Window light','Overcast','Cinematic lighting']}
|
export const styleGroups={Render:['Photorealistic','Pixar','3D Disney','Comic','Watercolor','Oil painting'],Quality:['Realistic skin','Skin pores','Fine detail','Natural texture'],Lighting:['Golden hour','Soft studio','Window light','Overcast','Cinematic lighting']}
|
||||||
export function normalizeStyles(value={}) { const known=Object.values(styleGroups).flat();const positive=[...new Set((value.positive||[]).filter(s=>known.includes(s)))];return {positive,negative:[...new Set((value.negative||[]).filter(s=>known.includes(s)&&!positive.includes(s)))]} }
|
export const defaultNegatives=['blurry','deformed','low quality','text','watermark']
|
||||||
|
export function normalizeStyles(value={}) { const known=[...Object.values(styleGroups).flat(),...defaultNegatives];const positive=[...new Set((value.positive||[]).filter(s=>known.includes(s)))];return {positive,negative:[...new Set((value.negative||[]).filter(s=>known.includes(s)&&!positive.includes(s)))]} }
|
||||||
export function toggleStyle(value,chip,negative=false) { const next=normalizeStyles(value),key=negative?'negative':'positive',other=negative?'positive':'negative';next[other]=next[other].filter(s=>s!==chip);next[key]=next[key].includes(chip)?next[key].filter(s=>s!==chip):[...next[key],chip];return next }
|
export function toggleStyle(value,chip,negative=false) { const next=normalizeStyles(value),key=negative?'negative':'positive',other=negative?'positive':'negative';next[other]=next[other].filter(s=>s!==chip);next[key]=next[key].includes(chip)?next[key].filter(s=>s!==chip):[...next[key],chip];return next }
|
||||||
export function stylePrompt(value,negative=false){return normalizeStyles(value)[negative?'negative':'positive'].join(', ')}
|
export function stylePrompt(value,negative=false){return normalizeStyles(value)[negative?'negative':'positive'].join(', ')}
|
||||||
export function parseBatch(value){return String(value||'').split(/\r?\n\s*\r?\n/).map(s=>s.trim()).filter(Boolean)}
|
export function parseBatch(value){return String(value||'').split(/\r?\n\s*\r?\n/).map(s=>s.trim()).filter(Boolean)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user