// app.jsx — AutoCut shell: header, screen switcher, Tweaks panel. const TXT_APP = { vi: { tweaks: "Tinh chỉnh", appearance: "Giao diện", theme: "Chủ đề", accent: "Màu chủ đạo", demo: "Demo", replayPipeline: "Chạy lại tiến trình", skipToOutput: "Xem kết quả", resetToInput: "Quay lại soạn thảo", project: "Dự án", compose: "Soạn thảo", generate: "Khởi tạo", deliver: "Hoàn tất", history: "Lịch sử", templates: "Mẫu", light: "Sáng", dark: "Tối", logout: "Đăng xuất", apiError: "Không gọi được /api/generate: ", }, en: { tweaks: "Tweaks", appearance: "Appearance", theme: "Theme", accent: "Accent", demo: "Demo", replayPipeline: "Replay pipeline", skipToOutput: "Skip to output", resetToInput: "Reset to input", project: "Project", compose: "Compose", generate: "Generate", deliver: "Deliver", history: "History", templates: "Templates", light: "Light", dark: "Dark", logout: "Log out", apiError: "Failed to call /api/generate: ", } }; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "dark", "accent": "#FFB547" }/*EDITMODE-END*/; const ACCENT_OPTIONS = [ "#FFB547", // amber (default) "#19E5BE", // cyan / mint "#FF6584", // magenta "#A8E060", // lime "#7B8BFF", // violet ]; // Convert FE hex "#RRGGBB" or "#RRGGBBAA" → ASS color "&HAABBGGRR". // ASS alpha is inverted: 00 = opaque, FF = transparent. function hexToAssColor(hex) { if (!hex || typeof hex !== "string") return null; let h = hex.replace("#", ""); if (h.length === 6) h += "FF"; if (h.length !== 8) return null; const r = h.slice(0, 2).toUpperCase(); const g = h.slice(2, 4).toUpperCase(); const b = h.slice(4, 6).toUpperCase(); const aHex = parseInt(h.slice(6, 8), 16); const aAss = (255 - aHex).toString(16).padStart(2, "0").toUpperCase(); return `&H${aAss}${b}${g}${r}`; } // map hex → oklch components so we can recompute soft/line variants function hexToOklchVars(hex) { // simple approach: just use rgba with opacity for soft/line const r = parseInt(hex.slice(1,3), 16); const g = parseInt(hex.slice(3,5), 16); const b = parseInt(hex.slice(5,7), 16); // perceived lightness for contrasted fg const luminance = (0.299*r + 0.587*g + 0.114*b) / 255; return { accent: hex, soft: `rgba(${r},${g},${b},0.14)`, line: `rgba(${r},${g},${b},0.32)`, fg: luminance > 0.6 ? "#1a1408" : "#fff8ee", }; } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [authed, setAuthed] = React.useState(() => isAuthed()); const [appMode, setAppMode] = React.useState("studio"); // studio (script→video) | enhance (video→video) const [screen, setScreen] = React.useState("input"); // input | pipeline | output const [job, setJob] = React.useState({ id: null, videoUrl: null, error: null }); const initialUiLang = localStorage.getItem("autocut.uiLang") || "vi"; const [uiLang, setUiLang] = React.useState(initialUiLang); const appTxt = TXT_APP[uiLang] || TXT_APP.vi; const [state, setState] = React.useState(() => ({ source: initialUiLang === "vi" ? SAMPLE_SCRIPT_VI : SAMPLE_SCRIPT, customPrompt: "", customStyle: "", sourceMode: "script", uploads: [], // [{file: File, description: string, previewUrl: string}] settings: { language: initialUiLang === "vi" ? "Vietnamese" : "English", style: "presentation", aspect: "16:9", resolution: "1080", voice: "narrator-warm", pacing: 1.0, enableSubtitles: false, numScenes: 0, templates: null, // null → backend uses the style's default selection; SettingsPane fills from /api/styles templatesOnly: true, // strict-mode: every scene MUST use a template (Agent 1 cannot set template=null) }, sub: { font: "Arial", size: 36, maxWords: 6, position: "bottom", marginV: 120, text: "#FFFFFFFF", outline: "#000000FF", back: "#000000A0", highlight: "#FFD60AFF", name: "Studio", }, })); const changeUiLang = React.useCallback((lang) => { setUiLang(lang); localStorage.setItem("autocut.uiLang", lang); setState((prev) => { const isSourceDefaultSample = prev.source === SAMPLE_SCRIPT || prev.source === SAMPLE_SCRIPT_VI; const nextSource = isSourceDefaultSample ? (lang === "vi" ? SAMPLE_SCRIPT_VI : SAMPLE_SCRIPT) : prev.source; return { ...prev, source: nextSource, settings: { ...prev.settings, language: lang === "vi" ? "Vietnamese" : "English", }, }; }); }, []); // Remix → Studio handoff: drop the rewritten script into the input screen. const handleSendToStudio = React.useCallback((script, langB) => { setState((prev) => ({ ...prev, source: script || prev.source, settings: { ...prev.settings, language: langB || prev.settings.language }, })); setAppMode("studio"); setScreen("input"); }, []); const startJob = React.useCallback(async () => { setJob({ id: null, videoUrl: null, error: null }); try { // Stage 1: if user uploaded any local assets, send them first and get an upload_id. let uploadId = null; const uploads = state.uploads || []; if (uploads.length > 0) { const fd = new FormData(); uploads.forEach((u) => fd.append("files", u.file)); fd.append("descriptions", JSON.stringify(uploads.map((u) => u.description || ""))); const upRes = await fetch("/api/uploads", { method: "POST", body: fd }); if (!upRes.ok) { const errBody = await upRes.text(); throw new Error(`/api/uploads ${upRes.status}: ${errBody}`); } const upData = await upRes.json(); uploadId = upData.upload_id; } // Stage 2: kick off generation with the upload_id (if any). const res = await fetch("/api/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: state.source, custom_prompt: state.customPrompt || "", style: state.settings.style, custom_style: (state.settings.style === "custom") ? (state.customStyle || "") : "", language: state.settings.language, aspect_ratio: state.settings.aspect, enable_subtitles: state.settings.enableSubtitles !== false, num_scenes: state.settings.numScenes > 0 ? state.settings.numScenes : null, // null → user never touched selection → backend uses style defaults // [] → user explicitly unchecked everything → no templates (fully custom Agent 2) // [...] → user's explicit subset templates: state.settings.templates !== null ? state.settings.templates : null, templates_only: state.settings.templatesOnly !== false, upload_id: uploadId, }), }); if (!res.ok) throw new Error(`API ${res.status}`); const data = await res.json(); setJob({ id: data.job_id, videoUrl: null, error: null }); setScreen("pipeline"); } catch (e) { setJob({ id: null, videoUrl: null, error: String(e.message || e) }); alert(appTxt.apiError + (e.message || e)); } }, [state.source, state.customPrompt, state.customStyle, state.settings, state.uploads, appTxt.apiError]); // apply theme + accent globally React.useEffect(() => { document.documentElement.dataset.theme = t.theme || "dark"; const v = hexToOklchVars(t.accent); document.documentElement.style.setProperty("--accent", v.accent); document.documentElement.style.setProperty("--accent-soft", v.soft); document.documentElement.style.setProperty("--accent-line", v.line); document.documentElement.style.setProperty("--accent-fg", v.fg); }, [t.theme, t.accent]); // keyboard: cmd+enter to generate from input screen React.useEffect(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && screen === "input") { if (state.source.trim().split(/\s+/).length >= 20) startJob(); } }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [screen, state.source, startJob]); if (!authed) { return setAuthed(true)} />; } return (
= 20} appMode={appMode} onAppModeChange={setAppMode} theme={t.theme} onToggleTheme={() => { const isDark = t.theme === "dark"; setTweak({ theme: isDark ? "light" : "dark", accent: isDark ? "#60A5FA" : "#FFB547" }); }} uiLang={uiLang} onUiLangChange={changeUiLang} />
{appMode === "enhance" && } {appMode === "remix" && } {appMode === "studio" && screen === "input" && ( )} {appMode === "studio" && screen === "pipeline" && ( { setJob((j) => ({ ...j, videoUrl })); setScreen("output"); }} onError={(err) => setJob((j) => ({ ...j, error: err }))} uiLang={uiLang} /> )} {appMode === "studio" && screen === "output" && ( setScreen("input")} onUpdateJob={(j) => setJob(j)} uiLang={uiLang}/> )}
setTweak("theme", v)}/> setTweak("accent", v)}/> setScreen("pipeline")}/> setScreen("output")}/> setScreen("input")}/> { localStorage.removeItem("autocut.authed"); setAuthed(false); }}/>
); } function TopBar({ screen, onScreenChange, canPipeline, appMode, onAppModeChange, theme, onToggleTheme, uiLang, onUiLangChange }) { const appTxt = TXT_APP[uiLang] || TXT_APP.vi; const modeLabels = uiLang === "en" ? { studio: "Studio", enhance: "Enhance", remix: "Remix" } : { studio: "Studio", enhance: "Làm đẹp", remix: "Viết lại" }; const steps = uiLang === "en" ? [ { id: "input", label: "Compose" }, { id: "pipeline", label: "Generate" }, { id: "output", label: "Deliver" }, ] : [ { id: "input", label: "Soạn thảo" }, { id: "pipeline", label: "Khởi tạo" }, { id: "output", label: "Hoàn tất" }, ]; const activeIdx = steps.findIndex(s => s.id === screen); return (
{appTxt.project} · voyager_doc
{/* mode toggle: Studio (script→video) vs Enhance (video→video) */}
{["studio", "enhance", "remix"].map((m) => ( ))}
{/* center stepper — only for studio (script→video) flow */}
{/* Language Switcher Toggle */}
3.2 / 5 hr
K
); } ReactDOM.createRoot(document.getElementById("root")).render();