20 Commits
Author SHA1 Message Date
Pascal Rössler 47ea4cedc9 Merge pull request #27 from Quasimondo/main
Update font2img_node.py
2024-05-29 20:29:05 +02:00
Mario Klingemann bb211a312e Update font2img_node.py
Added optional skip_first_frames to allow for batch processing of longer videos
2024-05-29 19:05:27 +02:00
Pascal Rössler fc9b0b6c98 Merge pull request #24 from Quasimondo/patch-1
Update font2img_node.py
2024-05-21 20:50:40 +02:00
Mario Klingemann 0c6ba0227f Update font2img_node.py
Fixed calculation of frame_number so the first frame starts at 1 (instead of 0)
2024-05-21 17:09:07 +02:00
Pascal Rössler c1b7356be4 Update README.md 2024-05-12 22:43:17 +02:00
Pascal Rössler 0b647966fd Merge pull request #22 from ForeignGods/dev
incremental textfile prefix save
2024-05-12 22:42:17 +02:00
Pascal Rössler 5a9a397a71 incremental textfile prefix save 2024-05-12 22:42:04 +02:00
Pascal Rössler c578055671 Update README.md 2024-04-28 19:33:12 +02:00
Pascal Rössler 7a60075cc8 Update README.md 2024-04-28 19:31:40 +02:00
Pascal Rössler 949e9d545d Merge pull request #20 from ForeignGods/dev
JS corrections, updated example
2024-04-28 19:26:27 +02:00
Pascal Rössler 4858988097 JS corrections, updated example 2024-04-28 19:26:10 +02:00
Pascal Rössler 265a4e80ee Update README.md 2024-04-28 19:23:57 +02:00
Pascal Rössler 094da69f6d Merge pull request #19 from ForeignGods/dev
fix js node names
2024-04-28 12:30:30 +02:00
Pascal Rössler aba5fb6c05 fix js node names 2024-04-28 12:30:10 +02:00
Pascal Rössler 3aa790b773 Update README.md 2024-04-27 17:32:22 +02:00
Pascal Rössler de14afcb47 Update README.md 2024-04-27 17:30:39 +02:00
Pascal Rössler 42b577c966 Update README.md 2024-04-27 17:20:03 +02:00
Pascal Rössler 72935bb0db Update README.md 2024-04-27 17:11:01 +02:00
Pascal Rössler d774770ed6 Update README.md 2024-04-27 16:57:57 +02:00
Pascal Rössler bd04ca7e37 Update README.md 2024-04-27 16:56:20 +02:00
12 changed files with 2224 additions and 1840 deletions
+55 -25
View File
@@ -1,13 +1,24 @@
![ezgif com-optimize(2)](https://github.com/ForeignGods/ComfyUI-Mana-Nodes/assets/78089013/f48b37c2-c3db-408f-ada8-a6bf336b6549)
![Static Badge](https://img.shields.io/badge/release-v1.0-black?style=plastic&logo=GitHub&logoColor=white&color=green) [![Downloads](https://img.shields.io/github/downloads/ForeignGods/ComfyUI-Mana-Nodes/total.svg)](https://github.com/ForeignGods/ComfyUI-Mana-Nodes/releases)
![Static Badge](https://img.shields.io/badge/release-v1.0.0-black?style=plastic&logo=GitHub&logoColor=white&color=green)
[![Custom Badge](https://img.shields.io/badge/buy-coffe-orange?style=plastic&logo=buymeacoffee&logoColor=white&link=URL)](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>
![Screenshot 2024-04-27 at 17-07-10 ComfyUI](https://github.com/ForeignGods/ComfyUI-Mana-Nodes/assets/78089013/ee456e65-9950-4138-8b37-23b007ec92d9)
#### 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 |
| ------ | ------ | ------ |
|![demo1](https://github.com/ForeignGods/ComfyUI-Mana-Nodes/assets/78089013/7b77b9cc-457f-4061-ac6c-2f78efb8bffc)|![demo2](https://github.com/ForeignGods/ComfyUI-Mana-Nodes/assets/78089013/89bc4309-6c46-4d08-9d9c-521e00415e65)|![demo3](https://github.com/ForeignGods/ComfyUI-Mana-Nodes/assets/78089013/ae2e09c5-459c-4b4d-ad71-4db31684573f)|
#### 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).
![Screenshot 2024-04-28 at 19-18-01 ComfyUI](https://github.com/ForeignGods/ComfyUI-Mana-Nodes/assets/78089013/fa739ab0-91e5-4df7-9bd9-727abb6fb86a)
### 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)
![TRANSCRIPTION](https://github.com/ForeignGods/ComfyUI-Mana-Nodes/assets/78089013/e4d6aa73-3a4b-483e-b763-73b88c8cb261)
## 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.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -6,7 +6,6 @@ import os
from ..helpers.utils import tensor2pil
from pathlib import Path
import folder_paths
import hashlib
class audio2video:
+7 -5
View File
@@ -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)
+1 -2
View File
@@ -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.")
+1 -1
View File
@@ -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}),
+26 -6
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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];
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -247,7 +247,7 @@ app.registerExtension({
});
},
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "audio2video") {
if (nodeData.name !== "Combine Video") {
return;
}