diff --git a/js/ltx_director.js b/js/ltx_director.js index fba2cb9..d101a47 100644 --- a/js/ltx_director.js +++ b/js/ltx_director.js @@ -53,7 +53,15 @@ function stripAudioSegmentTransient(s) { } function dedupeById(items = []) { - return items.filter((seg, index, self) => index === self.findIndex((s) => s.id === seg.id)); + const seen = new Set(); + const deduped = []; + for (const seg of items || []) { + const id = seg?.id; + if (!id || seen.has(id)) continue; + seen.add(id); + deduped.push(seg); + } + return deduped; } function findActiveSegmentAtFrame(segments = [], targetFrame, type) { @@ -61,6 +69,10 @@ function findActiveSegmentAtFrame(segments = [], targetFrame, type) { (seg) => seg?.type === type && targetFrame >= seg.start && targetFrame < seg.start + seg.length ); } + +function segmentMatchesAudioSource(seg, audioFile, blobUrl) { + return !!seg && (seg.audioFile === audioFile || seg._blobUrl === blobUrl); +} function hideWidget(w) { if (!w) return; @@ -2004,6 +2016,14 @@ class TimelineEditor { return found; } + _forEachMatchingAudioSegment(audioFile, blobUrl, visitor) { + for (const seg of this.timeline.audioSegments || []) { + if (segmentMatchesAudioSource(seg, audioFile, blobUrl)) { + visitor(seg); + } + } + } + _getSegmentsByFileKey(fileKey) { const matches = []; this._forEachMatchingSegment(fileKey, (seg) => { @@ -2231,11 +2251,10 @@ class TimelineEditor { const audioBuffer = await this._loadAudioBuffer(seg); - const matchingSegs = this.timeline.audioSegments.filter(s => s.audioFile === seg.audioFile || s._blobUrl === seg._blobUrl); - for (const s of matchingSegs) { - s._audioBuffer = audioBuffer; - s._decoding = false; - } + this._forEachMatchingAudioSegment(seg.audioFile, seg._blobUrl, (s) => { + s._audioBuffer = audioBuffer; + s._decoding = false; + }); } catch (err) { console.warn("Failed to preload audio segment:", err); seg._decoding = false; diff --git a/ltx_director.py b/ltx_director.py index 61392a6..cd0bebc 100644 --- a/ltx_director.py +++ b/ltx_director.py @@ -32,7 +32,12 @@ from .prompt_relay import ( from .patches import detect_model_type, apply_patches -log = logging.getLogger(__name__) +log = logging.getLogger(__name__) +CHARACTER_TAG_GROUPS = ( + ("@character1", "@char1"), + ("@character2", "@char2"), + ("@character3", "@char3"), +) # Setup global event loop exception handler to silence ConnectionResetError (WinError 10054/10053) on Windows try: @@ -281,40 +286,26 @@ def _time_range_to_latent_indices(rel_start_frames: float, rel_length_frames: fl return start_idx, end_idx -def _preprocess_prompts_with_characters(global_prompt, local_prompts, char1="", char2="", char3=""): - """Invisibly swaps out @character1/@char1 tags with their high-fidelity VLM descriptions.""" - gp = global_prompt or "" - char1 = char1 if char1 else "" - char2 = char2 if char2 else "" - char3 = char3 if char3 else "" - - # Process Global Prompt - for tag in ["@character1", "@char1"]: - if tag in gp: - gp = gp.replace(tag, char1) - for tag in ["@character2", "@char2"]: - if tag in gp: - gp = gp.replace(tag, char2) - for tag in ["@character3", "@char3"]: - if tag in gp: - gp = gp.replace(tag, char3) - - # Process Local Timeline Prompts - locals_list = [p.strip() for p in local_prompts.split("|")] if local_prompts else [] - processed_locals = [] - for lp in locals_list: - for tag in ["@character1", "@char1"]: - if tag in lp: - lp = lp.replace(tag, char1) - for tag in ["@character2", "@char2"]: - if tag in lp: - lp = lp.replace(tag, char2) - for tag in ["@character3", "@char3"]: - if tag in lp: - lp = lp.replace(tag, char3) - processed_locals.append(lp) - - return gp, " | ".join(processed_locals) +def _preprocess_prompts_with_characters(global_prompt, local_prompts, char1="", char2="", char3=""): + """Invisibly swaps out @character1/@char1 tags with their high-fidelity VLM descriptions.""" + replacements = {} + for tags, value in zip(CHARACTER_TAG_GROUPS, (char1 or "", char2 or "", char3 or "")): + for tag in tags: + replacements[tag] = value + + def apply_replacements(text: str) -> str: + updated = text or "" + for tag, replacement in replacements.items(): + if tag in updated: + updated = updated.replace(tag, replacement) + return updated + + gp = apply_replacements(global_prompt or "") + if not local_prompts: + return gp, "" + + processed_locals = [apply_replacements(part.strip()) for part in local_prompts.split("|")] + return gp, " | ".join(processed_locals) def _load_image_source(b64_or_url: str, filename: str = None, cache: dict | None = None, @@ -1391,8 +1382,7 @@ def _build_reference_mode_outputs( raise ValueError("Licon MSR (Prefix) ref option requires connecting the VAE to LTX Director!") prompt_text = (global_prompt or "") + " " + (local_prompts or "") - tag_pairs = [("@character1", "@char1"), ("@character2", "@char2"), ("@character3", "@char3")] - referenced_slots = [i for i, tags in enumerate(tag_pairs) if any(t in prompt_text for t in tags)] + referenced_slots = [i for i, tags in enumerate(CHARACTER_TAG_GROUPS) if any(t in prompt_text for t in tags)] selected = [] for slot in referenced_slots: if slot < len(char_slot_images):