@telepat/rilo 0.1.0 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -176
- package/README.zh-CN.md +100 -0
- package/frontend/dist/assets/index-BvNWHzrr.js +13 -0
- package/frontend/dist/assets/index-Wcsyfv3q.css +1 -0
- package/frontend/dist/favicon.svg +19 -0
- package/frontend/dist/index.html +14 -0
- package/package.json +3 -1
- package/src/api/middleware/auth.js +67 -11
- package/src/api/openapi/spec.js +3 -1
- package/src/api/routes/jobs.js +21 -2
- package/src/api/routes/projects.js +16 -1
- package/src/api/server.js +42 -8
- package/src/cli/commands/preview.js +126 -0
- package/src/cli/index.js +42 -3
- package/src/pipeline/orchestrator.js +17 -1
- package/src/store/projectStore.js +5 -0
- package/src/types/job.js +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{color-scheme:dark;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:15px;--bg: #0d1117;--bg-card: #161b22;--bg-elevated: #1c2330;--border: #30363d;--border-subtle: #21262d;--text: #e6edf3;--text-muted: #7d8590;--text-subtle: #484f58;--accent: #ff4e00;--accent-hover: #ff7a00;--green: #3fb950;--yellow: #d29922;--orange: #db6d28;--red: #f85149;--flame: #ff4e00;--flame-mid: #ff7a00;--flame-yellow: #ffcc00;--sidebar-w: 260px;background:var(--bg);color:var(--text)}*,*:before,*:after{box-sizing:border-box}body{margin:0;height:100dvh;overflow:hidden;background:var(--bg)}button,input,textarea,select{font:inherit;color:inherit}button{cursor:pointer}p,h2,h3{margin:0}textarea,input{border:1px solid var(--border);border-radius:8px;padding:.55rem .75rem;background:var(--bg);color:var(--text);width:100%;resize:vertical;transition:border-color .15s}textarea:focus,input:focus{outline:none;border-color:var(--accent)}.app-shell{display:grid;grid-template-columns:var(--sidebar-w) 1fr;height:100dvh;overflow:hidden}.sidebar{display:flex;flex-direction:column;border-right:1px solid var(--border);background:var(--bg-card);overflow:hidden}.sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:1.1rem 1rem;border-bottom:1px solid var(--border-subtle);flex-shrink:0}.brand-wordmark{font-size:1.1rem;font-weight:800;letter-spacing:.02em;text-transform:uppercase;color:var(--text);flex-shrink:0}.sidebar-section{flex:1;display:flex;flex-direction:column;gap:.65rem;padding:.9rem;overflow:hidden}.sidebar-row{display:flex;align-items:center;justify-content:space-between}.sidebar-section-title{font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted)}.project-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px;overflow-y:auto;flex:1;min-height:0}.project-item{width:100%;text-align:left;background:transparent;border:1px solid transparent;border-radius:6px;padding:.55rem .75rem;font-size:.88rem;color:var(--text-muted);transition:background .1s,color .1s,border-color .1s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.project-item:hover{background:var(--bg-elevated);color:var(--text)}.project-item-active{background:var(--bg-elevated);border-color:var(--border);border-left-color:var(--accent);color:var(--text);font-weight:500}.sidebar-footer{padding:.6rem .75rem;border-top:1px solid var(--border-subtle);flex-shrink:0}.main-area{display:flex;flex-direction:column;overflow:hidden}.project-header{flex-shrink:0;background:var(--bg-card);border-bottom:1px solid var(--border);padding:1rem 1.5rem 0;display:flex;flex-direction:column;gap:.75rem;z-index:10}.project-header-top{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:wrap}.project-title-row{display:flex;align-items:center;gap:.6rem;min-width:0}.project-name{font-size:1.05rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.header-actions{display:flex;gap:.4rem;align-items:center;flex-wrap:wrap;flex-shrink:0}.step-dots{display:flex;gap:0;align-items:center;overflow-x:auto;padding-bottom:.15rem}.dot{display:flex;align-items:center;gap:.35rem;font-size:.75rem;padding:.2rem .65rem;border-radius:20px;transition:background .2s;white-space:nowrap;flex-shrink:0}.dot:before{content:"";width:7px;height:7px;border-radius:50%;flex-shrink:0}.dot-done{color:var(--green)}.dot-done:before{background:var(--green)}.dot-running{color:var(--yellow);animation:pulse 1.2s ease-in-out infinite}.dot-running:before{background:var(--yellow)}.dot-idle{color:var(--text-muted)}.dot-idle:before{background:var(--border)}.dot-label{font-weight:500}@keyframes pulse{0%,to{opacity:1}50%{opacity:.45}}.tab-spacer{flex:1}.analytics-pane{gap:1.25rem}.analytics-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem}@media(max-width:800px){.analytics-summary{grid-template-columns:repeat(2,1fr)}}.analytics-stat-card{background:var(--bg-card);border:1px solid var(--border-subtle);border-radius:10px;padding:1.2rem 1.4rem;display:flex;flex-direction:column;gap:.4rem}.stat-value{font-size:1.5rem;font-weight:600;color:var(--text);line-height:1}.stat-label{font-size:.78rem;color:var(--text-muted)}.stat-ok{color:var(--green)}.stat-fail{color:var(--red)}.analytics-empty{margin-top:.25rem}.analytics-error{color:var(--red)}.analytics-runs{display:flex;flex-direction:column;gap:.35rem}.analytics-runs-title{font-size:.78rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.25rem}.run-row{background:var(--bg-card);border:1px solid var(--border-subtle);border-radius:8px;overflow:hidden}.run-row-header{display:flex;align-items:center;gap:.75rem;width:100%;background:transparent;border:none;padding:.8rem 1rem;text-align:left;cursor:pointer;transition:background .12s}.run-row-header:hover{background:var(--bg-elevated)}.run-badge{font-size:.72rem;font-weight:600;padding:.15em .55em;border-radius:6px;flex-shrink:0}.run-badge-ok{background:color-mix(in srgb,var(--green) 15%,transparent);color:var(--green)}.run-badge-fail{background:color-mix(in srgb,var(--red) 15%,transparent);color:var(--red)}.run-badge-running{background:color-mix(in srgb,var(--yellow) 15%,transparent);color:var(--yellow)}.run-row-date{font-size:.82rem;color:var(--text);flex:1}.run-row-meta{font-size:.82rem;color:var(--text-muted);flex-shrink:0}.run-row-cost{min-width:5rem;text-align:right}.run-row-chevron{font-size:.65rem;color:var(--text-subtle);flex-shrink:0}.run-stages{border-top:1px solid var(--border-subtle);padding:.75rem 1rem 1rem}.stages-table{width:100%;border-collapse:collapse;font-size:.82rem}.stages-table th{text-align:left;color:var(--text-muted);font-weight:500;padding:.2rem .6rem .4rem;border-bottom:1px solid var(--border-subtle)}.stages-table td{padding:.3rem .6rem;color:var(--text)}.stage-name-cell{display:flex;align-items:center;gap:.45rem}.stage-status-cell{color:var(--text-muted)}.stage-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0;display:inline-block}.stage-dot-ok{background:var(--green)}.stage-dot-reused{background:var(--accent)}.stage-dot-fail{background:var(--red)}.stage-dot-running{background:var(--yellow)}.stage-dot-idle{background:var(--border)}.stage-row-pending td{color:var(--text-subtle)}.tab-bar{display:flex;gap:0;border-bottom:none;margin:0 -1.5rem;padding:0 1.5rem;overflow-x:auto}.tab{background:transparent;border:none;border-bottom:2px solid transparent;border-radius:0;padding:.7rem 1rem;font-size:.88rem;font-weight:500;color:var(--text-muted);transition:color .15s,border-color .15s;white-space:nowrap;flex-shrink:0;display:flex;align-items:center;gap:.4rem}.tab:hover{color:var(--text)}.tab-active{color:var(--text);border-bottom-color:var(--accent)}.tab-count{font-size:.72rem;background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:10px;padding:.05em .45em;color:var(--text-muted);font-weight:500;min-width:1.4em;text-align:center}.tab-content{flex:1;overflow-y:auto;padding:1.5rem}.tab-pane{display:flex;flex-direction:column;gap:2rem}.config-inner-tab-bar{display:flex;gap:0;border-bottom:1px solid var(--border-subtle);margin-bottom:1.5rem;overflow-x:auto}.config-inner-tab{background:transparent;border:none;border-bottom:2px solid transparent;border-radius:0;padding:.45rem .85rem;font-size:.82rem;font-weight:500;color:var(--text-muted);cursor:pointer;transition:color .15s,border-color .15s;white-space:nowrap;margin-bottom:-1px}.config-inner-tab:hover{color:var(--text)}.config-inner-tab-active{color:var(--text);border-bottom-color:var(--accent)}.btn{background:var(--bg-elevated);border:1px solid var(--border);border-radius:6px;padding:.45rem .9rem;font-size:.88rem;font-weight:500;color:var(--text);transition:background .15s,border-color .15s,opacity .15s;display:inline-flex;align-items:center;gap:.3rem;white-space:nowrap}.btn:hover:not(:disabled){background:#2d3748;border-color:#4a5568}.btn:disabled{opacity:.45;cursor:default}.btn-sm{padding:.3rem .65rem;font-size:.8rem}.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}.btn-primary:hover:not(:disabled){background:var(--accent-hover);border-color:var(--accent-hover)}.btn-secondary{background:color-mix(in srgb,var(--accent) 12%,var(--bg-elevated));border-color:color-mix(in srgb,var(--accent) 35%,transparent);color:var(--accent-hover)}.btn-secondary:hover:not(:disabled){background:color-mix(in srgb,var(--accent) 22%,var(--bg-elevated))}.btn-ghost{background:transparent;border-color:transparent}.btn-ghost:hover:not(:disabled){background:var(--bg-elevated);border-color:var(--border)}.btn-danger{color:var(--red)}.btn-danger:hover:not(:disabled){border-color:var(--red)}.full-width{width:100%;justify-content:center}.badge{font-size:.72rem;font-weight:600;padding:.15em .55em;border-radius:10px;border:1px solid currentColor;text-transform:uppercase;letter-spacing:.05em;flex-shrink:0}.badge-ok{color:var(--green)}.badge-running{color:var(--yellow);animation:pulse 1.2s ease-in-out infinite}.badge-pending{color:var(--orange)}.badge-fail{color:var(--red)}.badge-paused{color:var(--yellow)}.pause-banner{font-size:.82rem;color:var(--yellow);background:color-mix(in srgb,var(--yellow) 10%,transparent);border:1px solid color-mix(in srgb,var(--yellow) 30%,transparent);border-radius:6px;padding:.5rem .75rem;margin-bottom:.5rem}.spinner{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite;flex-shrink:0}@keyframes spin{to{transform:rotate(360deg)}}@keyframes toast-in{0%{opacity:0;transform:translateY(.75rem)}to{opacity:1;transform:translateY(0)}}.toast-popup{position:fixed;bottom:1.25rem;right:1.25rem;z-index:200;display:flex;align-items:center;justify-content:space-between;gap:.75rem;min-width:240px;max-width:400px;padding:.65rem .9rem;border-radius:10px;font-size:.84rem;background:var(--bg-elevated);border:1px solid var(--border);box-shadow:0 8px 32px #00000059;animation:toast-in .18s ease-out}.toast-ok{border-left:3px solid var(--green)}.toast-error{border-left:3px solid var(--red)}.toast-close{background:transparent;border:none;padding:0 .2rem;color:var(--text-muted);font-size:.8rem;flex-shrink:0;cursor:pointer}.toast-close:hover{color:var(--text)}.modal-overlay{position:fixed;inset:0;background:#000000a6;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:100;padding:1rem}.modal-panel{background:var(--bg-card);border:1px solid var(--border);border-radius:14px;width:100%;max-width:540px;max-height:90dvh;overflow-y:auto;box-shadow:0 24px 64px #00000080;outline:none}.modal-header{display:flex;align-items:center;justify-content:space-between;padding:1.15rem 1.5rem;border-bottom:1px solid var(--border-subtle)}.modal-title{font-size:1rem;font-weight:600}.modal-close{background:transparent;border:none;font-size:1rem;color:var(--text-muted);padding:.15rem .35rem;border-radius:4px}.modal-close:hover{color:var(--text);background:var(--bg-elevated)}.modal-body{padding:1.5rem}.modal-form{display:flex;flex-direction:column;gap:1rem}.modal-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.25rem}.field{display:flex;flex-direction:column;gap:.5rem}.field-label{font-size:.82rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}.field-hint{font-size:.78rem;color:var(--text-muted);font-weight:400;text-transform:none;letter-spacing:0}.editor-grid{display:grid;gap:1rem;grid-template-columns:repeat(2,minmax(0,1fr))}.editor-grid .full-width{grid-column:1 / -1}.save-row{display:flex;align-items:center;gap:.75rem}.output-actions-row{margin-top:.75rem;display:flex;gap:.5rem;flex-wrap:wrap}.config-grid{display:grid;gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(190px,1fr))}.config-item{border:1px solid var(--border-subtle);border-radius:10px;background:var(--bg-card);padding:.7rem .8rem;display:flex;flex-direction:column;gap:.3rem}.config-key{font-size:.74rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;font-weight:600}.config-value{font-size:.95rem;color:var(--text);font-weight:500;word-break:break-word}.config-json{margin:0;border:1px solid var(--border-subtle);border-radius:10px;background:var(--bg-card);padding:.85rem;overflow:auto;font-size:.8rem;line-height:1.45;color:var(--text)}.config-form{display:flex;flex-direction:column;gap:1.5rem;margin-bottom:2rem}.config-form-fields{display:flex;flex-direction:column;gap:1rem}.config-group{border:1px solid var(--border);border-radius:10px;background:color-mix(in srgb,var(--bg-elevated) 65%,transparent);padding:1rem 1.15rem}.config-group-header{margin-bottom:.9rem}.config-group-title{margin:0;font-size:.86rem;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--text)}.config-group-note{margin:.2rem 0 0}.config-group-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.config-form-field{display:flex;flex-direction:column;gap:.35rem}.config-form-field:has(input[type=checkbox]){flex-direction:row;align-items:center;gap:.5rem}.config-form-field:has(input[type=checkbox]) input[type=checkbox]{width:1rem;height:1rem;flex-shrink:0;cursor:pointer;order:-1}.config-form-field--readonly{justify-content:flex-end;padding:.4rem 0 .1rem}.config-form-label{font-size:.74rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;font-weight:600}.config-form-optional{font-weight:400;text-transform:none;letter-spacing:0;opacity:.7}.config-select,.config-input{background:var(--bg-elevated);border:1px solid var(--border);border-radius:7px;color:var(--text);font-size:.9rem;padding:.45rem .65rem;width:100%;outline:none;transition:border-color .15s;box-sizing:border-box;font-family:inherit;appearance:auto}.config-select:focus,.config-input:focus{border-color:var(--accent)}.config-form-actions{display:flex;align-items:center;gap:.75rem}.config-color-input-row{display:flex;align-items:center;gap:.5rem}.config-inline-actions{display:flex;gap:.5rem;flex-wrap:wrap}.config-color-picker{width:2.35rem;height:2.1rem;border:1px solid var(--border);border-radius:7px;padding:.1rem;background:var(--bg-elevated);cursor:pointer;flex-shrink:0}@media(max-width:860px){.config-group-grid{grid-template-columns:1fr}}.combo-box{position:relative}.combo-box-list{position:absolute;top:100%;left:0;right:0;z-index:200;margin:2px 0 0;padding:0;list-style:none;background:var(--bg-elevated);border:1px solid var(--border);border-radius:7px;box-shadow:0 4px 16px #00000040;max-height:220px;overflow-y:auto}.combo-box-option{padding:.45rem .65rem;font-size:.9rem;color:var(--text);cursor:pointer}.combo-box-option:hover,.combo-box-option--active{background:var(--accent);color:#fff}.model-config-section{border:1px solid var(--border);border-radius:10px;margin-bottom:1.5rem;overflow:hidden}.model-config-toggle{all:unset;display:flex;align-items:center;justify-content:space-between;width:100%;gap:.75rem;padding:.9rem 1.15rem;cursor:pointer;background:var(--bg-elevated);box-sizing:border-box;transition:background .15s}.model-config-toggle:hover{background:var(--bg-hover, color-mix(in srgb, var(--bg-elevated) 85%, var(--accent)))}.model-config-summary{display:flex;flex-direction:column;gap:.2rem;min-width:0}.model-config-name{font-size:.92rem;font-weight:600;color:var(--text)}.model-config-url{font-size:.78rem}a.model-config-url{color:var(--accent);text-decoration:none}a.model-config-url:hover{text-decoration:underline}.model-config-pricing{font-size:.78rem}.model-config-chevron{flex-shrink:0;font-size:.72rem;color:var(--text-muted)}.model-config-body{padding:1.15rem 1.25rem;border-top:1px solid var(--border);background:var(--bg)}.model-config-body .config-form{margin-bottom:0}.asset-grid{display:grid;gap:1rem;grid-template-columns:repeat(auto-fill,minmax(var(--grid-col-min, 200px),1fr))}.asset-card{border:1px solid var(--border-subtle);border-radius:10px;padding:.65rem;display:flex;flex-direction:column;gap:.6rem;background:var(--bg-card)}.asset-card>.btn:last-child{margin-top:auto}.asset-index{font-size:.72rem;color:var(--text-muted);font-weight:600;padding:0 .15rem}.asset-status-row{display:flex;align-items:center;justify-content:space-between;gap:.45rem}.card-prompt{display:flex;flex-direction:column;gap:.3rem}.card-prompt-header{display:flex;align-items:center;justify-content:space-between}.card-prompt-edit-btn{padding:.1rem .35rem;font-size:.75rem;opacity:.6;flex-shrink:0}.card-prompt-edit-btn:hover:not(:disabled){opacity:1}.card-prompt-row{display:flex;align-items:flex-start;gap:.35rem}.prompt-text{font-size:.78rem;color:var(--text-muted);line-height:1.45;margin:0;flex:1}.prompt-textarea{font-size:.8rem;resize:none;overflow:hidden;min-height:3.5rem}.prompt-edit-actions{display:flex;gap:.4rem}.media-wrap{aspect-ratio:var(--ar, 9/16);width:100%;background:#000;border-radius:8px;overflow:hidden;display:flex;align-items:center;justify-content:center}.media-wrap img,.media-wrap video{width:100%;height:100%;object-fit:cover;display:block}.media-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:var(--bg-elevated)}.media-placeholder-content{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.45rem;text-align:center;padding:.65rem}.media-placeholder-title{font-size:.76rem;color:var(--text-muted);font-weight:500}.final-video-section{margin-bottom:.5rem}.final-video-wrap{margin:0 auto}.tab-content:has(.tab-pane-output){overflow:hidden;display:flex;flex-direction:column}.tab-pane-output{flex:1;min-height:0;display:flex;flex-direction:column;gap:0}.tab-pane-output .final-video-section{flex:1;min-height:0;margin-bottom:0;display:flex;flex-direction:column}.tab-pane-output .final-video-wrap{flex:1;min-height:0;display:flex;align-items:center;justify-content:center}.tab-pane-output .media-wrap{aspect-ratio:unset;height:100%;width:auto;max-width:100%}.section-label{font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);margin-bottom:.65rem}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.85rem;padding:3.5rem 1.5rem;text-align:center;height:100%;min-height:200px}.empty-state-title{font-size:1.15rem;font-weight:600;color:var(--text)}.empty-state-brand{font-size:clamp(1.8rem,5vw,2.4rem);font-weight:900;letter-spacing:.04em;text-transform:uppercase;color:var(--text);opacity:.9;margin-bottom:.5rem}.muted{color:var(--text-muted)}.size-sm{font-size:.82rem}.size-xs{font-size:.75rem}@media(max-width:900px){:root{--sidebar-w: 200px}.editor-grid{grid-template-columns:1fr}.project-header-top{gap:.5rem}}@media(max-width:640px){.app-shell{grid-template-columns:1fr}.sidebar{display:none}body{overflow:auto}}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="flameGrad" x1="0%" y1="100%" x2="0%" y2="0%">
|
|
4
|
+
<stop offset="0%" stop-color="#ff3d00"/>
|
|
5
|
+
<stop offset="60%" stop-color="#ff7a00"/>
|
|
6
|
+
<stop offset="100%" stop-color="#ffcc00"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
|
|
10
|
+
<!-- Rounded square background -->
|
|
11
|
+
<rect width="32" height="32" rx="7" fill="#111111"/>
|
|
12
|
+
|
|
13
|
+
<!-- Flame: main triangle, centered in 32x32, with ~4px padding -->
|
|
14
|
+
<!-- Points: tip at top-center, base spanning bottom -->
|
|
15
|
+
<polygon points="16,5 26,27 6,27" fill="#ff4e00"/>
|
|
16
|
+
<!-- Inner lighter triangle -->
|
|
17
|
+
<polygon points="16,12 22,27 10,27" fill="#ff7a00"/>
|
|
18
|
+
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
|
+
<title>Rilo</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-BvNWHzrr.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Wcsyfv3q.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telepat/rilo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"rilo": "src/cli/index.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"src/",
|
|
10
|
+
"frontend/dist/",
|
|
10
11
|
"models/",
|
|
11
12
|
"index.js",
|
|
12
13
|
"README.md"
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"frontend:lint": "npm --prefix frontend run lint",
|
|
31
32
|
"frontend:build": "npm --prefix frontend run build",
|
|
32
33
|
"frontend:preview": "npm --prefix frontend run preview",
|
|
34
|
+
"prepack": "node -e \"const fs=require('node:fs'); if(!fs.existsSync('frontend/dist/index.html')) { console.error('Missing frontend build output at frontend/dist/index.html. Run npm run build before publish.'); process.exit(1); }\"",
|
|
33
35
|
"docs:start": "npm --prefix docs-site run start",
|
|
34
36
|
"docs:build": "npm --prefix docs-site run build",
|
|
35
37
|
"docs:serve": "npm --prefix docs-site run serve",
|
|
@@ -5,6 +5,32 @@ function unauthorized(res) {
|
|
|
5
5
|
res.status(401).json({ error: 'Unauthorized' });
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
function isLoopbackAddress(address) {
|
|
9
|
+
if (!address || typeof address !== 'string') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const normalized = address.toLowerCase();
|
|
14
|
+
return normalized === '127.0.0.1'
|
|
15
|
+
|| normalized === '::1'
|
|
16
|
+
|| normalized === '::ffff:127.0.0.1';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function shouldBypassAuth(req, options) {
|
|
20
|
+
if (!options.previewMode) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (options.allowUnauthenticatedExposedPreview) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const socketAddress = typeof req?.socket?.remoteAddress === 'string'
|
|
29
|
+
? req.socket.remoteAddress
|
|
30
|
+
: '';
|
|
31
|
+
return isLoopbackAddress(socketAddress);
|
|
32
|
+
}
|
|
33
|
+
|
|
8
34
|
function getTokenFromAuthorizationHeader(req) {
|
|
9
35
|
const authHeader = req.get('authorization');
|
|
10
36
|
if (!authHeader) {
|
|
@@ -51,20 +77,50 @@ export function isAuthorizedApiRequest(req, { allowQueryAccessToken = false } =
|
|
|
51
77
|
return isMatchingToken(queryToken);
|
|
52
78
|
}
|
|
53
79
|
|
|
54
|
-
export function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
export function createAuthGuards(options = {}) {
|
|
81
|
+
const authOptions = {
|
|
82
|
+
previewMode: Boolean(options.previewMode),
|
|
83
|
+
allowUnauthenticatedExposedPreview: Boolean(options.allowUnauthenticatedExposedPreview)
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function requireBearerTokenWithOptions(req, res, next) {
|
|
87
|
+
if (shouldBypassAuth(req, authOptions)) {
|
|
88
|
+
next();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!isAuthorizedApiRequest(req)) {
|
|
93
|
+
unauthorized(res);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
next();
|
|
58
98
|
}
|
|
59
99
|
|
|
60
|
-
next
|
|
61
|
-
|
|
100
|
+
function requireBearerTokenOrAccessTokenWithOptions(req, res, next) {
|
|
101
|
+
if (shouldBypassAuth(req, authOptions)) {
|
|
102
|
+
next();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
62
105
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
if (!isAuthorizedApiRequest(req, { allowQueryAccessToken: true })) {
|
|
107
|
+
unauthorized(res);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
next();
|
|
67
112
|
}
|
|
68
113
|
|
|
69
|
-
|
|
114
|
+
return {
|
|
115
|
+
requireBearerToken: requireBearerTokenWithOptions,
|
|
116
|
+
requireBearerTokenOrAccessToken: requireBearerTokenOrAccessTokenWithOptions
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function requireBearerToken(req, res, next) {
|
|
121
|
+
return createAuthGuards().requireBearerToken(req, res, next);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function requireBearerTokenOrAccessToken(req, res, next) {
|
|
125
|
+
return createAuthGuards().requireBearerTokenOrAccessToken(req, res, next);
|
|
70
126
|
}
|
package/src/api/openapi/spec.js
CHANGED
|
@@ -57,7 +57,8 @@ const schemas = {
|
|
|
57
57
|
properties: {
|
|
58
58
|
story: { type: 'string' },
|
|
59
59
|
project: { type: 'string' },
|
|
60
|
-
forceRestart: { type: 'boolean' }
|
|
60
|
+
forceRestart: { type: 'boolean' },
|
|
61
|
+
pauseAfterKeyframes: { type: 'boolean' }
|
|
61
62
|
},
|
|
62
63
|
required: ['story'],
|
|
63
64
|
additionalProperties: true
|
|
@@ -180,6 +181,7 @@ const schemas = {
|
|
|
180
181
|
type: 'object',
|
|
181
182
|
properties: {
|
|
182
183
|
forceRestart: { type: 'boolean' },
|
|
184
|
+
pauseAfterKeyframes: { type: 'boolean' },
|
|
183
185
|
targetType: {
|
|
184
186
|
type: 'string',
|
|
185
187
|
enum: ['script', 'voiceover', 'keyframe', 'segment', 'align', 'burnin']
|
package/src/api/routes/jobs.js
CHANGED
|
@@ -7,12 +7,27 @@ export function createJobsRouter() {
|
|
|
7
7
|
const router = express.Router();
|
|
8
8
|
|
|
9
9
|
router.post('/', async (req, res) => {
|
|
10
|
-
const { story, project, forceRestart } = req.body || {};
|
|
10
|
+
const { story, project, forceRestart, pauseAfterKeyframes } = req.body || {};
|
|
11
11
|
if (!story) {
|
|
12
12
|
res.status(400).json({ error: 'story is required' });
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
if (typeof story !== 'string') {
|
|
17
|
+
res.status(400).json({ error: 'story must be a string' });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (forceRestart !== undefined && typeof forceRestart !== 'boolean') {
|
|
22
|
+
res.status(400).json({ error: 'forceRestart must be a boolean when provided' });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (pauseAfterKeyframes !== undefined && typeof pauseAfterKeyframes !== 'boolean') {
|
|
27
|
+
res.status(400).json({ error: 'pauseAfterKeyframes must be a boolean when provided' });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
const resolvedProject = resolveProjectName(project || `api-${Date.now()}`);
|
|
17
32
|
const existingActiveJob = findActiveJobByProject(resolvedProject);
|
|
18
33
|
if (existingActiveJob) {
|
|
@@ -28,7 +43,11 @@ export function createJobsRouter() {
|
|
|
28
43
|
await writeProjectStory(resolvedProject, story);
|
|
29
44
|
|
|
30
45
|
const job = createJob({ story, project: resolvedProject });
|
|
31
|
-
|
|
46
|
+
const pipelineOptions = { forceRestart: forceRestart === true };
|
|
47
|
+
if (pauseAfterKeyframes !== undefined) {
|
|
48
|
+
pipelineOptions.pauseAfterKeyframes = pauseAfterKeyframes;
|
|
49
|
+
}
|
|
50
|
+
setImmediate(() => runPipeline(job.id, pipelineOptions));
|
|
32
51
|
res.status(202).json({ jobId: job.id, status: job.status });
|
|
33
52
|
});
|
|
34
53
|
|
|
@@ -578,6 +578,7 @@ export function createProjectsRouter(deps = {}) {
|
|
|
578
578
|
|
|
579
579
|
const {
|
|
580
580
|
forceRestart,
|
|
581
|
+
pauseAfterKeyframes,
|
|
581
582
|
targetType,
|
|
582
583
|
index
|
|
583
584
|
} = req.body || {};
|
|
@@ -597,6 +598,11 @@ export function createProjectsRouter(deps = {}) {
|
|
|
597
598
|
return;
|
|
598
599
|
}
|
|
599
600
|
|
|
601
|
+
if (pauseAfterKeyframes !== undefined) {
|
|
602
|
+
res.status(400).json({ error: 'pauseAfterKeyframes is not supported for targeted regeneration' });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
600
606
|
const targetRequiresIndex = normalizedTargetType === 'keyframe' || normalizedTargetType === 'segment';
|
|
601
607
|
if (targetRequiresIndex && !hasIndex) {
|
|
602
608
|
res.status(400).json({ error: 'index is required for keyframe/segment targeted regeneration' });
|
|
@@ -628,9 +634,18 @@ export function createProjectsRouter(deps = {}) {
|
|
|
628
634
|
return;
|
|
629
635
|
}
|
|
630
636
|
|
|
637
|
+
if (pauseAfterKeyframes !== undefined && typeof pauseAfterKeyframes !== 'boolean') {
|
|
638
|
+
res.status(400).json({ error: 'pauseAfterKeyframes must be a boolean when provided' });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
631
642
|
const story = await readProjectStory(project);
|
|
632
643
|
const job = createJobFn({ story, project });
|
|
633
|
-
|
|
644
|
+
const pipelineOptions = { forceRestart: forceRestart === true };
|
|
645
|
+
if (pauseAfterKeyframes !== undefined) {
|
|
646
|
+
pipelineOptions.pauseAfterKeyframes = pauseAfterKeyframes;
|
|
647
|
+
}
|
|
648
|
+
setImmediate(() => runPipelineFn(job.id, pipelineOptions));
|
|
634
649
|
|
|
635
650
|
res.status(202).json({
|
|
636
651
|
jobId: job.id,
|
package/src/api/server.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { pathToFileURL } from 'node:url';
|
|
3
5
|
import { assertRequiredApiEnv, env } from '../config/env.js';
|
|
4
6
|
import { createJobsRouter } from './routes/jobs.js';
|
|
5
7
|
import { createProjectAssetsRouter } from './routes/projectAssets.js';
|
|
6
8
|
import { createProjectsRouter } from './routes/projects.js';
|
|
7
9
|
import { createWebhookRouter } from './routes/webhooks.js';
|
|
8
|
-
import {
|
|
10
|
+
import { createAuthGuards } from './middleware/auth.js';
|
|
9
11
|
import { buildOpenApiSpec } from './openapi/spec.js';
|
|
10
12
|
|
|
11
13
|
const swaggerUiHtml = `<!doctype html>
|
|
@@ -42,11 +44,29 @@ function getRequestBaseUrl(req, { fallbackPort = 3000 } = {}) {
|
|
|
42
44
|
return `http://localhost:${fallbackPort}`;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
function hasDashboardBundle(dashboardDir) {
|
|
48
|
+
if (!dashboardDir || typeof dashboardDir !== 'string') {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return fs.existsSync(path.join(dashboardDir, 'index.html'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createApiApp({ baseUrl, dashboardDir, auth } = {}) {
|
|
56
|
+
const authOptions = {
|
|
57
|
+
previewMode: Boolean(auth?.previewMode),
|
|
58
|
+
allowUnauthenticatedExposedPreview: Boolean(auth?.allowUnauthenticatedExposedPreview)
|
|
59
|
+
};
|
|
60
|
+
const shouldRequireApiToken = !authOptions.previewMode;
|
|
61
|
+
if (shouldRequireApiToken) {
|
|
62
|
+
assertRequiredApiEnv();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const guards = createAuthGuards(authOptions);
|
|
66
|
+
const dashboardEnabled = hasDashboardBundle(dashboardDir);
|
|
47
67
|
|
|
48
68
|
const app = express();
|
|
49
|
-
app.set('trust proxy',
|
|
69
|
+
app.set('trust proxy', !authOptions.previewMode);
|
|
50
70
|
app.use(express.json({ limit: '2mb' }));
|
|
51
71
|
|
|
52
72
|
app.get('/health', (_req, res) => {
|
|
@@ -67,16 +87,30 @@ export function createApiApp({ baseUrl } = {}) {
|
|
|
67
87
|
});
|
|
68
88
|
|
|
69
89
|
app.use('/webhooks', createWebhookRouter());
|
|
70
|
-
app.use('/projects', requireBearerTokenOrAccessToken, createProjectAssetsRouter());
|
|
71
|
-
app.use(requireBearerToken);
|
|
90
|
+
app.use('/projects', guards.requireBearerTokenOrAccessToken, createProjectAssetsRouter());
|
|
91
|
+
app.use(guards.requireBearerToken);
|
|
72
92
|
app.use('/jobs', createJobsRouter());
|
|
73
93
|
app.use('/projects', createProjectsRouter());
|
|
74
94
|
|
|
95
|
+
if (dashboardEnabled) {
|
|
96
|
+
app.use(express.static(dashboardDir));
|
|
97
|
+
app.get('/', (_req, res) => {
|
|
98
|
+
res.sendFile(path.join(dashboardDir, 'index.html'));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
return app;
|
|
76
103
|
}
|
|
77
104
|
|
|
78
|
-
export function startApiServer({ port = env.port, baseUrl } = {}) {
|
|
79
|
-
const app = createApiApp({ baseUrl });
|
|
105
|
+
export function startApiServer({ port = env.port, host, baseUrl, dashboardDir, auth } = {}) {
|
|
106
|
+
const app = createApiApp({ baseUrl, dashboardDir, auth });
|
|
107
|
+
|
|
108
|
+
if (host) {
|
|
109
|
+
return app.listen(port, host, () => {
|
|
110
|
+
console.log(`rilo api listening on ${host}:${port}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
80
114
|
return app.listen(port, () => {
|
|
81
115
|
console.log(`rilo api listening on :${port}`);
|
|
82
116
|
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { startApiServer } from '../../api/server.js';
|
|
6
|
+
import { openPath } from './openHome.js';
|
|
7
|
+
|
|
8
|
+
function parsePort(value, fallback) {
|
|
9
|
+
if (!value) {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parsed = Number(value);
|
|
14
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
15
|
+
throw new Error(`Invalid port: ${value}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveDashboardDir() {
|
|
22
|
+
const commandDir = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
return path.resolve(commandDir, '../../../frontend/dist');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveWorkerEntryPoint() {
|
|
27
|
+
const commandDir = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
return path.resolve(commandDir, '../../worker/processor.js');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildWorkerProcess() {
|
|
32
|
+
const workerPath = resolveWorkerEntryPoint();
|
|
33
|
+
return spawn(process.execPath, [workerPath], {
|
|
34
|
+
stdio: 'inherit'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printUnsafeExposeWarning(url) {
|
|
39
|
+
console.warn('WARNING: running preview in exposed mode without auth.');
|
|
40
|
+
console.warn(`WARNING: dashboard and API are reachable from ${url}`);
|
|
41
|
+
console.warn('WARNING: use only on trusted networks or isolated containers.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveOpenUrl(host, port) {
|
|
45
|
+
if (host === '0.0.0.0' || host === '::') {
|
|
46
|
+
return `http://127.0.0.1:${port}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return `http://${host}:${port}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function startPreview({ args = process.argv.slice(3) } = {}) {
|
|
53
|
+
const expose = args.includes('--expose');
|
|
54
|
+
const unsafeNoAuth = args.includes('--unsafe-no-auth');
|
|
55
|
+
const noOpen = args.includes('--no-open');
|
|
56
|
+
|
|
57
|
+
if (!expose && unsafeNoAuth) {
|
|
58
|
+
throw new Error('--unsafe-no-auth can only be used with --expose');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (expose && !unsafeNoAuth) {
|
|
62
|
+
throw new Error('Exposed preview requires --unsafe-no-auth');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const portArgIndex = args.indexOf('--port');
|
|
66
|
+
const portArg = portArgIndex === -1 ? null : args[portArgIndex + 1] || null;
|
|
67
|
+
const port = parsePort(portArg, 3000);
|
|
68
|
+
|
|
69
|
+
const hostArgIndex = args.indexOf('--host');
|
|
70
|
+
const hostArg = hostArgIndex === -1 ? null : args[hostArgIndex + 1] || null;
|
|
71
|
+
const host = hostArg || (expose ? '0.0.0.0' : '127.0.0.1');
|
|
72
|
+
|
|
73
|
+
const dashboardDir = resolveDashboardDir();
|
|
74
|
+
if (!fs.existsSync(path.join(dashboardDir, 'index.html'))) {
|
|
75
|
+
throw new Error('Dashboard bundle not found. Run `npm run frontend:build` first.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const apiServer = startApiServer({
|
|
79
|
+
port,
|
|
80
|
+
host,
|
|
81
|
+
dashboardDir,
|
|
82
|
+
auth: {
|
|
83
|
+
previewMode: true,
|
|
84
|
+
allowUnauthenticatedExposedPreview: unsafeNoAuth
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const worker = buildWorkerProcess();
|
|
89
|
+
const openUrl = resolveOpenUrl(host, port);
|
|
90
|
+
|
|
91
|
+
const stopAll = () => {
|
|
92
|
+
if (!worker.killed) {
|
|
93
|
+
worker.kill('SIGTERM');
|
|
94
|
+
}
|
|
95
|
+
apiServer.close();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
process.once('SIGINT', () => {
|
|
99
|
+
stopAll();
|
|
100
|
+
process.exit(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
process.once('SIGTERM', () => {
|
|
104
|
+
stopAll();
|
|
105
|
+
process.exit(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
worker.once('exit', (code) => {
|
|
109
|
+
apiServer.close(() => {
|
|
110
|
+
if (typeof code === 'number' && code !== 0) {
|
|
111
|
+
process.exit(code);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (unsafeNoAuth) {
|
|
117
|
+
printUnsafeExposeWarning(openUrl);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`Dashboard: ${openUrl}`);
|
|
121
|
+
console.log(`Docs: ${openUrl}/docs`);
|
|
122
|
+
|
|
123
|
+
if (!noOpen) {
|
|
124
|
+
await openPath(openUrl);
|
|
125
|
+
}
|
|
126
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '../store/projectStore.js';
|
|
15
15
|
import { openSettings } from './commands/settingsFlow.js';
|
|
16
16
|
import { openHome } from './commands/openHome.js';
|
|
17
|
+
import { startPreview } from './commands/preview.js';
|
|
17
18
|
import { applyStoredSettings } from '../config/env.js';
|
|
18
19
|
|
|
19
20
|
function getArg(flag) {
|
|
@@ -46,10 +47,11 @@ async function main() {
|
|
|
46
47
|
|
|
47
48
|
if (process.argv.includes('--help')) {
|
|
48
49
|
const helpText = `
|
|
49
|
-
Rilo —
|
|
50
|
+
Rilo — Turn a story into a finished video
|
|
50
51
|
|
|
51
52
|
USAGE
|
|
52
53
|
rilo --project <name> [--story-file <path>] [--force]
|
|
54
|
+
rilo preview [--port <n>] [--host <host>] [--no-open] [--expose --unsafe-no-auth]
|
|
53
55
|
rilo settings
|
|
54
56
|
rilo home
|
|
55
57
|
rilo --help
|
|
@@ -62,6 +64,7 @@ COMMANDS
|
|
|
62
64
|
--project <name> Project identifier (required); creates projects/<name>/
|
|
63
65
|
--story-file <path> Path to story text file (required on first run)
|
|
64
66
|
--force Force restart from earlier stages (use after config changes)
|
|
67
|
+
--full-run Skip the pause after keyframe generation and run all the way through
|
|
65
68
|
|
|
66
69
|
rilo settings
|
|
67
70
|
Configure API tokens, timeouts, and binary paths interactively
|
|
@@ -69,9 +72,19 @@ COMMANDS
|
|
|
69
72
|
rilo home
|
|
70
73
|
Open ~/.rilo, the default home for projects and output files
|
|
71
74
|
|
|
75
|
+
rilo preview [--port <n>] [--host <host>] [--no-open] [--expose --unsafe-no-auth]
|
|
76
|
+
Start API, worker, and dashboard preview for local use
|
|
77
|
+
|
|
78
|
+
--port <n> API/dashboard port (default: 3000)
|
|
79
|
+
--host <host> Host bind address (default: 127.0.0.1)
|
|
80
|
+
--no-open Do not auto-open browser
|
|
81
|
+
--expose Bind preview for external/container access
|
|
82
|
+
--unsafe-no-auth Allow unauthenticated exposed preview (required with --expose)
|
|
83
|
+
|
|
72
84
|
FLAGS
|
|
73
85
|
--help Show this help message
|
|
74
86
|
--version Show version information
|
|
87
|
+
--full-run Skip the keyframe review pause and run all pipeline stages
|
|
75
88
|
|
|
76
89
|
EXAMPLES
|
|
77
90
|
# First run: create project and generate
|
|
@@ -89,6 +102,12 @@ EXAMPLES
|
|
|
89
102
|
# Open the default Rilo home folder
|
|
90
103
|
rilo home
|
|
91
104
|
|
|
105
|
+
# Run local dashboard/API preview
|
|
106
|
+
rilo preview
|
|
107
|
+
|
|
108
|
+
# Expose preview over container or tunnel (unsafe)
|
|
109
|
+
rilo preview --expose --unsafe-no-auth --host 0.0.0.0 --port 3000
|
|
110
|
+
|
|
92
111
|
# Using npx (no installation needed)
|
|
93
112
|
npx @telepat/rilo --project wedding-case --story-file ./story.txt
|
|
94
113
|
npx @telepat/rilo home
|
|
@@ -115,6 +134,9 @@ SETTINGS
|
|
|
115
134
|
Configure via interactive menu:
|
|
116
135
|
rilo settings
|
|
117
136
|
|
|
137
|
+
Recommended install:
|
|
138
|
+
npm install -g @telepat/rilo
|
|
139
|
+
|
|
118
140
|
Or with environment variables:
|
|
119
141
|
export RILO_REPLICATE_API_TOKEN=r8_xxxxx
|
|
120
142
|
export RILO_MAX_RETRIES=5
|
|
@@ -129,7 +151,7 @@ SETTINGS
|
|
|
129
151
|
INVOCATION METHODS
|
|
130
152
|
Global install: rilo --project <name> --story-file <path>
|
|
131
153
|
No install (npx): npx @telepat/rilo --project <name> --story-file <path>
|
|
132
|
-
|
|
154
|
+
Contributor workflow: npm run dev -- --project <name> --story-file <path>
|
|
133
155
|
`;
|
|
134
156
|
console.log(helpText);
|
|
135
157
|
return;
|
|
@@ -146,6 +168,12 @@ INVOCATION METHODS
|
|
|
146
168
|
return;
|
|
147
169
|
}
|
|
148
170
|
|
|
171
|
+
if (process.argv[2] === 'preview') {
|
|
172
|
+
await applyStoredSettings();
|
|
173
|
+
await startPreview();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
149
177
|
// Merge stored settings (config.json + keystore) into env before running
|
|
150
178
|
await applyStoredSettings();
|
|
151
179
|
|
|
@@ -156,6 +184,7 @@ INVOCATION METHODS
|
|
|
156
184
|
|
|
157
185
|
const project = resolveProjectName(projectArg);
|
|
158
186
|
const forceRestart = hasFlag('--force');
|
|
187
|
+
const fullRun = hasFlag('--full-run');
|
|
159
188
|
const storyFile = getArg('--story-file');
|
|
160
189
|
|
|
161
190
|
await ensureProject(project);
|
|
@@ -173,7 +202,17 @@ INVOCATION METHODS
|
|
|
173
202
|
}
|
|
174
203
|
|
|
175
204
|
const job = createJob({ story, project });
|
|
176
|
-
const result = await runPipeline(job.id, { forceRestart });
|
|
205
|
+
const result = await runPipeline(job.id, { forceRestart, pauseAfterKeyframes: !fullRun });
|
|
206
|
+
|
|
207
|
+
if (result.status === 'paused') {
|
|
208
|
+
console.log(JSON.stringify({
|
|
209
|
+
jobId: result.id,
|
|
210
|
+
project,
|
|
211
|
+
status: 'paused',
|
|
212
|
+
message: `Keyframes generated. Review assets in projects/${project}/assets/, then run again to continue to video generation.`
|
|
213
|
+
}, null, 2));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
177
216
|
|
|
178
217
|
if (result.status !== 'completed') {
|
|
179
218
|
throw new Error(`Generation failed: ${result.error || 'unknown error'}`);
|
|
@@ -571,9 +571,10 @@ export async function regenerateProjectAsset(projectName, target, options = {})
|
|
|
571
571
|
}
|
|
572
572
|
|
|
573
573
|
const updatedAt = new Date().toISOString();
|
|
574
|
+
const nextStatus = targetType === 'keyframe' ? JobStatus.PAUSED : (runState.status || JobStatus.COMPLETED);
|
|
574
575
|
await deps.persistArtifacts(project, artifacts);
|
|
575
576
|
await deps.writeProjectRunState(project, {
|
|
576
|
-
status:
|
|
577
|
+
status: nextStatus,
|
|
577
578
|
error: runState.error || null,
|
|
578
579
|
steps,
|
|
579
580
|
artifacts,
|
|
@@ -629,6 +630,9 @@ export async function runPipeline(jobId, options = {}) {
|
|
|
629
630
|
const projectDir = deps.getProjectDir(project);
|
|
630
631
|
const targetDurationSec = resolveTargetDurationSec(projectConfig);
|
|
631
632
|
const plannedShots = resolveShotCount(projectConfig);
|
|
633
|
+
const shouldPause = options.pauseAfterKeyframes !== undefined
|
|
634
|
+
? Boolean(options.pauseAfterKeyframes)
|
|
635
|
+
: Boolean(projectConfig.pauseAfterKeyframes);
|
|
632
636
|
const safeStory = deps.preprocessStory(job.payload.story);
|
|
633
637
|
analyticsProjectDir = projectDir;
|
|
634
638
|
const analyticsRunId = deps.createRunId();
|
|
@@ -1326,6 +1330,12 @@ export async function runPipeline(jobId, options = {}) {
|
|
|
1326
1330
|
await finishStageAnalytics(JobStep.KEYFRAMES, { executed: true, status: 'succeeded', details: { mode: 'partial_regen', changedShots: changed } });
|
|
1327
1331
|
await finishStageAnalytics(JobStep.SEGMENTS, { executed: true, status: 'succeeded', details: { mode: 'partial_regen' } });
|
|
1328
1332
|
currentJob = getJob(jobId);
|
|
1333
|
+
|
|
1334
|
+
if (shouldPause) {
|
|
1335
|
+
const paused = updateJob(jobId, { status: JobStatus.PAUSED });
|
|
1336
|
+
await persistCheckpoint(paused, null, deps);
|
|
1337
|
+
return paused;
|
|
1338
|
+
}
|
|
1329
1339
|
}
|
|
1330
1340
|
|
|
1331
1341
|
if (!currentJob.steps[JobStep.KEYFRAMES]) {
|
|
@@ -1387,6 +1397,12 @@ export async function runPipeline(jobId, options = {}) {
|
|
|
1387
1397
|
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1388
1398
|
await finishStageAnalytics(JobStep.KEYFRAMES, { executed: true, status: 'succeeded' });
|
|
1389
1399
|
currentJob = getJob(jobId);
|
|
1400
|
+
|
|
1401
|
+
if (shouldPause) {
|
|
1402
|
+
const paused = updateJob(jobId, { status: JobStatus.PAUSED });
|
|
1403
|
+
await persistCheckpoint(paused, null, deps);
|
|
1404
|
+
return paused;
|
|
1405
|
+
}
|
|
1390
1406
|
} else if (
|
|
1391
1407
|
currentJob.artifacts.keyframeUrls.length > 0 &&
|
|
1392
1408
|
(!currentJob.artifacts.keyframePaths || currentJob.artifacts.keyframePaths.length === 0)
|