Files
olivv-cs-ComfyUI-FunPack/easy_gen/frontend/app.js
T
digital-garbage 99c7dd5ba6 Add project Export button to Easy Gen
Easy Gen's "Save" persists into FunPack's own project store on that
machine, same as the Editor's autosave — it was never a way to move a
project between machines. The backend route for that (download the actual
.funpack_project.json, re-importable via "Load Project File...") already
existed and easy_gen/frontend/api.js already had the URL helper, but no
button anywhere called it, unlike the Editor's File > "Save Project File...".
Added an "Export..." button next to Save that flushes a save first, then
downloads the project file.
2026-07-24 11:29:58 +03:00

200 lines
7.5 KiB
JavaScript

// Easy Gen entrypoint: wires the big-preview / prompt / upload / generate bar
// to the shared easy_store.js + preview.js, and opens the "FunPack Easy Gen"
// project picker on boot. No re-attach-on-reload support yet (the Editor's
// /api/active flow) — a reload while a run is in flight loses the progress
// view, though the run itself still completes server-side.
(function () {
const S = window.Store;
const API = window.MovieEditorAPI;
const LAST_PROJECT_KEY = "funpack_easy_last_project";
const nameEl = document.getElementById("easy-project-name");
const healthEl = document.getElementById("easy-health-chip");
const promptEl = document.getElementById("easy-prompt");
const uploadBtn = document.getElementById("easy-upload-btn");
const uploadInput = document.getElementById("easy-upload-input");
const galleryBtn = document.getElementById("easy-gallery-btn");
const generateBtn = document.getElementById("easy-generate-btn");
const advancedBtn = document.getElementById("easy-advanced-btn");
const projectsBtn = document.getElementById("easy-projects-btn");
const saveBtn = document.getElementById("easy-save-btn");
const exportBtn = document.getElementById("easy-export-btn");
const preview = window.EasyPreview.create(document.getElementById("easy-preview"));
let saveTimer = null;
function scheduleSave() {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => { S.save().catch(() => {}); }, 800);
}
let promptFocused = false;
promptEl.addEventListener("focus", () => { promptFocused = true; });
promptEl.addEventListener("blur", () => { promptFocused = false; });
promptEl.addEventListener("input", () => {
S.scheduleGlobalPromptApply(promptEl.value);
});
// Self-contained: reads Store.getEditorSetting("autocomplete")/get().shortcuts
// and no-ops until a project + shortcut library are loaded.
window.ShortcutAutocomplete?.attach(promptEl);
function removeUpload() {
S.setSceneMedia(null, null);
scheduleSave();
}
preview.setOnClearUpload(removeUpload);
let lastProjectId = null;
function render(st) {
const p = st.project;
nameEl.textContent = p ? p.name : "No project";
generateBtn.disabled = !p;
uploadBtn.disabled = !p;
galleryBtn.disabled = !p;
saveBtn.disabled = !p;
exportBtn.disabled = !p;
if (p && !promptFocused) {
const text = p.global_prompt || "";
if (promptEl.value !== text) promptEl.value = text;
}
const source = p?.scenes[0]?.source;
const attached = source && source.type !== "empty" && source.media_ref;
uploadBtn.textContent = attached ? "⬆ Uploaded ✕" : "⬆ Upload";
preview.setUpload(attached ? API.mediaUrl(source.media_ref) : null, source?.type === "v2v" ? "video" : "image");
const pid = p ? p.id : null;
if (pid !== lastProjectId) {
lastProjectId = pid;
preview.stopPolling();
preview.showEmpty();
generateBtn.textContent = "Generate";
}
}
S.subscribe(render);
uploadBtn.onclick = () => {
const st = S.get();
const attached = st.project && st.project.scenes[0]?.source?.type !== "empty";
if (attached) {
removeUpload();
return;
}
uploadInput.click();
};
uploadInput.onchange = async () => {
const file = uploadInput.files?.[0];
uploadInput.value = "";
if (!file) return;
uploadBtn.disabled = true;
uploadBtn.textContent = "Uploading…";
try {
const res = await API.uploadMedia(file);
const media = res.media?.[0]; // POST /media wraps the result: { media: [ {id, ...} ] }
if (!media) throw new Error("Upload succeeded but returned no media entry.");
const kind = file.type.startsWith("video") ? "video" : "image";
S.setSceneMedia(media.id, kind);
await S.save();
S.loadMedia();
} catch (e) {
alert("Upload failed: " + (e.message || e));
} finally {
render(S.get());
}
};
// Easy Gen has no rating UI, so Studio must never apply learned/rated steering here —
// forced at the graph level (not just hidden in Engine settings) so a project edited in
// the Cutting Room with these on still generates plainly when run from Easy Gen.
// "studio"/"sampler" are the built-in pipeline's fixed graph keys (see backend/builder.py).
const EASY_MODE_OVERRIDES = [
{ node: "studio", input: "mode", value: "Prompt only" },
{ node: "sampler", input: "embed_guidance", value: false },
{ node: "sampler", input: "score_slider", value: false },
{ node: "sampler", input: "output_guidance", value: false },
{ node: "sampler", input: "dynashift", value: false },
{ node: "sampler", input: "taste_nearest_prompt", value: false },
];
generateBtn.onclick = async () => {
const st = S.get();
if (!st.project) return;
if (!(st.project.global_prompt || "").trim()) {
alert("Type a prompt first.");
return;
}
generateBtn.disabled = true;
generateBtn.textContent = "Generating…";
try {
await S.save();
const resetSession = S.takeResetSessionFlag();
const res = await API.generate(st.project.id, null, null, resetSession, EASY_MODE_OVERRIDES);
preview.watch(st.project.id, res.prompt_id, {
onDone: () => { generateBtn.disabled = false; generateBtn.textContent = "Generate"; },
});
} catch (e) {
preview.showError("Generate failed: " + (e.message || e));
generateBtn.disabled = false;
generateBtn.textContent = "Generate";
}
};
advancedBtn.onclick = () => window.SettingsWindow.open("generation");
projectsBtn.onclick = () => window.ProjectMenu.open({ dismissable: !!S.get().project });
galleryBtn.onclick = () => window.EasyGallery.open();
// "Save" persists into FunPack's own project store on THIS machine — the same mechanism
// as the Editor's autosave. It doesn't help moving a project off a rental GPU box. This
// downloads the actual .funpack_project.json file (re-importable via "Load Project
// File…" on another machine), same as the Editor's File ▸ "Save Project File…".
exportBtn.onclick = async () => {
const st = S.get();
if (!st.project) return;
try { await S.save(); } catch (_) { /* export the last-saved version anyway */ }
const a = document.createElement("a");
a.href = API.downloadProjectUrl(st.project.id);
a.download = "";
document.body.appendChild(a); a.click(); document.body.removeChild(a);
};
let saveResetTimer = null;
saveBtn.onclick = async () => {
if (saveResetTimer) clearTimeout(saveResetTimer);
saveBtn.disabled = true;
saveBtn.textContent = "Saving…";
try {
await S.save();
saveBtn.textContent = "Saved ✓";
} catch (e) {
saveBtn.textContent = "Save failed";
alert("Save failed: " + (e.message || e));
} finally {
saveResetTimer = setTimeout(() => {
saveBtn.textContent = "Save";
saveBtn.disabled = !S.get().project;
}, 1200);
}
};
async function boot() {
S.refreshHealth();
S.loadLibraries();
S.loadMedia();
setInterval(() => S.refreshHealth(), 15000);
S.subscribe((st) => {
healthEl.textContent = st.health?.ok ? "● online" : "● offline";
healthEl.classList.toggle("bad", !(st.health && st.health.ok));
if (st.project) localStorage.setItem(LAST_PROJECT_KEY, st.project.id);
});
const lastId = localStorage.getItem(LAST_PROJECT_KEY);
if (lastId) {
try { await S.loadProject(lastId); return; } catch (_) { /* fall through to picker */ }
}
window.ProjectMenu.open({ dismissable: false });
}
boot();
})();