diff --git a/app/ui/features/audio/tracks.js b/app/ui/features/audio/tracks.js index 7986621f..9aa89cc1 100644 --- a/app/ui/features/audio/tracks.js +++ b/app/ui/features/audio/tracks.js @@ -24,7 +24,7 @@ export default { let volume = t.volume != null ? t.volume : 1, start = t.start_sec || 0; const body = c.region.stack({ gap: "sm", children: [ c.hint.default({ text: separated ? "This clip's own sound. It follows its clip; slide or trim it on the timeline." : "A sound over the cut. Slide and trim it on the timeline." }), - c.field.default({ label: "Volume", control: c.slider.readout({ label: "Volume", value: volume, min: 0, max: 2, step: 0.05, precision: 2, onCommit: (v) => { volume = v; } }) }), + c.field.default({ label: "Volume", hint: "1 = as recorded. Above 1 makes the rendered sound louder; the monitor plays at most 1.", control: c.slider.readout({ label: "Volume", value: volume, min: 0, max: 2, step: 0.05, precision: 2, onCommit: (v) => { volume = v; } }) }), separated ? null : c.field.default({ label: "Starts at (s)", control: c.number.md({ label: "Starts at", value: start, min: 0, step: 0.1, onChange: (v) => { start = v; } }) }), ].filter(Boolean) }); const win = c.modal.generic({ title: t.label || "Audio track", size: "sm", body }); diff --git a/app/ui/features/composer/enhance.js b/app/ui/features/composer/enhance.js index a0be6c40..6bec88d4 100644 --- a/app/ui/features/composer/enhance.js +++ b/app/ui/features/composer/enhance.js @@ -3,7 +3,9 @@ import { composer as c } from "../../composer/composer.js"; const NODE = "FunPackEnhancePrompt"; -const DIALS = [["temperature", 0.7, 0, 2, 0.05], ["top_p", 0.92, 0, 1, 0.01], ["top_k", 50, 1, 1000, 1], ["min_p", 0.05, 0, 1, 0.01], ["repetition_penalty", 1.3, 1, 2, 0.05], ["presence_penalty", 0, 0, 2, 0.05]]; +const DIALS = [["temperature", 0.7, 0, 2, 0.05, "Higher gives more varied wording, lower more predictable."], ["top_p", 0.92, 0, 1, 0.01, "Picks only among the likeliest words whose chances add up to this share."], + ["top_k", 50, 1, 1000, 1, "Picks only among this many likeliest words."], ["min_p", 0.05, 0, 1, 0.01, "Drops words much less likely than the likeliest one; higher drops more."], + ["repetition_penalty", 1.3, 1, 2, 0.05, "Above 1 discourages repeating words."], ["presence_penalty", 0, 0, 2, 0.05, "Above 0 discourages words already used."]]; export function enhance(app, own) { const ps = app.pipeline, page = c.region.stack({ gap: "sm" }); @@ -12,7 +14,7 @@ export function enhance(app, own) { const slot = () => (ps.slots() || []).find((s) => s.node === NODE) || null; const set = async (name, value) => { const sl = slot(); if (sl) { await ps.save({ inputs: { [sl.id]: { [name]: value } } }); draw(); } }; const field = (label, control, hint) => c.field.default({ label, control, hint }); - const num = (label, v, lo, hi, step, name) => field(label, c.number.md({ label, value: v, min: lo, max: hi, step, onChange: (x) => set(name, x) })); + const num = (label, v, lo, hi, step, name, hint) => field(label, c.number.md({ label, value: v, min: lo, max: hi, step, onChange: (x) => set(name, x) }), hint); function readout() { if (!last) return [c.label.section({ text: "Last run" }), c.hint.default({ text: "Nothing yet. The rewrite of a run shows here." })]; @@ -32,10 +34,10 @@ export function enhance(app, own) { c.toggle.default({ label: "Enhance prompt", checked: on, onChange: (x) => set("enabled", x) }), ...(!on ? readout() : [ field("Instructions", c.textarea.md({ label: "Instructions", rows: 6, value: val("instructions", ""), placeholder: defaults ? defaults.instructions : "(built-in instructions)", onCommit: (x) => set("instructions", x) }), "Empty uses the built-in instructions."), - num("Max tokens", val("max_length", 400), 32, 4096, 8, "max_length"), + num("Max tokens", val("max_length", 400), 32, 4096, 8, "max_length", "The longest the rewrite may be."), c.toggle.default({ label: "Use the picture", hint: "Sends the scene's picture to the model along with the prompt.", checked: val("use_image", true), onChange: (x) => set("use_image", x) }), c.toggle.default({ label: "Thinking", hint: "Let a thinking model reason first. Its reasoning eats the token budget.", checked: val("thinking", false), onChange: (x) => set("thinking", x) }), - c.toggle.default({ label: "Greedy (always the likeliest word)", checked: greedy, onChange: (x) => set("greedy", x) }), + c.toggle.default({ label: "Greedy (always the likeliest word)", hint: "The same rewrite every time; the dials below are not used.", checked: greedy, onChange: (x) => set("greedy", x) }), ...(greedy ? [] : DIALS.map(([n, d, lo, hi, st]) => num(n, val(n, d), lo, hi, st, n))), num("Seed", val("seed", 0), 0, 2 ** 53, 1, "seed"), c.hint.default({ text: "Seed 0 = a fresh rewrite every Generate; any other number repeats the same rewrite." }), diff --git a/app/ui/features/generate/index.js b/app/ui/features/generate/index.js index bebb82ee..5b5b789f 100644 --- a/app/ui/features/generate/index.js +++ b/app/ui/features/generate/index.js @@ -150,10 +150,10 @@ export default { if (key === drawn) return; // progress ticks must not rebuild the buttons drawn = key; const working = busy || held || g.state().phase === QUEUED || g.state().phase === RUNNING; // also a run this page did not start - const go = c.button.sm({ label: working ? "Generating…" : "▶ Generate", tone: "primary", disabled: working || !open, + const go = c.button.sm({ label: working ? "Generating…" : "▶ Generate", tone: "primary", disabled: working || !open, title: "Generate every scene on the timeline, except left-out scenes and video clips. An earlier render of a scene stays as a take.", onClick: () => runUnits(unitsOf(p.scenes)) }); - const one = c.button.sm({ label: picked > 1 ? `Selected (${picked})` : "Selected", disabled: working || !picked, onClick: () => runUnits(unitsOf(pickedScenes())) }); - const stop = c.button.sm({ label: "■ Stop", tone: "danger", onClick: () => { stopped = true; g.cancel(); } }); + const one = c.button.sm({ label: picked > 1 ? `Selected (${picked})` : "Selected", title: "Generate only the picked scenes", disabled: working || !picked, onClick: () => runUnits(unitsOf(pickedScenes())) }); + const stop = c.button.sm({ label: "■ Stop", tone: "danger", title: "Stop the run that is going", onClick: () => { stopped = true; g.cancel(); } }); stop.node.hidden = !working; put(go.node, one.node, stop.node); } diff --git a/app/ui/features/ideas/index.js b/app/ui/features/ideas/index.js index bea41137..d779f00c 100644 --- a/app/ui/features/ideas/index.js +++ b/app/ui/features/ideas/index.js @@ -87,7 +87,7 @@ export default { window.addEventListener("scroll", onMove, true); window.addEventListener("resize", onMove); app.editorSettings.push(() => c.region.stack({ gap: "sm", children: [ - c.toggle.default({ label: "Shortcut ideas", hint: "A 💡 beside prompt boxes lights up when shortcut categories in your library aren't used in the prompt. Click it for insertable ideas; it never pops up on its own.", + c.toggle.default({ label: "Shortcut ideas", hint: "Shows a 💡 beside the Story and Prompt boxes while you are in them. Click it for shortcuts to insert: pairings you use, what usually follows the previous shot, and categories this prompt does not use yet.", checked: on(), onChange: (v) => { p.setPref("suggestions", v); if (!v) closeBulb(); } })] })); return () => { closeBulb(); clearTimeout(hold); document.removeEventListener("focusin", onFocus); document.removeEventListener("focusout", onBlur); window.removeEventListener("scroll", onMove, true); window.removeEventListener("resize", onMove); }; }, diff --git a/app/ui/features/imports/index.js b/app/ui/features/imports/index.js index 5941e3e1..774b9622 100644 --- a/app/ui/features/imports/index.js +++ b/app/ui/features/imports/index.js @@ -1,4 +1,4 @@ -// "+ Import" on the timeline toolbar: a video from the media bin as a clip of its own, or a sound as an audio track at the playhead. +// "+ From bin" on the timeline toolbar: a video from the media bin as a clip of its own, or a sound as an audio track at the playhead. import { composer as c } from "../../composer/composer.js"; import { addVideoClip, addAudioTrack } from "../../shell/imports.js"; import { learn } from "../../shell/bin.js"; @@ -42,7 +42,7 @@ export default { if (seconds == null) c.toast.warn({ text: asset.kind === "video" ? "The file's length could not be read; the clip uses the project's length." : "The file's length could not be read: the lane cannot be trimmed, and plays to the end of the file." }); } - const menu = c.button.menu({ label: "+ Import", tone: "ghost", onClick: () => c.menu.dropdown({ anchor: menu, items: [{ id: "video", label: "Video clip…", disabled: !p.project }, { id: "audio", label: "Audio track…", disabled: !p.project }], + const menu = c.button.menu({ label: "+ From bin", tone: "ghost", onClick: () => c.menu.dropdown({ anchor: menu, items: [{ id: "video", label: "Video clip…", disabled: !p.project }, { id: "audio", label: "Audio track…", disabled: !p.project }], onPick: (id) => choose(id) }) }); host.append(menu.node); diff --git a/app/ui/features/inspector/project.js b/app/ui/features/inspector/project.js index 56f359d9..20c5835d 100644 --- a/app/ui/features/inspector/project.js +++ b/app/ui/features/inspector/project.js @@ -70,12 +70,12 @@ export function projectRows(p, slots, api) { c.hint.default({ text: `≈ ${per.toFixed(2)} s per shot` }), size.length ? c.field.row({ fields: size.map((x) => x.field) }) : null, ...custom(p, open), - c.field.default({ label: "Final render size", control: c.select.md({ label: "Final render size", options: sizes(open), value: open.export_size_from || "", + c.field.default({ label: "Final render size", hint: "The size of the exported video: the first clip's, the project's, or any rendered clip's own.", control: c.select.md({ label: "Final render size", options: sizes(open), value: open.export_size_from || "", onChange: (v) => p.setField("export_size_from", v) }) }), - c.field.default({ label: "Start shots", control: c.select.md({ label: "Start shots", options: STARTS, value: open.generation_mode === "t2v" ? "t2v" : "i2v", onChange: (v) => p.setField("generation_mode", v) }) })), + c.field.default({ label: "Start shots", hint: "From an image: a scene starts from its picture, or the last frame of the clip before it. From a prompt: every scene starts without a picture.", control: c.select.md({ label: "Start shots", options: STARTS, value: open.generation_mode === "t2v" ? "t2v" : "i2v", onChange: (v) => p.setField("generation_mode", v) }) })), c.label.section({ text: "Prompt" }), - card(c.field.default({ label: "Anchor", control: c.textarea.md({ label: "Anchor", value: p.anchor, rows: 2, onInput: (v) => p.setAnchor(v) }) }), - c.field.default({ label: "Negative prompt", control: c.textarea.md({ label: "Negative prompt", value: p.negative, rows: 2, onInput: (v) => p.setNegative(v) }) }), + card(c.field.default({ label: "Anchor", hint: "Put in front of every scene's prompt.", control: c.textarea.md({ label: "Anchor", value: p.anchor, rows: 2, onInput: (v) => p.setAnchor(v) }) }), + c.field.default({ label: "Negative prompt", hint: "What to steer away from. A model that ignores negative prompts (MiniMax H3) uses it only with Shadow negative or Negative erase on.", control: c.textarea.md({ label: "Negative prompt", value: p.negative, rows: 2, onInput: (v) => p.setNegative(v) }) }), c.toggle.default({ label: "Postfix", hint: "Appended to every scene (a style or quality tag, say).", checked: open.postfix_enabled !== false, onChange: (v) => p.setField("postfix_enabled", v) }), c.textarea.md({ label: "Postfix", value: p.postfix, rows: 2, disabled: open.postfix_enabled === false, onInput: (v) => p.setPostfix(v) })), c.collapsible.default({ label: "ADVANCED PROJECT SETTINGS", body: c.region.stack({ gap: "sm", children: [ diff --git a/app/ui/features/inspector/scene.js b/app/ui/features/inspector/scene.js index 7c8d9e46..177f60fb 100644 --- a/app/ui/features/inspector/scene.js +++ b/app/ui/features/inspector/scene.js @@ -52,15 +52,15 @@ export function sceneRows(p, app) { if (!sc) return [c.emptyState.default({ icon: "▭", title: "No scene", hint: "Add one on the timeline." })]; const cuts = p.scenes.filter((s) => genUnitId(s) === genUnitId(sc)).length > 1; return [ - c.banner.info({ text: "Generated by this project's pipeline", action: { label: "Engine settings →", onClick: () => app.openSettings && app.openSettings("engine") } }), + c.banner.info({ text: "Made with the project's pipeline and Engine settings", action: { label: "Engine settings →", onClick: () => app.openSettings && app.openSettings("engine") } }), ...(sc.removed_from_plan ? [c.banner.info({ text: "Removed from plan — its generated clip stays on the timeline.", action: { label: "Restore to plan", onClick: () => p.edit((pr) => restoreToPlan(pr, sc.id)) } })] : []), ...(cuts ? [c.hint.default({ text: "This scene has editorial cuts — Generate regens the whole uncut scene." })] : []), ...(isSubclip(sc) ? [c.hint.default({ text: "This is a cut: its prompt and source belong to the first part." })] : [card( c.field.default({ label: "Prompt", control: c.textarea.md({ label: "Prompt", value: sc.text || "", rows: 4, onInput: (v) => p.setText(sc.id, v) }) }), c.field.default({ label: "Source", control: c.select.md({ label: "Source", options: SOURCES, value: (sc.source || {}).type || "carry", onChange: (v) => p.setScene(sc.id, "source", { ...(sc.source || {}), type: v }) }) }))]), - c.label.section({ text: "Reference media (v5 pipeline)" }), - card(mediaField(app, p, sc, { label: "Aspect ratio source", hint: "Sets this scene's aspect ratio for generation — the project's own Width/Height set the actual resolution; this image's pixels are not used.", + c.label.section({ text: "Pictures" }), + card(mediaField(app, p, sc, { label: "Aspect ratio source", hint: "The scene's picture. Its shape sets this scene's aspect ratio (the project's Width/Height set the scale). A model that starts from a picture, such as LTX image-to-video, also begins the clip from it; the prompt enhancer can read it.", key: "source_image", multiple: false, kinds: ["image"], empty: "— choose aspect ratio source —" }), mediaField(app, p, sc, { label: "References", key: "references", multiple: true, kinds: ["image", "video", "audio"], empty: "+ Add reference" })), c.label.section({ text: "Length" }), diff --git a/app/ui/features/menus/index.js b/app/ui/features/menus/index.js index 18f5514e..18ca3196 100644 --- a/app/ui/features/menus/index.js +++ b/app/ui/features/menus/index.js @@ -66,14 +66,14 @@ export default { { id: "delete", label: "Delete Current Project", danger: true, disabled: !p.project }]; }, Edit: () => [{ id: "undo", label: "Undo", hint: "⌘Z", disabled: !p.canUndo }, { id: "redo", label: "Redo", hint: "⇧⌘Z", disabled: !p.canRedo }, { separator: true }, - { id: "add", label: "Add Scene", disabled: !p.project }, { id: "del", label: "Delete Scene", disabled: !sel.ids.length }, { separator: true }, - { id: "left", label: "Move Clip Left", hint: "timeline cut", disabled: !sel.focus }, { id: "right", label: "Move Clip Right", hint: "timeline cut", disabled: !sel.focus }, { separator: true }, + { id: "add", label: "Add Scene", disabled: !p.project }, { id: "del", label: sel.ids.length > 1 ? `Delete ${sel.ids.length} Scenes` : "Delete Scene", disabled: !sel.ids.length }, { separator: true }, + { id: "left", label: "Move Clip Left", hint: "earlier", disabled: !sel.focus }, { id: "right", label: "Move Clip Right", hint: "later", disabled: !sel.focus }, { separator: true }, { id: "exclude", label: "Toggle Exclude", disabled: !sel.focus }], View: () => [{ id: "refresh", label: "Refresh Preview" }, { id: "reset", label: "Reset Layout" }], Help: () => { const running = Boolean(app.tourOn?.()); return [...(app.has.has("tour") ? [{ id: "tour", label: running ? "Restart tour" : "Start tour" }, { id: "faq", label: "Skip to FAQ" }, - ...(running ? [{ id: "exit", label: "Exit tour" }] : []), { separator: true }] : []), { id: "welcome", label: "Welcome tour…" }]; + ...(running ? [{ id: "exit", label: "Exit tour" }] : []), { separator: true }] : []), { id: "welcome", label: "Welcome window…" }]; }, }; const off = app.on((what) => { if (what === "project.load") actions.load(); }); diff --git a/app/ui/features/montage/index.js b/app/ui/features/montage/index.js index 8b984811..6fa5c788 100644 --- a/app/ui/features/montage/index.js +++ b/app/ui/features/montage/index.js @@ -28,8 +28,8 @@ export default { c.field.default({ label: "Lead clip (plays in order)", control: c.select.md({ label: "Lead clip", value: lead, options: list.map((s) => ({ value: s.id, label: label(s) })), onChange: (v) => { lead = v; pool.delete(v); draw(); } }) }), c.field.default({ label: "Cutaway pool (random draw, in order within each clip)", control: c.checklist.default({ label: "Cutaway pool", values: [...pool], items: list.filter((s) => s.id !== lead).map((s) => ({ value: s.id, label: label(s) })), onChange: (v) => { pool.clear(); v.forEach((x) => pool.add(x)); } }) }), - c.field.default({ label: "Segment length (frames)", control: c.number.md({ label: "Segment length", value: seg, min: 9, step: 1, precision: 0, onChange: (v) => { seg = v; } }) }), - c.field.default({ label: "Acceleration", hint: "1.0 = constant, lower = faster cuts toward the end", control: c.slider.readout({ label: "Acceleration", value: decay, min: 0.5, max: 1, step: 0.05, onCommit: (v) => { decay = v; } }) }), + c.field.default({ label: "Segment length (frames)", hint: "How long the lead clip's first piece and each cutaway piece are.", control: c.number.md({ label: "Segment length", value: seg, min: 9, step: 1, precision: 0, onChange: (v) => { seg = v; } }) }), + c.field.default({ label: "Acceleration", hint: "1.0 = equal segments; lower = the lead clip's segments get shorter toward the end", control: c.slider.readout({ label: "Acceleration", value: decay, min: 0.5, max: 1, step: 0.05, onCommit: (v) => { decay = v; } }) }), ]); draw(); const win = c.modal.generic({ title: "Auto Montage", subtitle: "Cuts the lead clip into shrinking segments and randomly inserts cutaways between them.", size: "md", body }); diff --git a/app/ui/features/render/index.js b/app/ui/features/render/index.js index bfd3e0ca..f8dd58d1 100644 --- a/app/ui/features/render/index.js +++ b/app/ui/features/render/index.js @@ -29,7 +29,7 @@ export default { } catch (err) { tell(err.message); } finally { busy = false; draw(); } } function draw() { - const next = c.button.sm({ label: busy ? "Rendering…" : "⧉ Render", tone: "render", disabled: busy || !p.project, onClick: render }).node; + const next = c.button.sm({ label: busy ? "Rendering…" : "⧉ Render", tone: "render", title: "Join the generated clips into one video and save it in the media bin", disabled: busy || !p.project, onClick: render }).node; if (node) node.replaceWith(next); else host.append(next); node = next; } diff --git a/app/ui/features/settings/index.js b/app/ui/features/settings/index.js index 8e453bcf..ad55ee35 100644 --- a/app/ui/features/settings/index.js +++ b/app/ui/features/settings/index.js @@ -40,7 +40,7 @@ export default { }; app.pipeline.ensureLoaded().then(offerShot); app.pipeline.subscribe(offerShot); - add("system", "Updates & ComfyUI", "Server connection, FunPack code updates, pipeline health.", "⟳", "good", + add("system", "Updates & ComfyUI", "Update FunPack, switch its branch, roll back, or restart ComfyUI.", "⟳", "good", system(app), "System", "update git branch restart rollback"); add("customnodes", "Custom Nodes", "Install, update and remove ComfyUI node packs.", "⧉", "neutral", packs(app), "System", "custom nodes packs install"); diff --git a/app/ui/features/settings/taste.js b/app/ui/features/settings/taste.js index b27fb77c..efce0461 100644 --- a/app/ui/features/settings/taste.js +++ b/app/ui/features/settings/taste.js @@ -38,7 +38,7 @@ export const taste = (app) => function mount() { page.set([ error ? c.banner.warn({ text: `Could not read the keys: ${error}` }) : null, keys && !keys.length && !error ? c.emptyState.default({ icon: "✦", title: "No keys yet", hint: "A key appears once you rate a render with learning on." }) : null, - ...(keys || []).map((name) => c.settingsRow.default({ label: name, hint: "Refinement key", control: c.toolbar.default({ items: [ + ...(keys || []).map((name) => c.settingsRow.default({ label: name, hint: "Taste key: what your ratings taught under this name", control: c.toolbar.default({ items: [ c.button.sm({ label: "⤓ Export", tone: "ghost", onClick: () => exportKey(name) }), c.button.sm({ label: "Delete", tone: "danger", onClick: () => remove(name) })] }) })), c.label.section({ text: "Bring a key here" }), diff --git a/app/ui/features/takes/index.js b/app/ui/features/takes/index.js index 7fa768a1..ed480bf7 100644 --- a/app/ui/features/takes/index.js +++ b/app/ui/features/takes/index.js @@ -31,7 +31,7 @@ export default { const items = list.map((t, i) => ({ id: String(i), label: `Take ${i + 1}`, thumb: still(t), icon: "▶", badge: mark(i === at ? takesOf(pr, sc).head.rating : t.rating) || undefined })); return [c.label.section({ text: "Takes" }), c.gallery.adaptive({ id: "takes", items, selection: [String(at)], cols: 3, empty: "", onActivate: (it) => p.edit((x) => pickTake(x, x.scenes.find((s) => s.id === sc.id), Number(it.id))) }), - c.field.default({ label: `Take ${at + 1} of ${list.length}`, hint: "Step through the renders of this scene; the one showing is the one on the clip. Rate each, the best stays.", + c.field.default({ label: `Take ${at + 1} of ${list.length}`, hint: "Step through the renders of this scene; the one showing is the one on the clip. Each take keeps its own rating.", control: c.toolbar.default({ items: [c.button.sm({ label: "‹ Earlier", disabled: at === 0, onClick: go(-1) }), c.button.sm({ label: "Later ›", disabled: at === list.length - 1, onClick: go(1) })] }) })]; }, }; diff --git a/app/ui/features/timeline/index.js b/app/ui/features/timeline/index.js index 56ca1924..dc10fde0 100644 --- a/app/ui/features/timeline/index.js +++ b/app/ui/features/timeline/index.js @@ -80,7 +80,7 @@ const tools = { const open = p.project, ids = open ? sel.ids : []; put(c.hint.default({ text: `${clock(head.at)} / ${clock(open ? totalSeconds(open) : 0)}` }).node, btn("+ Add", () => { const sc = p.edit((pr) => edits.addScene(pr)); if (sc) p.select(sc.id); }, !open), - btn("Split", split, !ids.length, "Split the selected clip at the playhead"), + btn("Split", split, !ids.length, "Cut the selected clip at the playhead (in the middle when the playhead is elsewhere)"), btn("Remove", () => ids.forEach((id) => p.edit((pr) => edits.removeScene(pr, id))), !ids.length), c.chip.neutral({ label: ids.length ? `${ids.length} clip${ids.length > 1 ? "s" : ""} selected` : "no clip selected" }).node); }; @@ -97,7 +97,7 @@ const zoom = { needs: ["say"], setup({ host, app }) { const b = (label, what, title) => c.button.sm({ label, tone: "ghost", title, onClick: () => app.say(what) }).node; - const keys = c.hint.default({ text: "J K L · S split · I O in/out · + − zoom" }).node; + const keys = c.hint.default({ text: "J back 1 s · K pause · L play · S split · I / O clip start / end · + − zoom" }).node; keys.classList.add("fp-keyhint"); // the first thing to go when the row runs out of room host.append(keys, b("−", "zoom.out", "Zoom out"), b("+", "zoom.in", "Zoom in"), b("fit", "zoom.fit", "Fit the cut to the window")); diff --git a/app/ui/features/tour/index.js b/app/ui/features/tour/index.js index db5c6184..9243de30 100644 --- a/app/ui/features/tour/index.js +++ b/app/ui/features/tour/index.js @@ -1,5 +1,5 @@ // The guided tour: a walk along the screen, one callout per part, on whatever project is open (it changes nothing). -// Help ▸ Welcome tour / Restart tour start it, Exit tour ends it, Skip to FAQ jumps to the last callout. +// Help ▸ Start tour / Restart tour start it, Exit tour ends it, Skip to FAQ jumps to the last callout. import { composer as c } from "../../composer/composer.js"; const zone = (n) => () => document.querySelectorAll("section.cx-zone")[n]; @@ -11,7 +11,7 @@ const STEPS = [ { at: zone(2), side: "left", title: "Properties", text: "Project settings (size, length, postfix) and the picked scene's prompt, source, references and length." }, { at: zone(3), side: "top", title: "Timeline", text: "One clip per scene. Drag the edges to trim, press S to split, Space to play. Lanes below carry separated sound." }, { at: button("◆ Composer"), side: "top", title: "Composer", text: "Write the whole story in one box, cut at the cut word; keep templates, $variables and shortcuts; chat with the prompt enhancer." }, - { at: button("▶ Generate"), side: "top", title: "Generate", text: "Makes every scene that has no render yet. “Generate this scene” in Properties makes just the picked one; rate a result so FunPack learns your taste." }, + { at: button("▶ Generate"), side: "top", title: "Generate", text: "Makes every scene on the timeline, except left-out scenes and video clips; an earlier render of a scene stays as a take. “Generate this scene” in Properties makes just the picked one; rate a result so FunPack learns your taste." }, { at: () => document.querySelector(".cx-frame-main"), side: "bottom", title: "FAQ", text: "Nothing happens? Check the ComfyUI chip in the status bar and the Log window. Wrong look? Settings ▸ Appearance. Lost a layout? View ▸ Reset Layout. Lost a project? File ▸ Open recent. That is the tour: Help ▸ Restart tour runs it again." }, ]; diff --git a/app/ui/features/welcome/index.js b/app/ui/features/welcome/index.js index 1895978d..f1e7c540 100644 --- a/app/ui/features/welcome/index.js +++ b/app/ui/features/welcome/index.js @@ -1,4 +1,4 @@ -// The welcome screen: shown the first time this browser opens the app, and from Help ▸ Welcome tour. +// The welcome screen: shown the first time this browser opens the app, and from Help ▸ Welcome window. import { composer as c } from "../../composer/composer.js"; import { list } from "../../shell/project.js"; @@ -38,7 +38,7 @@ export default { const recent = app.project.project; const done = () => { markSeen(); if (win) win.close("done"); }; const tiles = c.region.stack({ gap: "sm", children: [ - card("Update", "Check this install for a newer version", () => app.maintenance.update()), card("Switch branch", "Pick another branch", () => app.maintenance.switchBranch()), + card("Update", "Pull the newest version from origin and restart ComfyUI", () => app.maintenance.update()), card("Switch branch", "Pick another branch", () => app.maintenance.switchBranch()), card("Restart ComfyUI", "Reload the server without updating", () => app.maintenance.restart()), card("Rollback update", "Undo the last update", () => app.maintenance.rollback())] }); tiles.node.classList.add("fp-tiles"); win = c.modal.generic({ diff --git a/modules/models/minimax_h3/pipeline.py b/modules/models/minimax_h3/pipeline.py index 3d194d56..3fd10d7a 100644 --- a/modules/models/minimax_h3/pipeline.py +++ b/modules/models/minimax_h3/pipeline.py @@ -105,7 +105,9 @@ def h3_reference_to_video(): "length": 124, "ref_image_size": "match"}}, {"id": "negative", "group": "Preparation", "node": "CLIPTextEncode", # H3 samples at CFG 1, where a negative prompt does nothing -- wired - # only because FunPackSampler requires SOME conditioning here. + # only because FunPackSampler requires SOME conditioning here. The project's Negative + # prompt lands here so Shadow negative and Negative erase have words to work from. + "roles": [{"at": "project.negative", "input": "text", "label": "Negative prompt"}], "inputs": {"clip": ["clip", 0], "text": ""}}, {"id": "settings", "group": "Preparation", "node": "FunPackModifierSettings", "inputs": {"settings": "{}"}}, diff --git a/modules/sampling/alg/__init__.py b/modules/sampling/alg/__init__.py index d1b4a6d3..21846be8 100644 --- a/modules/sampling/alg/__init__.py +++ b/modules/sampling/alg/__init__.py @@ -61,8 +61,8 @@ SETTINGS = { }, "until_sigma": { "type": "float", "default": 0.6, "min": 0.0, "max": 1.0, "step": 0.05, - "label": "How long for", "ui": "slider", - "hint": "The starting image comes back sharp once the picture settles.", + "label": "Sharp again at noise", "ui": "slider", + "hint": "The blurred start is used until the noise falls to this level (1 = full noise, 0 = clean). Lower keeps it blurred for longer; 1 never blurs.", "when": {"enabled": True}, }, } diff --git a/modules/sampling/context_windows/__init__.py b/modules/sampling/context_windows/__init__.py index 3f167b41..58f9fa8c 100644 --- a/modules/sampling/context_windows/__init__.py +++ b/modules/sampling/context_windows/__init__.py @@ -37,16 +37,21 @@ SETTINGS = { "enabled": {"type": "bool", "default": False, "label": "Context windows (long scenes)", "hint": "Sample a long scene in overlapping windows so it fits the model's memory."}, "length": {"type": "int", "default": 145, "min": 9, "max": 2049, "step": 8, "label": "Window length (frames)", + "hint": "Frames the model sees at once. Shorter uses less memory; longer keeps motion steadier.", "when": {"enabled": True}}, "overlap": {"type": "int", "default": 40, "min": 0, "max": 512, "step": 8, "label": "Window overlap (frames)", + "hint": "Frames neighbouring windows share. More overlap joins smoother but runs slower; it must be shorter than the window.", "when": {"enabled": True}}, "schedule": {"type": "enum", "default": "standard_uniform", "label": "Window schedule", + "hint": "standard_uniform: windows shift a little each step. standard_static: the same windows every step. looped_uniform: the end wraps round to the start. batched: back-to-back windows with no overlap.", "options": [{"value": v, "label": v} for v in SCHEDULES], "when": {"enabled": True}}, "fuse": {"type": "enum", "default": "pyramid", "label": "Window blend", + "hint": "How overlapping windows are mixed: pyramid, relative, overlap-linear fade between neighbours; flat averages them equally.", "options": [{"value": v, "label": v} for v in FUSES], "when": {"enabled": True}}, "freenoise": {"type": "bool", "default": True, "label": "FreeNoise blending", "when": {"enabled": True}, "hint": "Shuffle the starting noise so neighbouring windows share it."}, "retain_first": {"type": "bool", "default": False, "label": "Pin anchor in every window", + "hint": "Keeps the first frame in every window so the whole clip stays tied to your starting image.", "when": {"enabled": True}}, } diff --git a/modules/sampling/decisiveness/__init__.py b/modules/sampling/decisiveness/__init__.py index a1a4749b..c25068f4 100644 --- a/modules/sampling/decisiveness/__init__.py +++ b/modules/sampling/decisiveness/__init__.py @@ -47,13 +47,13 @@ SETTINGS = { {"value": "learned", "label": "Learn from ratings"}, {"value": "manual", "label": "My value"}, ], - "label": "Value", "ui": "segmented", - "hint": "Learn: each rating nudges it toward what you liked.", + "label": "How hard", "ui": "segmented", + "hint": "Learn from ratings: each rating nudges how much it commits toward what you liked. My value: you set it below.", "when": {"enabled": True}, }, "k": { "type": "float", "default": 1.0, "min": 0.5, "max": 1.6, "step": 0.05, - "label": "k", "ui": "slider", + "label": "Commitment (k)", "ui": "slider", "hint": "Above 1 commits harder, below 1 loosens. 1 is off.", "when": {"enabled": True, "mode": "manual"}, }, diff --git a/modules/sampling/explore_first_step/__init__.py b/modules/sampling/explore_first_step/__init__.py index 5273e48b..8f5008f3 100644 --- a/modules/sampling/explore_first_step/__init__.py +++ b/modules/sampling/explore_first_step/__init__.py @@ -31,7 +31,7 @@ SETTINGS = { "enabled": { "type": "bool", "default": False, "label": "Seed search", - "hint": "After 10+ ratings, tries a few seeds for one step each and keeps the one your taste likes.", + "hint": "After 10+ rated clips (some liked, some disliked), tries a few seeds for one step each and keeps the one your taste likes.", }, "candidates": { "type": "int", "default": 3, "min": 2, "max": 8, diff --git a/modules/sampling/late_branch/__init__.py b/modules/sampling/late_branch/__init__.py index 6c11812b..f1b88039 100644 --- a/modules/sampling/late_branch/__init__.py +++ b/modules/sampling/late_branch/__init__.py @@ -46,8 +46,8 @@ SETTINGS = { {"value": "learned", "label": "Learn from ratings"}, {"value": "manual", "label": "My value"}, ], - "label": "Strength", "ui": "segmented", - "hint": "Learn: each rating nudges it toward what you liked.", + "label": "How strong", "ui": "segmented", + "hint": "Learn from ratings: each rating nudges how much the push is toward what you liked. My value: you set it below.", "when": {"enabled": True}, }, "strength": { diff --git a/modules/sampling/output_guidance/__init__.py b/modules/sampling/output_guidance/__init__.py index 01d45fc4..e4a9b60b 100644 --- a/modules/sampling/output_guidance/__init__.py +++ b/modules/sampling/output_guidance/__init__.py @@ -32,7 +32,7 @@ SETTINGS = { "enabled": { "type": "bool", "default": False, "label": "Taste guidance", - "hint": "After 10+ ratings, nudges the finish of each clip toward what you rated up.", + "hint": "After 10+ rated clips (some liked, some disliked), nudges the finish of each clip toward what you rated up.", }, "strength": { "type": "float", "default": 0.02, "min": 0.0, "max": 0.2, "step": 0.005, diff --git a/modules/sampling/prompt_nudge/__init__.py b/modules/sampling/prompt_nudge/__init__.py index 52b98915..029e3ad5 100644 --- a/modules/sampling/prompt_nudge/__init__.py +++ b/modules/sampling/prompt_nudge/__init__.py @@ -29,7 +29,7 @@ SETTINGS = { "enabled": { "type": "bool", "default": False, "label": "Taste prompt nudge", - "hint": "Shifts the prompt toward what your liked prompts share. Free: no extra passes.", + "hint": "Shifts the prompt toward what your liked prompts share. Needs 3 liked clips. Free: no extra passes.", }, "strength": { "type": "float", "default": 0.02, "min": 0.0, "max": 0.1, "step": 0.005, diff --git a/modules/sampling/q_steer/__init__.py b/modules/sampling/q_steer/__init__.py index 77065304..e2613905 100644 --- a/modules/sampling/q_steer/__init__.py +++ b/modules/sampling/q_steer/__init__.py @@ -30,7 +30,7 @@ SETTINGS = { "enabled": { "type": "bool", "default": False, "label": "Taste attention", - "hint": "Learns from your ratings and shifts what the picture pays attention to.", + "hint": "Learns from your ratings and shifts what the picture pays attention to. Does nothing until the Taste key has 2+ liked and 2+ disliked runs.", }, "strength": { "type": "float", "default": 2.0, "min": 0.0, "max": 10.0, "step": 0.05, diff --git a/modules/sampling/score_slider/__init__.py b/modules/sampling/score_slider/__init__.py index d4aa54e4..12840cd4 100644 --- a/modules/sampling/score_slider/__init__.py +++ b/modules/sampling/score_slider/__init__.py @@ -35,7 +35,7 @@ SETTINGS = { "enabled": { "type": "bool", "default": False, "label": "Taste slider", - "hint": "Leans the finish toward what your liked prompts share. Costs 2 extra passes per late step.", + "hint": "Leans the finish toward what your liked prompts share. Needs 3 liked clips. Costs 2 extra passes per late step.", }, "strength": { "type": "float", "default": 1.0, "min": -3.0, "max": 3.0, "step": 0.25, diff --git a/modules/sampling/shot_memory/__init__.py b/modules/sampling/shot_memory/__init__.py index 1bcf8027..7a824279 100644 --- a/modules/sampling/shot_memory/__init__.py +++ b/modules/sampling/shot_memory/__init__.py @@ -45,7 +45,7 @@ SETTINGS = { "enabled": { "type": "bool", "default": False, "label": "Shot memory", - "hint": "Starts from the layout of shots you liked, with new details.", + "hint": "Some runs start from the layout of a shot you liked, with new details; ratings tune how often.", }, "mode": { "type": "enum", "default": "learned", "options": [ @@ -53,7 +53,7 @@ SETTINGS = { {"value": "manual", "label": "My value"}, ], "label": "How much", "ui": "segmented", - "hint": "Learn: how strongly to reuse is tuned by your ratings.", + "hint": "Learn from ratings: each rating nudges how much is reused toward what you liked. My value: you set it below.", "when": {"enabled": True}, }, "amount": { diff --git a/modules/sampling/stas/__init__.py b/modules/sampling/stas/__init__.py index 262ce8a2..20008b8b 100644 --- a/modules/sampling/stas/__init__.py +++ b/modules/sampling/stas/__init__.py @@ -51,14 +51,14 @@ SETTINGS = { {"value": "learned", "label": "Learn from ratings"}, {"value": "manual", "label": "My value"}, ], - "label": "Strength", "ui": "segmented", - "hint": "Learn: each rating nudges it toward what you liked.", + "label": "How strong", "ui": "segmented", + "hint": "Learn from ratings: each rating nudges how much the boost is toward what you liked. My value: you set it below.", "when": {"enabled": True}, }, "alpha": { "type": "float", "default": 2.0, "min": 0.5, "max": 3.0, "step": 0.1, "label": "Alpha", "ui": "slider", - "hint": "The paper used 1.2-2.5.", + "hint": "Higher boosts more. The paper used 1.2-2.5.", "when": {"enabled": True, "mode": "manual"}, }, "block": { diff --git a/modules/sampling/temporal_style/__init__.py b/modules/sampling/temporal_style/__init__.py index 8bb3732f..863ab911 100644 --- a/modules/sampling/temporal_style/__init__.py +++ b/modules/sampling/temporal_style/__init__.py @@ -27,8 +27,8 @@ SETTINGS = { "style": { "type": "enum", "default": "natural", "label": "Temporal style", "options": [{"value": v, "label": v.replace("_", " ")} for v in STYLES], - "hint": "How motion and time feel. accelerate/freeze hold the motion, decelerate/rapid make it punchier, " - "pulse repeats ease-downs, loop joins the end to the start.", + "hint": "How motion feels. natural: unchanged. accelerate, freeze: smoother, held motion (freeze most). decelerate: punchier, bigger steps. " + "pulse: repeated punch-then-ease. rapid start/end: punchy at the start and/or end. loop: the end joins the start.", }, } diff --git a/modules/sampling/trajectory_guidance/__init__.py b/modules/sampling/trajectory_guidance/__init__.py index 485512cf..53b0dd30 100644 --- a/modules/sampling/trajectory_guidance/__init__.py +++ b/modules/sampling/trajectory_guidance/__init__.py @@ -29,7 +29,7 @@ SETTINGS = { "enabled": { "type": "bool", "default": False, "label": "Early taste guidance", - "hint": "After 10+ ratings, nudges motion and layout too, not just the finish.", + "hint": "After 10+ rated clips (some liked, some disliked), nudges motion and layout too, not just the finish.", }, "strength": { "type": "float", "default": 0.02, "min": 0.0, "max": 0.2, "step": 0.005, diff --git a/modules/sampling/velocity_bias/__init__.py b/modules/sampling/velocity_bias/__init__.py index f27f2ecf..ab5c6566 100644 --- a/modules/sampling/velocity_bias/__init__.py +++ b/modules/sampling/velocity_bias/__init__.py @@ -7,7 +7,7 @@ latent is turned toward the average banked direction, without adding energy (the with a cap so it never wipes the current clip. A thin overlay of remembered motion: the best use is crossing a remembered action onto a new prompt, and it can re-insert a cut your liked clips had. -"Like the nearest liked prompt" picks one real clip's motion instead of the average of all. +"Like the nearest liked prompt" picks one real clip's motion instead of the average of all (the average when none is close enough). Banked directions are the size of the whole latent, so a clip is only matched with banked clips of the same shape (length and frame size); a run of another shape says so and goes unbiased. Picture only: sound is never touched.