Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47ea4cedc9 | ||
|
|
bb211a312e | ||
|
|
fc9b0b6c98 | ||
|
|
0c6ba0227f | ||
|
|
c1b7356be4 | ||
|
|
0b647966fd | ||
|
|
5a9a397a71 | ||
|
|
c578055671 | ||
|
|
7a60075cc8 | ||
|
|
949e9d545d | ||
|
|
4858988097 | ||
|
|
265a4e80ee | ||
|
|
094da69f6d | ||
|
|
aba5fb6c05 | ||
|
|
3aa790b773 | ||
|
|
de14afcb47 | ||
|
|
42b577c966 | ||
|
|
72935bb0db | ||
|
|
d774770ed6 | ||
|
|
bd04ca7e37 |
@@ -1,13 +1,24 @@
|
||||
|
||||

|
||||
|
||||
 [](https://github.com/ForeignGods/ComfyUI-Mana-Nodes/releases)
|
||||

|
||||
[](https://buymeacoffee.com/foreigngods)
|
||||
<!--
|
||||
<a href="https://github.com/ForeignGods/ComfyUI-Mana-Nodes/releases">
|
||||
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/ForeignGods/ComfyUI-Mana-Nodes/latest/total">
|
||||
</a>
|
||||
-->
|
||||
Welcome to the ComfyUI-Mana-Nodes project!
|
||||
|
||||
This collection of custom nodes is designed to supercharge text-based content creation within the ComfyUI environment.
|
||||
|
||||
Whether you're working on dynamic captions, transcribing audio, or crafting engaging visual content, Mana Nodes has got you covered.
|
||||
|
||||
If you like Mana Nodes, give our repo a [⭐ Star](https://github.com/ForeignGods/ComfyUI-Mana-Nodes) and [👀 Watch](https://github.com/ForeignGods/ComfyUI-Mana-Nodes/subscription) our repository to stay updated.
|
||||
|
||||
## Installation
|
||||
You can install these nodes via the [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager)
|
||||
Simply clone the repo into the `custom_nodes` directory with this command:
|
||||
You can install Mana Nodes via the [ComfyUI-Manager](https://github.com/ltdrdata/ComfyUI-Manager)
|
||||
|
||||
Or simply clone the repo into the `custom_nodes` directory with this command:
|
||||
|
||||
```
|
||||
git clone https://github.com/ForeignGods/ComfyUI-Mana-Nodes.git
|
||||
@@ -177,6 +188,9 @@ Used as input on ✒️ <b>Text to Image Generator</b> node to define the canvas
|
||||
<details>
|
||||
<summary>⏰ <b>Scheduled Values</b></summary>
|
||||
|
||||

|
||||
|
||||
|
||||
#### Required Inputs
|
||||
|
||||
#### `frame_count`
|
||||
@@ -431,7 +445,7 @@ Path to the created video file.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>📣 <b>Generate Audio</b></summary>
|
||||
<summary>📣 <b>Generate Audio</b> (experimental)</summary>
|
||||
|
||||
|
||||
Converts text to speech and saves the output as an audio file.
|
||||
@@ -503,9 +517,39 @@ Prefix for naming the text file. (relative to .\output)
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Example Workflows
|
||||
|
||||
### LCM AnimateDiff Text Animation
|
||||
|
||||
#### Demo
|
||||
|
||||
| Demo 1 | Demo 2 | Demo 3 |
|
||||
| ------ | ------ | ------ |
|
||||
||||
|
||||
|
||||
|
||||
#### Workflow
|
||||
|
||||
[example_workflow_1.json](example_workflows/example_workflow_1.json)
|
||||
|
||||
The values for the ⏰ Scheduled Values node cannot be imported yet (you have to add them yourself).
|
||||
|
||||

|
||||
|
||||
### Speech Recognition Caption Generator
|
||||
|
||||
#### Demo
|
||||
|
||||
Turn on audio.
|
||||
|
||||
https://github.com/ForeignGods/ComfyUI-Mana-Nodes/assets/78089013/e5a39327-db61-46ad-abea-10e27e4551c1
|
||||
|
||||
#### Workflow
|
||||
|
||||
[example_workflow_2.json](example_workflows/example_workflow_2.json)
|
||||
|
||||

|
||||
|
||||
## To-Do
|
||||
|
||||
- [ ] Improve Speech Recognition
|
||||
@@ -515,26 +559,12 @@ Prefix for naming the text file. (relative to .\output)
|
||||
- [ ] Text to Image Generator Alpha Channel
|
||||
- [ ] Add Font Support for non Latin Characters
|
||||
- [ ] 3D Effects, Bevel/Emboss, Inner Shading, Fade in/out
|
||||
|
||||
## Font Licences
|
||||
- <b>Personal Use:</b> The included fonts are for personal, non-commercial use. Please refrain from using these fonts in any commercial project without obtaining the appropriate licenses.
|
||||
- <b>License Compliance:</b> Each font may come with its own license agreement. It is the responsibility of the user to review and comply with these agreements. Some fonts may require a license for commercial use, modification, or distribution.
|
||||
- <b>Removing Fonts:</b> If any font creator or copyright holder wishes their font to be removed from this repository, please contact us, and we will promptly comply with your request.
|
||||
|
||||
### Font Links
|
||||
- https://www.dafont.com/akira-expanded.font
|
||||
- https://www.dafont.com/aurora-pro.font
|
||||
- https://www.dafont.com/another-danger.font
|
||||
- https://www.dafont.com/doctor-glitch.font
|
||||
- https://www.dafont.com/ghastly-panic.font
|
||||
- https://www.dafont.com/metal-gothic.font
|
||||
- https://www.dafont.com/the-constellation.font
|
||||
- https://www.dafont.com/the-augusta.font
|
||||
- https://www.dafont.com/vogue.font
|
||||
- https://www.dafont.com/wreckside.font
|
||||
- [ ] Find a better way to define color animations
|
||||
- [ ] Make more Font Properties animatable
|
||||
|
||||
## Contributing
|
||||
<a href="https://www.buymeacoffee.com/foreigngods" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||
|
||||
Your contributions to improve Mana Nodes are welcome! If you have suggestions or enhancements, feel free to fork this repository, apply your changes, and create a pull request. For significant modifications or feature requests, please open an issue first to discuss what you'd like to change.
|
||||
Your contributions to improve Mana Nodes are welcome!
|
||||
|
||||
If you have suggestions or enhancements, feel free to fork this repository, apply your changes, and create a pull request. For significant modifications or feature requests, please open an issue first to discuss what you'd like to change.
|
||||
|
||||
|
||||
+1259
-1194
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ import os
|
||||
from ..helpers.utils import tensor2pil
|
||||
from pathlib import Path
|
||||
import folder_paths
|
||||
import hashlib
|
||||
|
||||
class audio2video:
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ class font2img:
|
||||
"optional": {
|
||||
"transcription": ("TRANSCRIPTION", {"default": None,"forceInput": True}),
|
||||
"highlight_font": ("TEXT_GRAPHIC_ELEMENT", {"default": None,"forceInput": True}),
|
||||
"skip_first_frames": ("INT", {"default": 0, "min": 0, "step": 1, "display": "number"}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@ class font2img:
|
||||
formatted_transcription = self.format_transcription(kwargs)
|
||||
text = formatted_transcription
|
||||
else:
|
||||
formatted_transcription = None
|
||||
formatted_transcription = text
|
||||
|
||||
frame_text_dict, is_structured_input = self.parse_text_input(text, kwargs)
|
||||
frame_text_dict = self.cumulative_text(frame_text_dict, frame_count)
|
||||
@@ -111,7 +112,7 @@ class font2img:
|
||||
sentence_frame_numbers = []
|
||||
|
||||
for i, (word, start_time, end_time) in enumerate(transcription_data):
|
||||
frame_number = round(start_time * transcription_fps)
|
||||
frame_number = 1 + round(start_time * transcription_fps)
|
||||
|
||||
if not current_sentence:
|
||||
current_sentence = word
|
||||
@@ -660,7 +661,8 @@ class font2img:
|
||||
structured_format = False
|
||||
frame_text_dict = {}
|
||||
frame_count = kwargs['frame_count']
|
||||
|
||||
skip_first_frames = kwargs.get('skip_first_frames', 0)
|
||||
|
||||
# Filter out empty lines
|
||||
lines = [line for line in text.split('\n') if line.strip()]
|
||||
|
||||
@@ -670,7 +672,7 @@ class font2img:
|
||||
for line in lines:
|
||||
parts = line.split(':', 1)
|
||||
if len(parts) == 2:
|
||||
frame_number = parts[0].strip().replace('"', '')
|
||||
frame_number = str(int(parts[0].strip().replace('"', ''))-skip_first_frames)
|
||||
text = parts[1].strip().replace('"', '').replace(',', '')
|
||||
frame_text_dict[frame_number] = text
|
||||
else:
|
||||
@@ -737,4 +739,4 @@ class font2img:
|
||||
return Image.new('RGB', (image_width, image_height), color=background_color_tuple)
|
||||
else:
|
||||
background_color_tuple = ImageColor.getrgb(background_color)
|
||||
return Image.new('RGB', (image_width, image_height), color=background_color_tuple)
|
||||
return Image.new('RGB', (image_width, image_height), color=background_color_tuple)
|
||||
|
||||
@@ -31,8 +31,8 @@ class scheduled_values:
|
||||
"easing_type": (easing_types, {"default": "linear", "display": "dropdown"}),
|
||||
"step_mode": (step_mode, {"default": "single", "display": "dropdown"}),
|
||||
"animation_reset": (animation_reset, {"default": "word", "display": "dropdown"}),
|
||||
"id": ("INT", {"default": 0, "step": 1, "display": "number"}),
|
||||
"scheduled_values": ("STRING", {"default": "[]", "display": "text","readOnly": True }),
|
||||
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID",
|
||||
@@ -48,7 +48,6 @@ class scheduled_values:
|
||||
def run(self, **kwargs):
|
||||
scheduled_values = str(kwargs['scheduled_values'])
|
||||
animation_reset = kwargs.get('animation_reset')
|
||||
|
||||
# this should be ok but maybe change it
|
||||
if scheduled_values == '[]':
|
||||
raise ValueError("scheduled_values is required and cannot be an empty list.")
|
||||
|
||||
@@ -18,7 +18,7 @@ class speech2text:
|
||||
return {
|
||||
"required": {
|
||||
"audio_file": ("STRING", {"display": "text","forceInput": True}),
|
||||
"wav2vec2_model": (cls.get_wav2vec2_models(), {"display": "dropdown", "default": "ailegends/xlsr-jonatasgrosman-wav2vec2-large-xlsr-53-english"}),
|
||||
"wav2vec2_model": (cls.get_wav2vec2_models(), {"display": "dropdown", "default": "jonatasgrosman/wav2vec2-large-xlsr-53-english"}),
|
||||
"spell_check_language": (spell_check_options, {"default": "English", "display": "dropdown"}),
|
||||
"framestamps_max_chars": ("INT", {"default": 25, "step": 1, "display": "number"}),
|
||||
"fps": ("INT", {"default": 30, "min": 1, "max": 60, "step": 1}),
|
||||
|
||||
@@ -28,11 +28,9 @@ class string2file:
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def run(self, string, filename_prefix,unique_id = None, extra_pnginfo=None):
|
||||
full_path = os.path.join(folder_paths.get_output_directory(), os.path.normpath(filename_prefix[0]))
|
||||
if not full_path.endswith('.txt'):
|
||||
full_path += '.txt'
|
||||
Path(os.path.dirname(full_path)).mkdir(parents=True, exist_ok=True)
|
||||
def run(self, string,unique_id = None, extra_pnginfo=None, **kwargs):
|
||||
|
||||
full_path = self.construct_text_path(kwargs)
|
||||
|
||||
# Write the string to the file
|
||||
with open(full_path, 'w') as file:
|
||||
@@ -44,4 +42,26 @@ class string2file:
|
||||
if node:
|
||||
node["widgets_values"] = [string]
|
||||
|
||||
return {"ui": {"text": string}, "result": (string,)}
|
||||
return {"ui": {"text": string}, "result": (string,)}
|
||||
|
||||
def construct_text_path(self, kwargs):
|
||||
base_directory = folder_paths.get_output_directory()
|
||||
filename_prefix = os.path.normpath(kwargs['filename_prefix'][0])
|
||||
full_path = os.path.join(base_directory, filename_prefix)
|
||||
|
||||
# Ensure the path ends with .mp4
|
||||
if not full_path.endswith('.txt'):
|
||||
full_path += '.txt'
|
||||
|
||||
# Increment filename if it already exists
|
||||
counter = 1
|
||||
while os.path.exists(full_path):
|
||||
# Construct a new path with an incremented number
|
||||
new_filename = f"{filename_prefix}_{counter}.txt"
|
||||
full_path = os.path.join(base_directory, new_filename)
|
||||
counter += 1
|
||||
|
||||
# Create the directory if it does not exist
|
||||
Path(os.path.dirname(full_path)).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
return full_path
|
||||
@@ -51,11 +51,11 @@ class text_graphic_element:
|
||||
"required": {
|
||||
"font_file": (cls.FONT_NAMES, {"default": cls.FONT_NAMES[0]}),
|
||||
"font_size": ("INT", {"default": 75, "min": 1, "step": 1, "display": "number"}),
|
||||
"font_color": ("STRING", {"default": "red", "display": "text"}),
|
||||
"font_color": ("STRING", {"default": "white", "display": "text"}),
|
||||
"kerning": ("INT", {"default": 0, "step": 1, "display": "number"}),
|
||||
"border_width": ("INT", {"default": 0, "min": 0, "step": 1, "display": "number"}),
|
||||
"border_color": ("STRING", {"default": "red", "display": "text"}),
|
||||
"shadow_color": ("STRING", {"default": "red", "display": "text"}),
|
||||
"border_color": ("STRING", {"default": "grey", "display": "text"}),
|
||||
"shadow_color": ("STRING", {"default": "grey", "display": "text"}),
|
||||
"shadow_offset_x": ("INT", {"default": 0, "min": 0, "step": 1, "display": "number"}),
|
||||
"shadow_offset_y": ("INT", {"default": 0, "min": 0, "step": 1, "display": "number"}),
|
||||
"x_offset": ("INT", {"default": 0, "step": 1, "display": "number"}),
|
||||
|
||||
+38
-16
@@ -38,7 +38,7 @@ function chainCallback(object, property, callback) {
|
||||
}
|
||||
|
||||
class TimelineWidget {
|
||||
constructor(node) {
|
||||
constructor(node, id) {
|
||||
this.node = node;
|
||||
this.keyframes = [];
|
||||
this.widgets = node.widgets;
|
||||
@@ -50,6 +50,7 @@ class TimelineWidget {
|
||||
this.generateButton = null;
|
||||
this.deleteButton = null;
|
||||
this.generatedKeyframes = [];
|
||||
this.id = id;
|
||||
this.createChartContainer();
|
||||
}
|
||||
|
||||
@@ -57,7 +58,9 @@ class TimelineWidget {
|
||||
this.chartContainer = document.createElement('div');
|
||||
this.chartContainer.style.height = '200px';
|
||||
this.chartContainer.style.width = '200px';
|
||||
this.node.addDOMWidget("Chart", "custom", this.chartContainer, {});
|
||||
|
||||
this.node.addDOMWidget("chart", "custom", this.chartContainer, {});
|
||||
|
||||
}
|
||||
|
||||
updateGenerateButtonState() {
|
||||
@@ -351,8 +354,11 @@ class TimelineWidget {
|
||||
showLine: true
|
||||
}
|
||||
];
|
||||
localStorage.setItem('savedGeneratedKeyframes', JSON.stringify(this.generatedKeyframes));
|
||||
// Update chart with new data
|
||||
|
||||
|
||||
|
||||
localStorage.setItem('savedGeneratedKeyframes_'+ this.id, JSON.stringify(this.generatedKeyframes));
|
||||
|
||||
this.chart.update();
|
||||
|
||||
// Refresh the points display and buttons
|
||||
@@ -465,7 +471,8 @@ class TimelineWidget {
|
||||
}
|
||||
|
||||
this.updateGenerateButtonState();
|
||||
localStorage.setItem('savedKeyframes', JSON.stringify(this.keyframes));
|
||||
localStorage.setItem('savedKeyframes_' + this.id, JSON.stringify(this.keyframes));
|
||||
|
||||
}
|
||||
|
||||
calculateValuesFromClick(event, canvas) {
|
||||
@@ -628,23 +635,35 @@ class TimelineWidget {
|
||||
app.registerExtension({
|
||||
name: "ManaNodes.scheduled_values",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name === "scheduled_values") {
|
||||
if (nodeData.name === "Scheduled Values") {
|
||||
|
||||
// Restoring state in onConfigure
|
||||
chainCallback(nodeType.prototype, "onConfigure", function () {
|
||||
const savedKeyframes = JSON.parse(localStorage.getItem('savedKeyframes'));
|
||||
const savedGeneratedKeyframes = JSON.parse(localStorage.getItem('savedGeneratedKeyframes'));
|
||||
|
||||
const id_widget = this.widgets.find(w => w.name === "id");
|
||||
if (id_widget.value == 0) {
|
||||
|
||||
let max = 1000000;
|
||||
id_widget.value = Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
this.timelineWidget.id = id_widget.value;
|
||||
const x = this.widgets.find(w => w.name === "frame_count").value;
|
||||
const y = this.widgets.find(w => w.name === "value_range").value;
|
||||
this.timelineWidget.maxX = x;
|
||||
this.timelineWidget.maxY = y;
|
||||
this.timelineWidget.updateTicks(x, y);
|
||||
|
||||
const step_size = this.widgets.find(w => w.name === "step_mode").value;
|
||||
this.timelineWidget.updateStepSize(step_size);
|
||||
|
||||
const savedKeyframes = JSON.parse(localStorage.getItem('savedKeyframes_' + id_widget.value));
|
||||
const savedGeneratedKeyframes = JSON.parse(localStorage.getItem('savedGeneratedKeyframes_' + id_widget.value));
|
||||
|
||||
if (savedKeyframes) {
|
||||
console.log('gesavedKeyframeslp',savedKeyframes);
|
||||
this.timelineWidget.keyframes = savedKeyframes;
|
||||
}
|
||||
|
||||
if (savedGeneratedKeyframes) {
|
||||
console.log('savedGeneratedKeyframes',savedGeneratedKeyframes);
|
||||
this.timelineWidget.generatedKeyframes = savedGeneratedKeyframes;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@@ -654,19 +673,23 @@ app.registerExtension({
|
||||
|
||||
let maxX = frame_count_widget ? parseInt(frame_count_widget.value, 10) : 20;
|
||||
let valueRange = value_range_widget ? parseInt(value_range_widget.value, 10) : 100;
|
||||
|
||||
|
||||
const timelineWidget = new TimelineWidget(this);
|
||||
loadChartJs(() => {
|
||||
timelineWidget.initChart(maxX, valueRange);
|
||||
});
|
||||
loadBootstrapCss();
|
||||
this.timelineWidget = timelineWidget;
|
||||
let max = 1000000;
|
||||
this.timelineWidget.id = Math.floor(Math.random() * max);
|
||||
this.widgets.find(w => w.name === "id").value = this.timelineWidget.id;
|
||||
|
||||
});
|
||||
|
||||
chainCallback(nodeType.prototype, 'onDrawBackground', function () {
|
||||
const frame_count_widget = this.widgets.find(w => w.name === "frame_count");
|
||||
const value_range_widget = this.widgets.find(w => w.name === "value_range");
|
||||
|
||||
let maxX = frame_count_widget ? parseInt(frame_count_widget.value, 10) : 20;
|
||||
let valueRange = value_range_widget ? parseInt(value_range_widget.value, 10) : 100;
|
||||
const step_size_widget = this.widgets.find(w => w.name === "step_mode");
|
||||
@@ -687,9 +710,8 @@ app.registerExtension({
|
||||
this.stepSize = stepSize;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (this.timelineWidget) {
|
||||
this.widgets.find(w => w.name === "id").value = this.timelineWidget.id;
|
||||
// Combine keyframes and generatedKeyframes.
|
||||
let combinedKeyframes = [...this.timelineWidget.keyframes, ...this.timelineWidget.generatedKeyframes];
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ComfyWidgets } from "../../../scripts/widgets.js";
|
||||
app.registerExtension({
|
||||
name: "ManaNodes.string2file",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "string2file") {
|
||||
if (nodeData.name === "Save/Preview Text") {
|
||||
function populate(values) {
|
||||
|
||||
let previewText;
|
||||
|
||||
@@ -247,7 +247,7 @@ app.registerExtension({
|
||||
});
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name !== "audio2video") {
|
||||
if (nodeData.name !== "Combine Video") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user