\n\n\n\n\n"],"color":"#222","bgcolor":"#000"},{"id":179,"type":"MarkdownNote","pos":[4415.296200866797,5026.288826643649],"size":[210,88],"flags":{"collapsed":true},"order":27,"mode":0,"inputs":[],"outputs":[],"title":"Clean Latent Slice","properties":{},"widgets_values":["\nDisable with Licon MSR\n"],"color":"#222","bgcolor":"#000"},{"id":94,"type":"UNETLoader","pos":[1187.7290384972068,4380.0957326862],"size":[260.8147768096842,82],"flags":{"collapsed":false},"order":28,"mode":0,"inputs":[{"localized_name":"unet_name","name":"unet_name","type":"COMBO","widget":{"name":"unet_name"},"link":null},{"localized_name":"weight_dtype","name":"weight_dtype","type":"COMBO","widget":{"name":"weight_dtype"},"link":null}],"outputs":[{"localized_name":"MODEL","name":"MODEL","type":"MODEL","links":[408]}],"properties":{"cnr_id":"comfy-core","ver":"0.21.1","Node name for S&R":"UNETLoader"},"widgets_values":["ltx\\ltx-2-3-22b-dev_transformer_only_fp8_input_scaled.safetensors","default"]},{"id":102,"type":"VHS_VideoCombine","pos":[4713.409101523936,3860.1307770768394],"size":[903.5017372863722,334],"flags":{"collapsed":false},"order":81,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":222},{"localized_name":"audio","name":"audio","shape":7,"type":"AUDIO","link":223},{"localized_name":"meta_batch","name":"meta_batch","shape":7,"type":"VHS_BatchManager","link":null},{"localized_name":"vae","name":"vae","shape":7,"type":"VAE","link":null},{"localized_name":"frame_rate","name":"frame_rate","type":"FLOAT","widget":{"name":"frame_rate"},"link":392},{"localized_name":"loop_count","name":"loop_count","type":"INT","widget":{"name":"loop_count"},"link":null},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"pingpong","name":"pingpong","type":"BOOLEAN","widget":{"name":"pingpong"},"link":null},{"localized_name":"save_output","name":"save_output","type":"BOOLEAN","widget":{"name":"save_output"},"link":null},{"name":"pix_fmt","type":["yuv420p","p010le"],"widget":{"name":"pix_fmt"},"link":null},{"name":"bitrate","type":"INT","widget":{"name":"bitrate"},"link":null},{"name":"megabit","type":"BOOLEAN","widget":{"name":"megabit"},"link":null},{"name":"save_metadata","type":"BOOLEAN","widget":{"name":"save_metadata"},"link":null}],"outputs":[{"localized_name":"Filenames","name":"Filenames","type":"VHS_FILENAMES","links":null}],"properties":{"cnr_id":"comfyui-videohelpersuite","ver":"8923bd836bdab8b7bbdf4ed104b7d045e70c66e2","Node name for S&R":"VHS_VideoCombine"},"widgets_values":{"frame_rate":24,"loop_count":0,"filename_prefix":"LTX-2","format":"video/nvenc_h264-mp4","pix_fmt":"yuv420p","bitrate":10,"megabit":true,"save_metadata":true,"pingpong":false,"save_output":true,"videopreview":{"hidden":false,"paused":false,"params":{"filename":"LTX-2_00650-audio.mp4","subfolder":"","type":"output","format":"video/nvenc_h264-mp4","frame_rate":25,"workflow":"LTX-2_00650.png","fullpath":"S:\\ComfyUI\\ComfyUI\\output\\LTX-2_00650-audio.mp4"}}}},{"id":29,"type":"KSamplerSelect","pos":[3911.538365353464,4514.496307535617],"size":[210,60],"flags":{},"order":29,"mode":0,"inputs":[{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null}],"outputs":[{"localized_name":"SAMPLER","name":"SAMPLER","type":"SAMPLER","links":[53]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.56","Node name for S&R":"KSamplerSelect","enableTabs":false,"tabWidth":65,"tabXOffset":10,"hasSecondTab":false,"secondTabText":"Send Back","secondTabOffset":80,"secondTabWidth":65},"widgets_values":["euler_ancestral_cfg_pp"]},{"id":11,"type":"BasicScheduler","pos":[3911.538365353464,4655.777557535617],"size":[210,110],"flags":{},"order":70,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":544},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"SIGMAS","name":"SIGMAS","type":"SIGMAS","links":[52]}],"properties":{"cnr_id":"comfy-core","ver":"0.16.4","Node name for S&R":"BasicScheduler"},"widgets_values":["linear_quadratic",12,1]},{"id":157,"type":"MarkdownNote","pos":[3651.9892769020544,4911.05295266081],"size":[214.491056348013,88],"flags":{"collapsed":false},"order":30,"mode":0,"inputs":[],"outputs":[],"title":"NAG","properties":{},"widgets_values":["\nNAG : enable both (ON)\n \n\ndisable both (OFF)\n\n\n"],"color":"#222","bgcolor":"#000"},{"id":204,"type":"LTXDirectorCS","pos":[1817.1933161031693,3818.332523118286],"size":[1146.460593284808,1237],"flags":{"collapsed":false},"order":49,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":482},{"localized_name":"clip","name":"clip","type":"CLIP","link":483},{"localized_name":"audio_vae","name":"audio_vae","shape":7,"type":"VAE","link":484},{"localized_name":"optional_latent","name":"optional_latent","shape":7,"type":"LATENT","link":null},{"localized_name":"global_prompt","name":"global_prompt","shape":7,"type":"STRING","link":null},{"localized_name":"vae","name":"vae","shape":7,"type":"VAE","link":485},{"localized_name":"start","name":"start","shape":7,"type":"FLOAT","link":null},{"localized_name":"end","name":"end","shape":7,"type":"FLOAT","link":null},{"localized_name":"duration","name":"duration","shape":7,"type":"FLOAT","link":null},{"localized_name":"frame_rate","name":"frame_rate","shape":7,"type":"FLOAT","widget":{"name":"frame_rate"},"link":null},{"localized_name":"custom_width","name":"custom_width","shape":7,"type":"INT","widget":{"name":"custom_width"},"link":null},{"localized_name":"custom_height","name":"custom_height","shape":7,"type":"INT","widget":{"name":"custom_height"},"link":null},{"localized_name":"resize_method","name":"resize_method","shape":7,"type":"COMBO","widget":{"name":"resize_method"},"link":null},{"localized_name":"reference_strength","name":"reference_strength","shape":7,"type":"FLOAT","widget":{"name":"reference_strength"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"MODEL","links":[486]},{"localized_name":"positive","name":"positive","type":"CONDITIONING","links":[487,488]},{"localized_name":"negative","name":"negative","type":"CONDITIONING","links":[499,543]},{"localized_name":"video_latent","name":"video_latent","type":"LATENT","links":[515]},{"localized_name":"audio_latent","name":"audio_latent","type":"LATENT","links":[491]},{"localized_name":"guide_data","name":"guide_data","type":"GUIDE_DATA","links":[501,518]},{"localized_name":"motion_guide_data","name":"motion_guide_data","type":"MOTION_GUIDE_DATA","links":[507,517]},{"localized_name":"frame_rate","name":"frame_rate","type":"FLOAT","links":[496,497]},{"localized_name":"combined_audio","name":"combined_audio","type":"AUDIO","links":null},{"localized_name":"clean_latent_frames","name":"clean_latent_frames","type":"INT","links":[498]},{"localized_name":"clean_pixel_frames","name":"clean_pixel_frames","type":"INT","links":null}],"properties":{"cnr_id":"WhatDreamsCost-ComfyUI","ver":"6a035f05769536a87d9764e8b91d5744dc014fd2","Node name for S&R":"LTXDirectorCS","global_prompt":"Aesthetic & Textures: Cinematic, photorealistic night footage. Sleek retro-futuristic coupe with a highly detailed brushed-steel metallic finish, reflecting ambient streetlights. Polished deep-dish bronze multi-piece wheels. Background features rough, weathered grey concrete overpass structures, wet asphalt, and distant city lights. High-contrast, moody atmosphere.","mainTrackEnabled":true,"audioTrackEnabled":true,"motionTrackEnabled":true,"audioTrackWasEnabledBeforeOverride":false,"inpaint_audio":true,"override_audio":false,"overrideAudio":false,"showFilenames":true,"use_custom_audio":true,"use_custom_motion":true,"frame_rate":25,"display_mode":"seconds","custom_width":1920,"custom_height":1088,"resize_method":"stretch to fit","divisible_by":32,"img_compression":18,"guide_strength":"","local_prompts":"","segment_lengths":"","timeline_data":"{\"mainTrackEnabled\":true,\"audioTrackEnabled\":true,\"motionTrackEnabled\":true,\"propHeight\":90,\"globalPropHeight\":67,\"showFilenames\":true,\"overrideAudio\":false,\"inpaint_audio\":true,\"global_prompt\":\"Aesthetic & Textures: Cinematic, photorealistic night footage. Sleek retro-futuristic coupe with a highly detailed brushed-steel metallic finish, reflecting ambient streetlights. Polished deep-dish bronze multi-piece wheels. Background features rough, weathered grey concrete overpass structures, wet asphalt, and distant city lights. High-contrast, moody atmosphere.\",\"retake_global_prompt\":\"\",\"retakeMode\":false,\"retakeStart\":24,\"retakeLength\":48,\"retakePrompt\":\"\",\"retakeStrength\":1,\"retakeVideo\":null,\"normalStartFrame\":0,\"normalDurationFrames\":200,\"reference_mode\":\"Licon MSR (Prefix)\",\"\"characters\":[{\"images\":[],\"description\":\"He has short graying hair and a rugged face marked with visible age spots and scarring on his cheeks. He is dressed in a tattered brown leather apron worn over an olive-green jacket layered under a scarf. ha has one cyber eye\"},{\"images\":[],\"description\":\"The character is a tall, slender humanoid with smooth pale skin and a strong jawline beneath sleek eye-goggles covering her eyes entirely. She wears an imposing black hooded headpiece atop the neck of a matte-black tactical suit accented with orange mechanical joints.\"},{\"images\":[],\"description\":\"\"}],\"segments\":[],\"motionSegments\":[],\"audioSegments\":[]}","epsilon":0.001,"start_second":0,"end_second":8,"duration_seconds":8,"start_frame":0,"end_frame":200,"duration_frames":200,"propHeight":90,"globalPropHeight":67,"reference_strength":1,"timeline_ui":"","has_serialized_properties":true,"retakeMode":false,"ltx_settings_ui":""},"widgets_values":[0,8,8,0,200,200,"{\"mainTrackEnabled\":true,\"audioTrackEnabled\":true,\"motionTrackEnabled\":true,\"propHeight\":90,\"globalPropHeight\":67,\"showFilenames\":true,\"overrideAudio\":false,\"inpaint_audio\":true,\"global_prompt\":\"Aesthetic & Textures: Cinematic, photorealistic night footage. Sleek retro-futuristic coupe with a highly detailed brushed-steel metallic finish, reflecting ambient streetlights. Polished deep-dish bronze multi-piece wheels. Background features rough, weathered grey concrete overpass structures, wet asphalt, and distant city lights. High-contrast, moody atmosphere.\",\"retake_global_prompt\":\"\",\"retakeMode\":false,\"retakeStart\":24,\"retakeLength\":48,\"retakePrompt\":\"\",\"retakeStrength\":1,\"retakeVideo\":null,\"normalStartFrame\":0,\"normalDurationFrames\":200,\"reference_mode\":\"Licon MSR (Prefix)\",\"\"characters\":[{\"images\":[],\"description\":\"He has short graying hair and a rugged face marked with visible age spots and scarring on his cheeks. He is dressed in a tattered brown leather apron worn over an olive-green jacket layered under a scarf. ha has one cyber eye\"},{\"images\":[],\"description\":\"The character is a tall, slender humanoid with smooth pale skin and a strong jawline beneath sleek eye-goggles covering her eyes entirely. She wears an imposing black hooded headpiece atop the neck of a matte-black tactical suit accented with orange mechanical joints.\"},{\"images\":[],\"description\":\"\"}],\"segments\":[],\"motionSegments\":[],\"audioSegments\":[]}","","",0.001,"",true,true,true,25,"seconds",1920,1088,"stretch to fit",32,18,false,1,"",""]},{"id":146,"type":"PrimitiveStringMultiline","pos":[4372.490971119918,4102.805179368286],"size":[210,88],"flags":{"collapsed":false},"order":31,"mode":0,"inputs":[{"localized_name":"value","name":"value","type":"STRING","widget":{"name":"value"},"link":null}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":[318]}],"properties":{"cnr_id":"comfy-core","ver":"0.22.0","Node name for S&R":"PrimitiveStringMultiline"},"widgets_values":["0.85, 0.6165, 0.3460, 0.0000"]},{"id":104,"type":"ManualSigmas","pos":[4112.490971119919,4132.805179368286],"size":[230,58],"flags":{"collapsed":false},"order":39,"mode":0,"inputs":[{"localized_name":"sigmas","name":"sigmas","type":"STRING","widget":{"name":"sigmas"},"link":318}],"outputs":[{"localized_name":"SIGMAS","name":"SIGMAS","type":"SIGMAS","links":[546]}],"properties":{"cnr_id":"comfy-core","ver":"0.14.1","Node name for S&R":"ManualSigmas"},"widgets_values":["0.85, 0.7250, 0.4219, 0.0"]},{"id":150,"type":"MarkdownNote","pos":[4713.409101523936,4789.328871446935],"size":[288.8383013624807,147.09966422352863],"flags":{"collapsed":false},"order":32,"mode":0,"inputs":[],"outputs":[],"title":"About Models","properties":{},"widgets_values":["\neuler_ancestral_cgf_pp (more photorealistic)\nres 3s (slower)\n\n\n20 steps (best)\n8 steps (ok)\n\nUpscale 3 ok 4 better (plug Basicscheduler to Sampler)\n\nDistilled 0.6 lora (good ref sheet fidelity)\n\n\n48 fps for perfect fast motion\n\nMSR Licon WORKING ! 0.75s artifacts when upscaled because of HD images reinjection\n \n"],"color":"#222","bgcolor":"#000"}],"links":[[12,9,0,10,1,"GUIDER"],[14,7,0,10,4,"LATENT"],[51,28,0,10,0,"NOISE"],[52,11,0,10,3,"SIGMAS"],[53,29,0,10,2,"SAMPLER"],[124,49,0,47,1,"GUIDER"],[125,53,0,47,2,"SAMPLER"],[127,50,0,47,4,"LATENT"],[144,57,0,52,1,"LATENT_UPSCALE_MODEL"],[157,28,0,47,0,"NOISE"],[180,47,0,48,0,"LATENT"],[196,78,0,79,2,"VAE"],[213,101,0,95,0,"MODEL"],[214,99,0,96,0,"MODEL"],[215,97,0,98,0,"MODEL"],[216,100,0,99,0,"MODEL"],[217,98,0,100,0,"MODEL"],[218,99,0,101,0,"MODEL"],[219,80,0,97,0,"MODEL"],[222,15,0,102,0,"IMAGE"],[223,16,0,102,1,"AUDIO"],[227,4,0,107,0,"VAE"],[228,108,0,16,1,"VAE"],[229,3,0,109,0,"VAE"],[231,111,0,52,2,"VAE"],[233,113,0,15,1,"VAE"],[235,13,1,16,0,"LATENT"],[267,10,0,13,0,"LATENT"],[289,128,0,127,1,"CONDITIONING"],[294,84,0,129,0,"CLIP"],[295,130,0,128,0,"CLIP"],[296,128,0,127,2,"CONDITIONING"],[318,146,0,104,0,"STRING"],[325,127,0,147,0,"MODEL"],[337,128,0,149,1,"CONDITIONING"],[382,13,1,174,0,"LATENT"],[383,175,0,50,1,"LATENT"],[384,176,0,52,0,"LATENT"],[392,181,0,102,4,"FLOAT"],[405,79,0,80,0,"MODEL"],[408,94,0,79,0,"MODEL"],[411,128,0,187,0,"CONDITIONING"],[413,149,0,5,1,"CONDITIONING"],[482,101,0,204,0,"MODEL"],[483,84,0,204,1,"CLIP"],[484,4,0,204,2,"VAE"],[485,3,0,204,5,"VAE"],[486,204,0,127,0,"MODEL"],[487,204,1,160,0,"CONDITIONING"],[488,204,1,5,0,"CONDITIONING"],[491,204,4,7,1,"LATENT"],[496,204,7,180,0,"FLOAT"],[497,204,7,5,2,"FLOAT"],[498,204,9,182,0,"INT"],[499,204,2,161,0,"CONDITIONING"],[500,52,0,205,3,"LATENT"],[501,204,5,205,4,"GUIDE_DATA"],[502,205,0,49,1,"CONDITIONING"],[503,205,1,49,2,"CONDITIONING"],[504,205,3,49,0,"MODEL"],[505,205,2,50,0,"LATENT"],[506,112,0,205,2,"VAE"],[507,204,6,205,5,"MOTION_GUIDE_DATA"],[508,103,0,205,6,"MODEL"],[509,205,0,169,0,"CONDITIONING"],[510,205,1,170,0,"CONDITIONING"],[511,167,0,205,0,"CONDITIONING"],[512,168,0,205,1,"CONDITIONING"],[513,5,0,206,0,"CONDITIONING"],[514,5,1,206,1,"CONDITIONING"],[515,204,3,206,3,"LATENT"],[516,127,0,206,6,"MODEL"],[517,204,6,206,5,"MOTION_GUIDE_DATA"],[518,204,5,206,4,"GUIDE_DATA"],[519,110,0,206,2,"VAE"],[520,206,0,9,1,"CONDITIONING"],[521,206,1,9,2,"CONDITIONING"],[522,206,2,7,0,"LATENT"],[523,206,3,9,0,"MODEL"],[524,206,1,166,0,"CONDITIONING"],[525,206,0,165,0,"CONDITIONING"],[527,171,0,207,0,"CONDITIONING"],[528,172,0,207,1,"CONDITIONING"],[529,48,0,207,2,"LATENT"],[532,163,0,208,1,"CONDITIONING"],[533,162,0,208,0,"CONDITIONING"],[534,208,2,173,0,"LATENT"],[535,48,0,209,0,"LATENT"],[536,209,0,15,0,"LATENT"],[537,196,0,209,2,"INT"],[538,205,3,54,0,"MODEL"],[539,13,0,208,2,"LATENT"],[543,204,2,149,0,"CONDITIONING"],[544,206,3,11,0,"MODEL"],[546,104,0,47,3,"SIGMAS"]],"groups":[{"id":4,"title":"Stage #1","bounding":[3016.805994963843,4308.288648200755,1606.2379320089217,760.3108486715846],"flags":{}},{"id":5,"title":"Stage #2 Upscale","bounding":[3014.511362276872,3740.2110485574576,1610.58065732864,543.7537097700324],"flags":{}},{"id":6,"title":"Process Video","bounding":[4647.885369591847,3745.09345122726,1012.506433346367,1323.8349380513605],"flags":{}},{"id":7,"title":"Timeline","bounding":[1777.8233971795028,3740.7638722614215,1223.960654049973,1327.678450477571],"flags":{}},{"id":8,"title":"Peformace tweaks (help low vram)","bounding":[1270.0934983588413,4153.903583195573,485.2883108883925,109.75103239530472],"flags":{}},{"id":9,"title":"LTX-2 Loras","bounding":[1266.851443434426,3821.7807258321477,493.249044492934,316.97960132943217],"flags":{}},{"id":10,"title":"Group","bounding":[1153.1440092407659,4279.708623295673,597.8960119181045,592.6764849338833],"flags":{}}],"config":{},"extra":{"ds":{"scale":0.9800820733973077,"offset":[-3016.3071410521075,-3952.936492586513]},"frontendVersion":"1.42.14","VHS_latentpreview":false,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4}
\ No newline at end of file
diff --git a/js/ltx_director.js b/js/ltx_director.js
index 8dd1e86..dca7603 100644
--- a/js/ltx_director.js
+++ b/js/ltx_director.js
@@ -11350,31 +11350,9 @@ const APPENDED_WIDGET_DEFAULTS = [
["segment_lengths", ""],
];
-app.registerExtension({
+app.registerExtension({
name: "LTXDirectorCS",
- async setup() {
- // On Run, ask Ollama to release the analysis model from VRAM so it doesn't
- // compete with LTX generation. Fully tolerant: failures are swallowed.
- if (app._ltxDirectorUnloadHookInstalled) return;
- app._ltxDirectorUnloadHookInstalled = true;
- const origQueuePrompt = app.queuePrompt;
- app.queuePrompt = async function (...args) {
- try {
- const nodes = app.graph?._nodes || [];
- const director = nodes.find(n => n && (n.comfyClass === "LTXDirectorCS" || n.type === "LTXDirectorCS"));
- if (director) {
- try {
- await api.fetchApi("/ltx_director/unload_ollama", {
- method: "POST",
- body: JSON.stringify({ provider: "ollama" }),
- });
- } catch (e) {}
- }
- } catch (e) {}
- return origQueuePrompt.apply(this, args);
- };
- },
- async beforeRegisterNodeDef(nodeType, nodeData, app) {
+ async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "LTXDirectorCS") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
diff --git a/js/msr_character.js b/js/msr_character.js
index b9f7fc7..b5fac4b 100644
--- a/js/msr_character.js
+++ b/js/msr_character.js
@@ -1,6 +1,5 @@
const { app } = window.comfyAPI.app;
const { api } = window.comfyAPI.api;
-const DEFAULT_ANALYZE_PROMPT = "Describe the character's physical appearance in two concise sentences. Specify their hair color/style, face details, and their clothing type/color. Keep the entire response very brief.";
function findWidget(node, name) {
return (node.widgets || []).find((widget) => widget.name === name);
@@ -58,139 +57,6 @@ function resolveCandidateImageUrls(originNode) {
return urls;
}
-async function blobToOptimizedDataUrl(blob, maxDim = 1024, quality = 0.82) {
- const objectUrl = URL.createObjectURL(blob);
- try {
- const image = await new Promise((resolve, reject) => {
- const img = new Image();
- img.onload = () => resolve(img);
- img.onerror = () => reject(new Error("Failed to decode image blob"));
- img.src = objectUrl;
- });
-
- const width = image.naturalWidth || image.width || 0;
- const height = image.naturalHeight || image.height || 0;
- const scale = width > 0 && height > 0 ? Math.min(1, maxDim / Math.max(width, height)) : 1;
- const targetWidth = Math.max(1, Math.round(width * scale)) || width || 1;
- const targetHeight = Math.max(1, Math.round(height * scale)) || height || 1;
-
- const canvas = document.createElement("canvas");
- canvas.width = targetWidth;
- canvas.height = targetHeight;
- const ctx = canvas.getContext("2d");
- if (!ctx) {
- throw new Error("Could not acquire canvas context");
- }
- ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
-
- const optimizedBlob = await new Promise((resolve, reject) => {
- canvas.toBlob((result) => {
- if (result) resolve(result);
- else reject(new Error("Canvas toBlob failed"));
- }, "image/jpeg", quality);
- });
-
- const dataUrl = await new Promise((resolve) => {
- const reader = new FileReader();
- reader.onloadend = () => resolve(reader.result);
- reader.readAsDataURL(optimizedBlob);
- });
-
- return {
- dataUrl,
- originalWidth: width,
- originalHeight: height,
- outputWidth: targetWidth,
- outputHeight: targetHeight,
- outputBytes: optimizedBlob.size || 0,
- };
- } finally {
- URL.revokeObjectURL(objectUrl);
- }
-}
-
-async function imageInputToDataUrl(node, inputName) {
- const originNode = getOriginNodeForInput(node, inputName);
- const candidateUrls = resolveCandidateImageUrls(originNode);
- if (!candidateUrls.length) {
- return {
- dataUrl: null,
- debug: {
- inputName,
- originNodeId: originNode?.id ?? null,
- originNodeType: originNode?.type ?? null,
- candidateUrls: [],
- selectedUrl: null,
- mimeType: null,
- blobBytes: 0,
- dataUrlLength: 0,
- originalWidth: 0,
- originalHeight: 0,
- outputWidth: 0,
- outputHeight: 0,
- outputBytes: 0,
- error: "No candidate image URLs found",
- },
- };
- }
-
- for (const imageUrl of candidateUrls) {
- try {
- const response = await fetch(imageUrl);
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- const blob = await response.blob();
- if (!blob.type.startsWith("image/")) {
- throw new Error(`Unexpected blob type: ${blob.type || "unknown"}`);
- }
- const optimized = await blobToOptimizedDataUrl(blob);
- const dataUrl = optimized.dataUrl;
- return {
- dataUrl,
- debug: {
- inputName,
- originNodeId: originNode?.id ?? null,
- originNodeType: originNode?.type ?? null,
- candidateUrls,
- selectedUrl: imageUrl,
- mimeType: blob.type || null,
- blobBytes: blob.size || 0,
- dataUrlLength: typeof dataUrl === "string" ? dataUrl.length : 0,
- originalWidth: optimized.originalWidth,
- originalHeight: optimized.originalHeight,
- outputWidth: optimized.outputWidth,
- outputHeight: optimized.outputHeight,
- outputBytes: optimized.outputBytes,
- error: null,
- },
- };
- } catch (error) {
- console.warn("[MSRCharacter] Failed candidate image source", imageUrl, error);
- }
- }
-
- return {
- dataUrl: null,
- debug: {
- inputName,
- originNodeId: originNode?.id ?? null,
- originNodeType: originNode?.type ?? null,
- candidateUrls,
- selectedUrl: null,
- mimeType: null,
- blobBytes: 0,
- dataUrlLength: 0,
- originalWidth: 0,
- originalHeight: 0,
- outputWidth: 0,
- outputHeight: 0,
- outputBytes: 0,
- error: "All candidate image URLs failed",
- },
- };
-}
-
function setWidgetValue(node, widget, value) {
if (!widget) return;
const previous = widget.value;
@@ -220,9 +86,6 @@ function setWidgetValue(node, widget, value) {
function syncFormFromWidgets(node) {
const description = findWidget(node, "description")?.value || "";
const alias = findWidget(node, "alias")?.value || "";
- const provider = findWidget(node, "analyze_provider")?.value || "ollama";
- const model = findWidget(node, "analyze_model")?.value || "";
- const baseUrl = findWidget(node, "analyze_base_url")?.value || "";
if (node._msrAliasInput && node._msrAliasInput.value !== alias) {
node._msrAliasInput.value = alias;
@@ -235,8 +98,7 @@ function syncFormFromWidgets(node) {
alias
? `Alias: @${String(alias).replace(/^@/, "")}`
: "Alias: none",
- `Analyze: ${provider}${model ? ` / ${model}` : ""}${baseUrl ? ` / ${baseUrl}` : ""}
`,
- `Description is stored in the field below.
`,
+ `Description is stored in the field below and appended to the director prompt as a tagged character reference.
`,
].join("");
}
}
@@ -323,70 +185,6 @@ function buildCharacterUi(node) {
gap: "6px",
});
- const actionsRow = document.createElement("div");
- Object.assign(actionsRow.style, {
- display: "flex",
- gap: "8px",
- alignItems: "center",
- flexWrap: "wrap",
- });
-
- const analyzeButton = document.createElement("button");
- analyzeButton.type = "button";
- analyzeButton.textContent = "Analyze";
- Object.assign(analyzeButton.style, {
- alignSelf: "flex-start",
- background: "#2b4f38",
- color: "#f3f3f3",
- border: "1px solid #496d56",
- borderRadius: "6px",
- padding: "6px 10px",
- fontSize: "11px",
- cursor: "pointer",
- });
- analyzeButton.addEventListener("click", () => {
- const widgetButton = (node.widgets || []).find((widget) => widget.msrAnalyze);
- if (!widgetButton) {
- alert("Analyze button widget is missing on this node.");
- return;
- }
- analyzeCharacterNode(node, widgetButton);
- });
-
- const settingsButton = document.createElement("button");
- settingsButton.type = "button";
- settingsButton.textContent = "Analyze Settings";
- Object.assign(settingsButton.style, {
- alignSelf: "flex-start",
- background: "#252525",
- color: "#ddd",
- border: "1px solid #444",
- borderRadius: "6px",
- padding: "6px 10px",
- fontSize: "11px",
- cursor: "pointer",
- });
- settingsButton.addEventListener("click", () => {
- const providerWidget = findWidget(node, "analyze_provider");
- const baseUrlWidget = findWidget(node, "analyze_base_url");
- const modelWidget = findWidget(node, "analyze_model");
- const promptWidget = findWidget(node, "analyze_prompt");
- const provider = window.prompt("Analyze provider: ollama, lmstudio, custom, off", providerWidget?.value || "ollama");
- if (provider == null) return;
- const baseUrl = window.prompt("Analyze base URL (blank = default)", baseUrlWidget?.value || "");
- if (baseUrl == null) return;
- const model = window.prompt("Analyze model (blank = provider default)", modelWidget?.value || "");
- if (model == null) return;
- const prompt = window.prompt("Analyze prompt (blank = default)", promptWidget?.value || DEFAULT_ANALYZE_PROMPT);
- if (prompt == null) return;
- setWidgetValue(node, providerWidget, provider.trim() || "ollama");
- setWidgetValue(node, baseUrlWidget, baseUrl.trim());
- setWidgetValue(node, modelWidget, model.trim());
- setWidgetValue(node, promptWidget, prompt.trim());
- syncFormFromWidgets(node);
- node.setDirtyCanvas?.(true, true);
- });
-
const aliasLabel = document.createElement("label");
aliasLabel.textContent = "Alias";
Object.assign(aliasLabel.style, {
@@ -439,13 +237,10 @@ function buildCharacterUi(node) {
syncFormFromWidgets(node);
});
- actionsRow.appendChild(analyzeButton);
- actionsRow.appendChild(settingsButton);
form.appendChild(aliasLabel);
form.appendChild(aliasInput);
form.appendChild(descLabel);
form.appendChild(descInput);
- form.appendChild(actionsRow);
container.appendChild(previewRow);
container.appendChild(meta);
@@ -456,79 +251,9 @@ function buildCharacterUi(node) {
node._msrMeta = meta;
node._msrAliasInput = aliasInput;
node._msrDescriptionInput = descInput;
- node._msrAnalyzeButton = analyzeButton;
return container;
}
-async function analyzeCharacterNode(node, buttonWidget) {
- const descriptionWidget = findWidget(node, "description");
- if (!descriptionWidget) return;
- const providerWidget = findWidget(node, "analyze_provider");
- const baseUrlWidget = findWidget(node, "analyze_base_url");
- const modelWidget = findWidget(node, "analyze_model");
- const promptWidget = findWidget(node, "analyze_prompt");
-
- buttonWidget.label = "Analyzing...";
- if (node._msrAnalyzeButton) {
- node._msrAnalyzeButton.textContent = "Analyzing...";
- node._msrAnalyzeButton.disabled = true;
- }
- node.setDirtyCanvas?.(true, true);
-
- try {
- const imageResults = await Promise.all([
- imageInputToDataUrl(node, "image_1"),
- imageInputToDataUrl(node, "image_2"),
- ]);
- const imageDebug = imageResults.map((result) => result?.debug || null).filter(Boolean);
- const images = (
- imageResults.map((result) => result?.dataUrl || null)
- ).filter(Boolean);
-
- if (!images.length) {
- throw new Error("Connect at least one image input before analyzing.");
- }
-
- const response = await api.fetchApi("/ltx_director/analyze_character", {
- method: "POST",
- body: JSON.stringify({
- provider: providerWidget?.value || "ollama",
- base_url: baseUrlWidget?.value || "",
- model: modelWidget?.value || "",
- prompt: promptWidget?.value || "",
- image_b64: images,
- image_debug: imageDebug,
- }),
- });
- const result = await response.json();
- if (result.status !== "success") {
- console.warn("[MSRCharacter] analyze debug", {
- sentImages: imageDebug,
- response: result,
- });
- throw new Error(result.message || "Unknown analysis error");
- }
-
- const description = result.description || "";
- setWidgetValue(node, descriptionWidget, description);
- if (node._msrDescriptionInput) {
- node._msrDescriptionInput.value = description;
- }
- syncFormFromWidgets(node);
- refreshCharacterPreview(node);
- } catch (error) {
- console.error("[MSRCharacter] analyze failed", error);
- alert(`MSR Character analyze failed: ${error.message || error}`);
- } finally {
- buttonWidget.label = "Analyze with Ollama";
- if (node._msrAnalyzeButton) {
- node._msrAnalyzeButton.textContent = "Analyze";
- node._msrAnalyzeButton.disabled = false;
- }
- node.setDirtyCanvas?.(true, true);
- }
-}
-
function hideNodeWidget(widget) {
if (!widget) return;
widget.hidden = true;
@@ -547,30 +272,6 @@ app.registerExtension({
nodeType.prototype.onNodeCreated = function () {
if (originalOnNodeCreated) originalOnNodeCreated.apply(this, arguments);
- if (!(this.widgets || []).find((widget) => widget.msrAnalyze)) {
- this.addWidget("button", "Analyze with Ollama", null, () => {
- analyzeCharacterNode(this, (this.widgets || []).find((widget) => widget.msrAnalyze));
- }, { serialize: false });
- const buttonWidget = this.widgets[this.widgets.length - 1];
- buttonWidget.msrAnalyze = true;
- buttonWidget.label = "Analyze with Ollama";
- }
-
- if (!findWidget(this, "analyze_provider")) {
- this.addWidget("combo", "analyze_provider", "ollama", null, {
- values: ["ollama", "lmstudio", "custom", "off"],
- });
- }
- if (!findWidget(this, "analyze_base_url")) {
- this.addWidget("text", "analyze_base_url", "");
- }
- if (!findWidget(this, "analyze_model")) {
- this.addWidget("text", "analyze_model", "");
- }
- if (!findWidget(this, "analyze_prompt")) {
- this.addWidget("text", "analyze_prompt", DEFAULT_ANALYZE_PROMPT);
- }
-
if (!this._msrPreviewWidget) {
const previewContainer = buildCharacterUi(this);
this._msrPreviewWidget = this.addDOMWidget("msr_character_ui", "msr_character_ui", previewContainer, {
@@ -597,10 +298,9 @@ app.registerExtension({
return result;
};
- ["alias", "description", "analyze_provider", "analyze_base_url", "analyze_model", "analyze_prompt"].forEach((widgetName) => {
+ ["alias", "description"].forEach((widgetName) => {
hideNodeWidget(findWidget(this, widgetName));
});
- hideNodeWidget((this.widgets || []).find((widget) => widget.msrAnalyze));
this.size[0] = Math.max(this.size[0] || 0, 330);
syncFormFromWidgets(this);
diff --git a/ltx_director.py b/ltx_director.py
index bd1d018..5ca2a78 100644
--- a/ltx_director.py
+++ b/ltx_director.py
@@ -366,50 +366,32 @@ def _build_character_tag_groups(characters: list[dict]) -> list[tuple[str, ...]]
return groups
-def _character_prompt_replacements(characters: list[dict]) -> dict[str, str]:
- replacements: dict[str, str] = {}
- for character, tags in zip(characters or [], _build_character_tag_groups(characters)):
- replacement = character.get("description", "") or ""
- for tag in tags:
- replacements[tag] = replacement
- return replacements
+def _build_character_reference_block(characters: list[dict] | None = None) -> str:
+ lines: list[str] = []
+ for idx, character in enumerate(characters or []):
+ description = (character.get("description", "") or "").strip()
+ if not description:
+ continue
+
+ alias = _normalize_character_alias(character.get("alias", ""))
+ canonical_tag = f"@char{idx + 1}"
+ ref_label = canonical_tag if not alias else f"{canonical_tag} / @{alias}"
+ lines.append(f"{ref_label} - {description}")
+
+ if not lines:
+ return ""
+
+ return "Character references:\n" + "\n".join(lines)
-def _prepend_character_descriptions(global_prompt: str, characters: list[dict] | None = None) -> str:
- descriptions = [
- (character.get("description", "") or "").strip()
- for character in (characters or [])
- if (character.get("description", "") or "").strip()
- ]
- if not descriptions:
- return global_prompt or ""
-
- prefix = ". ".join(descriptions)
- if not (global_prompt or "").strip():
- return prefix
- return f"{prefix}. {global_prompt.strip()}"
-
-
-def _preprocess_prompts_with_characters(global_prompt, local_prompts, characters: list[dict] | None = None):
- """Invisibly swaps out @characterN/@charN/@alias tags with their character descriptions."""
- if "@" not in (global_prompt or "") and "@" not in (local_prompts or ""):
- return global_prompt or "", local_prompts or ""
-
- replacements = _character_prompt_replacements(characters or [])
-
- 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 _append_character_references(global_prompt: str, characters: list[dict] | None = None) -> str:
+ prompt = (global_prompt or "").strip()
+ reference_block = _build_character_reference_block(characters)
+ if not reference_block:
+ return prompt
+ if not prompt:
+ return reference_block
+ return f"{prompt}\n\n{reference_block}"
def _load_image_source(b64_or_url: str, filename: str = None, cache: dict | None = None,
@@ -512,312 +494,6 @@ async def ltx_director_check_file(request):
# --- Provider defaults shared by the analyze + unload endpoints ---
-_PROVIDER_DEFAULTS = {
- "ollama": {"url": "http://127.0.0.1:11434", "model": "huihui_ai/qwen3.5-abliterated:2b"},
- "lmstudio": {"url": "http://127.0.0.1:1234", "model": ""},
- "custom": {"url": "", "model": ""},
-}
-
-_ANALYZE_SYSTEM_PROMPT = ""
-
-_ANALYZE_PROMPT = (
- "Describe the character's physical appearance in two concise sentences. "
- "Specify their hair color/style, face details, and their clothing type/color. "
- "Keep the entire response very brief."
-)
-
-
-def _resolve_analyze_prompt(data: dict) -> str:
- prompt = (data.get("prompt") or "").strip()
- return prompt or _ANALYZE_PROMPT
-
-
-def _extract_ollama_generated_text(resp_json: dict) -> str:
- if not isinstance(resp_json, dict):
- return ""
-
- candidates = [resp_json.get("response"), resp_json.get("content"), resp_json.get("thinking")]
- message = resp_json.get("message")
- if isinstance(message, dict):
- candidates.extend([
- message.get("content"),
- message.get("reasoning_content"),
- message.get("thinking"),
- ])
-
- cleaned_candidates = []
- for candidate in candidates:
- if isinstance(candidate, str):
- text = candidate.strip()
- if "" in text:
- text = text.split("")[-1].strip()
- if text:
- cleaned_candidates.append(text)
-
- if not cleaned_candidates:
- return ""
-
- usable = [text for text in cleaned_candidates if _analysis_text_is_usable(text)]
- if usable:
- return max(usable, key=len)
-
- return max(cleaned_candidates, key=len)
-
-
-def _collect_ollama_text_candidates(resp_json: dict) -> list[str]:
- if not isinstance(resp_json, dict):
- return []
-
- candidates = [resp_json.get("response"), resp_json.get("content"), resp_json.get("thinking")]
- message = resp_json.get("message")
- if isinstance(message, dict):
- candidates.extend([
- message.get("content"),
- message.get("reasoning_content"),
- message.get("thinking"),
- ])
-
- cleaned = []
- for candidate in candidates:
- if isinstance(candidate, str):
- text = candidate.strip()
- if "" in text:
- text = text.split("")[-1].strip()
- if text:
- cleaned.append(text)
- return cleaned
-
-
-def _analysis_text_is_usable(text: str) -> bool:
- text = (text or "").strip()
- if not text:
- return False
- if len(text) < 24:
- return False
- if len(text.split()) < 6:
- return False
- return True
-
-
-def _compress_analysis_image_b64(b64_payload: str, max_dim: int = 768, quality: int = 82) -> str:
- """Shrink analysis images so multimodal providers do not burn their full context on pixels."""
- try:
- raw = base64.b64decode(_normalise_b64_payload(b64_payload))
- with Image.open(_io.BytesIO(raw)) as img:
- img = img.convert("RGB")
- w, h = img.size
- if max(w, h) > max_dim:
- scale = max_dim / float(max(w, h))
- img = img.resize((max(1, int(round(w * scale))), max(1, int(round(h * scale)))), Image.LANCZOS)
- out = _io.BytesIO()
- img.save(out, format="JPEG", quality=quality, optimize=True)
- return base64.b64encode(out.getvalue()).decode("ascii")
- except Exception:
- return _normalise_b64_payload(b64_payload)
-
-
-def _prepare_analysis_images(cleaned_b64_list: list[str], max_dim: int = 768, quality: int = 82) -> list[str]:
- return [
- _compress_analysis_image_b64(b64, max_dim=max_dim, quality=quality)
- for b64 in cleaned_b64_list
- ]
-
-
-def _resolve_provider(data):
- provider = (data.get("provider") or "ollama").lower()
- defs = _PROVIDER_DEFAULTS.get(provider, _PROVIDER_DEFAULTS["ollama"])
- base_url = (data.get("base_url") or defs["url"]).rstrip("/")
- model = data.get("model") or defs["model"]
- return provider, base_url, model
-
-
-# --- Character reference analysis endpoint (Ollama / LM Studio / Custom OpenAI-compatible) ---
-@PromptServer.instance.routes.post("/ltx_director/analyze_character")
-async def analyze_character_endpoint(request):
- try:
- import aiohttp
- data = await request.json()
- image_b64 = data.get("image_b64", "")
- image_debug = data.get("image_debug") or []
- char_index = int(data.get("char_index", 0))
- provider, base_url, model_name = _resolve_provider(data)
- analyze_prompt = _resolve_analyze_prompt(data)
-
- if provider == "off":
- return web.json_response({"status": "error", "message": "Analyze is set to Off / Manual."})
- if not image_b64:
- return web.json_response({"status": "error", "message": "No image provided for analysis."})
-
- b64_list = image_b64 if isinstance(image_b64, list) else [image_b64]
- cleaned_b64_list = []
- for b64 in b64_list:
- if "," in b64:
- b64 = b64.split(",", 1)[1]
- cleaned_b64_list.append(b64)
- if not cleaned_b64_list:
- return web.json_response({"status": "error", "message": "No valid base64 images decoded."})
- if provider in ("lmstudio", "custom") and not model_name:
- return web.json_response({
- "status": "error",
- "message": f"No model name set for {provider}. Open the gear menu and enter your loaded model's name.",
- })
-
- log.info("[LTXDirector] Analyzing Character %d via %s (%s, model '%s')...",
- char_index + 1, provider, base_url, model_name)
-
- try:
- async with aiohttp.ClientSession() as session:
- if provider == "ollama":
- debug_candidates = []
- analysis_images = cleaned_b64_list
- payload = {
- "model": model_name,
- "system": _ANALYZE_SYSTEM_PROMPT,
- "prompt": analyze_prompt,
- "images": analysis_images,
- "stream": False,
- "keep_alive": 0,
- "options": {
- "temperature": 0.2,
- "num_predict": 768,
- },
- }
- async with session.post(f"{base_url}/api/generate", json=payload, timeout=300) as response:
- if response.status != 200:
- err_txt = await response.text()
- if response.status == 400 and "exceeds the available context size" in err_txt:
- analysis_images = _prepare_analysis_images(cleaned_b64_list, max_dim=512, quality=70)
- payload["images"] = analysis_images
- async with session.post(f"{base_url}/api/generate", json=payload, timeout=300) as retry_response:
- if retry_response.status != 200:
- retry_err = await retry_response.text()
- return web.json_response({"status": "error", "message": f"Ollama HTTP {retry_response.status}: {retry_err}"})
- resp_json = await retry_response.json()
- debug_candidates.extend(_collect_ollama_text_candidates(resp_json))
- generated_text = _extract_ollama_generated_text(resp_json)
- else:
- return web.json_response({"status": "error", "message": f"Ollama HTTP {response.status}: {err_txt}"})
- else:
- resp_json = await response.json()
- debug_candidates.extend(_collect_ollama_text_candidates(resp_json))
- generated_text = _extract_ollama_generated_text(resp_json)
-
- if not _analysis_text_is_usable(generated_text):
- chat_payload = {
- "model": model_name,
- "messages": [
- {
- "role": "system",
- "content": _ANALYZE_SYSTEM_PROMPT,
- },
- {
- "role": "user",
- "content": analyze_prompt,
- "images": analysis_images,
- },
- ],
- "stream": False,
- "keep_alive": 0,
- "options": {
- "temperature": 0.2,
- "num_predict": 768,
- },
- }
- async with session.post(f"{base_url}/api/chat", json=chat_payload, timeout=300) as response:
- if response.status == 200:
- resp_json = await response.json()
- debug_candidates.extend(_collect_ollama_text_candidates(resp_json))
- chat_text = _extract_ollama_generated_text(resp_json)
- if _analysis_text_is_usable(chat_text):
- generated_text = chat_text
- if not _analysis_text_is_usable(generated_text):
- return web.json_response({
- "status": "error",
- "message": "Ollama returned only a truncated analysis response.",
- "debug_candidates": debug_candidates,
- "image_debug": image_debug,
- "image_lengths": [len(b64) for b64 in cleaned_b64_list],
- "description": generated_text,
- })
- else:
- # OpenAI-compatible vision chat (LM Studio / Custom).
- content = [{"type": "text", "text": analyze_prompt}]
- for b64 in cleaned_b64_list:
- content.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}})
- payload = {
- "model": model_name,
- "messages": [
- {"role": "system", "content": _ANALYZE_SYSTEM_PROMPT},
- {"role": "user", "content": content},
- ],
- "max_tokens": 4096, "stream": False,
- }
- async with session.post(f"{base_url}/v1/chat/completions", json=payload, timeout=120) as response:
- if response.status != 200:
- err_txt = await response.text()
- return web.json_response({"status": "error", "message": f"{provider} HTTP {response.status}: {err_txt}"})
- resp_json = await response.json()
- try:
- msg = resp_json["choices"][0]["message"]
- generated_text = (msg.get("content") or "").strip()
- # Reasoning/"thinking" models (Gemma, Qwen-thinking, etc.) may leave
- # content empty and put their output in reasoning_content instead.
- if not generated_text:
- generated_text = (msg.get("reasoning_content") or "").strip()
- except (KeyError, IndexError, TypeError):
- return web.json_response({"status": "error", "message": f"Unexpected response shape from {provider}."})
- except aiohttp.ClientConnectorError:
- return web.json_response({
- "status": "error",
- "message": f"Could not connect to {provider} at {base_url}. Make sure the server is running and reachable.",
- })
-
- if "" in generated_text:
- generated_text = generated_text.split("")[-1].strip()
-
- log.info("[LTXDirector] Analysis complete: %s", generated_text)
- return web.json_response({"status": "success", "description": generated_text})
-
- except Exception as e:
- log.error(f"[LTXDirector] Failed to analyze character: {e}")
- return web.json_response({"status": "error", "message": str(e)}, status=500)
-
-
-
-@PromptServer.instance.routes.post("/ltx_director/unload_ollama")
-async def unload_ollama_endpoint(request):
- """Evict the analysis model from VRAM right before a generation run, so the VLM doesn't
- compete with LTX for VRAM (the cause of intermittent CUDA offload crashes).
-
- Ollama supports a clean instant unload (keep_alive=0). LM Studio / Custom have no reliable
- cross-version HTTP unload, so for those this is a graceful no-op — users should set a short
- JIT / auto-unload TTL in their server instead. Fully tolerant: never raises into the run.
- """
- try:
- import aiohttp
- try:
- data = await request.json()
- except Exception:
- data = {}
- provider, base_url, model_name = _resolve_provider(data)
-
- if provider == "ollama":
- payload = {"model": model_name, "keep_alive": 0}
- try:
- async with aiohttp.ClientSession() as session:
- async with session.post(f"{base_url}/api/generate", json=payload, timeout=8) as response:
- await response.text()
- log.info("[LTXDirector] Asked Ollama to release '%s' from VRAM before the run.", model_name)
- except Exception:
- pass # Ollama not running / unreachable -> nothing to free.
- return web.json_response({"status": "ok", "provider": provider})
-
- # LM Studio / Custom: no reliable HTTP unload across versions -> graceful no-op.
- return web.json_response({"status": "ok", "provider": provider, "note": "no-op (set a JIT/TTL unload in your server)"})
- except Exception as e:
- return web.json_response({"status": "error", "message": str(e)})
-
-
def read_wav_peaks(wav_path):
import wave
with wave.open(wav_path, 'rb') as w:
@@ -1635,6 +1311,8 @@ def _build_reference_mode_outputs(
vae,
global_prompt: str,
local_prompts: str,
+ raw_global_prompt: str,
+ raw_local_prompts: str,
segment_lengths: str,
duration_frames: int,
epsilon: float,
@@ -1667,7 +1345,7 @@ def _build_reference_mode_outputs(
tensor = _resize_image(tensor, latent_w, latent_h, "stretch to fit", divisible_by)
return tensor
- prompt_text = (global_prompt or "") + " " + (local_prompts or "")
+ prompt_text = (raw_global_prompt or "") + " " + (raw_local_prompts or "")
tag_groups = _build_character_tag_groups(characters)
referenced_slots = [i for i, tags in enumerate(tag_groups) if any(t in prompt_text for t in tags)]
selected = []
@@ -2103,21 +1781,12 @@ class LTXDirector(io.ComfyNode):
image_cache=image_cache,
input_dir=input_dir,
)
- global_prompt = _prepend_character_descriptions(global_prompt, characters)
+ raw_global_prompt = global_prompt or ""
+ raw_local_prompts = local_prompts or ""
+ global_prompt = _append_character_references(raw_global_prompt, characters)
char_slot_images = [list(character.get("images") or []) for character in characters]
char_images = [img for slot_images in char_slot_images for img in slot_images]
-
- # --- @charN substitution ---
- # OFF: swap @char tags for their VLM descriptions in the prompt text.
- # Licon MSR: leave the tags raw — there the reference IMAGE drives identity, and the
- # tags are used only to pick which character slots feed the slideshow.
- if reference_mode == "Licon MSR (Prefix)":
- ref_global, ref_local = global_prompt, local_prompts
- else:
- ref_global, ref_local = _preprocess_prompts_with_characters(
- global_prompt, local_prompts, characters
- )
- global_prompt, local_prompts = ref_global, ref_local
+ local_prompts = raw_local_prompts
guide_data, derived_w, derived_h = _build_guide_data_from_timeline(
tdata=tdata,
@@ -2168,6 +1837,8 @@ class LTXDirector(io.ComfyNode):
vae=vae,
global_prompt=global_prompt,
local_prompts=local_prompts,
+ raw_global_prompt=raw_global_prompt,
+ raw_local_prompts=raw_local_prompts,
segment_lengths=segment_lengths,
duration_frames=duration_frames,
epsilon=epsilon,