Files
igrzf12 c4001f8d53 feat(server): implement edge-tts and improve timeline functionality
- Add EdgeTTSNode for text-to-speech integration
- Implement MontagenTimeRange for better time range management
- Update LGraphNode to support new properties and methods
- Enhance LGraphNodeItem with item_index and set_time functionality
- Modify MontagenMaterial to handle remote and local assets
- Refactor MontagenTime class with additional properties and methods
- Improve timeline sorting and synchronization
- Update Utils with image download and processing functions
2025-04-28 09:33:00 +08:00

581 lines
18 KiB
Python

from __future__ import annotations
import io, os
import random
import shutil
from .videosave import save_video
from .Utils import (
to_base36_random,
create_default_option,
GIFTYPE,
IMAGETYPE,
VIDEOTYPE,
TEXTTYPE,
extract_middle_frame_thumbnail,
extract_gif_middle_frame,
extract_image_thumbnail,
flat_to_tree,
tree_to_flat,
supported_group_config_type,
SYNCACION,
MODIFYACTION,
BYPASSACTION,
)
from typing import TYPE_CHECKING
from .LGraphNodeItem import LGraphNodeItem
from .MontagenTimeRange import MontagenTimeRange
if TYPE_CHECKING:
from .LGraph import LGraph
class LGraphNodeInput:
def __init__(self, data):
self.data = data
def serialize(self):
return self.data
@property
def name(self):
return self.data["name"]
@property
def type(self):
return self.data["type"]
@property
def link(self):
return self.data["link"]
@link.setter
def link(self, value):
self.data["link"] = value
class LGraphNodeOutput:
def __init__(self, data):
self.data = data
def serialize(self):
return self.data
@property
def name(self):
return self.data["name"]
@property
def type(self):
return self.data["type"]
@property
def links(self):
return self.data["links"]
@links.setter
def links(self, value):
self.data["links"] = value
class LGraphNode:
def __init__(self, graph: LGraph, data: dict[str, any]):
self.graph = graph
self.data = data
self._default_opt = None
def serialize(self):
return self.data
@property
def widgets(self) -> list[any]:
return self.data.get("widgets_values")
@property
def id(self):
return self.data["id"]
@property
def node_id(self):
return (
f"{self.id}_{self.graph.montagen_workflow_id}"
if self.graph.montagen_workflow_id
else self.id
)
@property
def properties(self) -> dict[str, any]:
if "properties" not in self.data:
self.properties = {}
return self.data.get("properties", {})
@properties.setter
def properties(self, value: dict[str, any]):
self.data["properties"] = value
@property
def meta(self):
if "outputs" not in self.properties:
self.properties["outputs"] = {}
if "meta" not in self.properties["outputs"]:
self.properties["outputs"]["meta"] = {}
return self.properties.get("outputs", {}).get("meta", {})
@property
def assets(self) -> list:
if "outputs" not in self.properties:
self.properties["outputs"] = {}
if "assets" not in self.properties["outputs"]:
self.properties["outputs"]["assets"] = []
return self.properties.get("outputs", {}).get("assets", [])
@assets.setter
def assets(self, value: list):
if "outputs" not in self.properties:
self.properties["outputs"] = {}
self.properties["outputs"]["assets"] = value
@property
def default_opt(self):
if not self._default_opt:
self._default_opt = create_default_option(self.type)
return self._default_opt
@property
def z_index(self):
index = self.properties.get("zIndex", None)
if index:
return index
index = 0
if self.type == TEXTTYPE:
index = 3000
index = index + random.randint(0, 1000)
self.properties["zIndex"] = index
return index
@property
def timerange(self) -> MontagenTimeRange:
if "outputs" not in self.properties:
self.properties["outputs"] = {}
if "timerange" not in self.properties["outputs"]:
self.properties["outputs"]["timerange"] = []
return MontagenTimeRange(
self.properties.get("outputs", {}).get("timerange", [])
)
@property
def items(self):
return [LGraphNodeItem(self, i, item) for i, item in enumerate(self.items_raw)]
@property
def items_raw(self) -> list[dict[str, any]]:
if "outputs" not in self.properties:
self.properties["outputs"] = {}
if "items" not in self.properties["outputs"]:
self.properties["outputs"]["items"] = []
list: list[dict[str, any]] = self.properties.get("outputs", {}).get("items", [])
list.sort(key=lambda x: LGraphNodeItem(self, 0, x).item_index)
return list
@items.setter
def items(self, value: list[LGraphNodeItem]):
if "outputs" not in self.properties:
self.properties["outputs"] = {}
self.properties["outputs"]["items"] = [item.serialize() for item in value]
@property
def single_file_name(self):
if self.single_asset:
return self.single_asset.get("file_name")
return None
@property
def single_asset(self):
if self.assets:
return self.assets[0]
return None
@single_asset.setter
def single_asset(self, value):
self.assets = [value]
@property
def single_item(self):
if self.items:
return self.items[0]
return None
@single_item.setter
def single_item(self, value: LGraphNodeItem):
self.items = [value]
@property
def type(self) -> str:
return self.properties.get("montagen_type", None)
@type.setter
def type(self, value: str):
self.properties["montagen_type"] = value
@property
def node_type(self) -> str:
return self.properties.get("montagen_node_type", None)
@node_type.setter
def node_type(self, value: str):
self.properties["montagen_node_type"] = value
@property
def timeline_name(self) -> str:
return self.properties.get("montagen_timeline_name", None)
@timeline_name.setter
def timeline_name(self, value: str):
self.properties["montagen_timeline_name"] = value
@property
def reserve_file(self) -> bool:
return self.properties.get("reserve_file", False)
@reserve_file.setter
def reserve_file(self, value: bool):
self.properties["reserve_file"] = value
@property
def is_montagen_node(self):
return self.type is not None
@property
def node_name(self) -> str:
return self.properties.get("montagen_name", None)
@node_name.setter
def node_name(self, value: str):
self.properties["montagen_name"] = value
@property
def inputs(self):
if "inputs" not in self.data:
self.data["inputs"] = []
return [LGraphNodeInput(input) for input in self.data["inputs"]]
@property
def outputs(self):
if "outputs" not in self.data:
self.data["outputs"] = []
return [LGraphNodeOutput(output) for output in self.data["outputs"]]
@property
def workflow(self):
return self.graph.owner_workflow
def reset(self):
if self.is_montagen_node:
self.assets = []
self.items = []
def to_json(self):
flat_dict = tree_to_flat(self.meta, supported_group_config_type[self.type])
return {
"id": self.node_id,
"workflowId": self.workflow.workflow_id,
"name": self.node_name,
"type": self.type,
"nodeType": self.node_type,
"assets": self.assets,
"clips": [clip.to_json() for item in self.items for clip in item.clips],
"meta": {
key: flat_dict.get(key, value[1].get("default"))
for key, value in supported_group_config_type[self.type].items()
},
}
def to_timeline_json(self):
flat_dict = tree_to_flat(self.meta, supported_group_config_type[self.type])
return {
"id": self.node_id,
"workflowId": self.workflow.workflow_id,
"name": self.node_name,
"type": self.type,
"nodeType": self.node_type,
"assets": self.assets,
"meta": {
key: flat_dict.get(key, value[1].get("default"))
for key, value in supported_group_config_type[self.type].items()
},
"clips": [],
}
def has_filename(self, file_name):
for asset in self.assets:
if asset["file_name"] == file_name:
return True
return False
def set_file_output(self, file_output_index):
if file_output_index >= 0:
self.widgets[file_output_index] = self.single_file_name
def sync_time_resoureces_range(
self, time_range: MontagenTimeRange, resoureces: list[str], action: str
):
if action == BYPASSACTION:
return
used_item_ids = set()
index = 0
for time_unit in time_range.time_range:
item_id = time_unit.id
item, item_index = self.Get_item_and_index(item_id)
if not time_unit.is_selected:
if item:
item.set_time(time_unit.start, time_unit.duration)
used_item_ids.add(item.item_id)
continue
if not item:
item_index = len(self.items)
old_file = (
None if self.reserve_file else self.get_asset_file_name(item_index)
)
material, src = self.workflow.workflow_add_material(
self.node_name, item_index, resoureces[index], self.type
)
self.set_asset(item_index, material)
if not item:
item = self.create_item()
item.item_index = time_unit.index
item.item_id = item_id
self.items_raw.append(item.serialize())
item.set_main_content(
src, time_unit.start, time_unit.duration, meta=self.meta, flush=True
)
self.workflow.workflow_del_material(old_file)
index += 1
used_item_ids.add(item.item_id)
if action == SYNCACION:
delete_items = [
item for item in self.items if item.item_id not in used_item_ids
]
self.items = [item for item in self.items if item.item_id in used_item_ids]
for item in delete_items:
item.delete()
def sync_time_text_range(self, time_range: MontagenTimeRange, action: str):
if action == BYPASSACTION:
return
used_item_ids = set()
for time_unit in time_range.time_range:
item_id = time_unit.id
item, item_index = self.Get_item_and_index(item_id)
if not time_unit.is_selected:
if item:
item.set_time(time_unit.start, time_unit.duration)
used_item_ids.add(item.item_id)
continue
if not item:
item = self.create_item()
item.item_index = time_unit.index
item.item_id = item_id
self.items_raw.append(item.serialize())
item.set_main_content(
time_unit.content,
time_unit.start,
time_unit.duration,
meta=self.meta,
flush=True,
)
used_item_ids.add(item.item_id)
if action == SYNCACION:
delete_items = [
item for item in self.items if item.item_id not in used_item_ids
]
self.items = [item for item in self.items if item.item_id in used_item_ids]
for item in delete_items:
item.delete()
def sync_time_images_range(
self,
time_range: MontagenTimeRange,
images: list,
action: str,
):
if action == BYPASSACTION:
return
used_item_ids = set()
index = 0
for time_unit in time_range.time_range:
item_id = time_unit.id
item, item_index = self.Get_item_and_index(item_id)
if not time_unit.is_selected:
if item:
item.set_time(time_unit.start, time_unit.duration)
used_item_ids.add(item.item_id)
continue
if not item:
item_index = len(self.items)
old_file = (
None if self.reserve_file else self.get_asset_file_name(item_index)
)
(file_fullName, tmp_fullName) = self.get_output_path(item_index, "png")
images[index].save(file_fullName)
material, src = self.workflow.workflow_add_material(
self.node_name, item_index, file_fullName, self.type
)
self.set_asset(item_index, material)
if not item:
item = self.create_item()
item.item_index = time_unit.index
item.item_id = item_id
self.items_raw.append(item.serialize())
meta = {"desc": time_unit.content}
item.set_main_content(
src,
time_unit.start,
time_unit.duration,
meta=self.meta,
addition_meta=meta,
flush=True,
)
self.workflow.workflow_del_material(old_file)
index += 1
used_item_ids.add(item.item_id)
if action == SYNCACION:
delete_items = [
item for item in self.items if item.item_id not in used_item_ids
]
self.items = [item for item in self.items if item.item_id in used_item_ids]
for item in delete_items:
item.delete()
def sync_file_meta(self, file_meta: dict):
old_name = self.single_file_name
self.single_asset = file_meta
src = file_meta.get("src")
self.create_single_item_if_not_exists(src)
if old_name != self.single_file_name and not self.reserve_file and old_name:
self.workflow.workflow_del_material(old_name)
def sync_file_images(self, property: dict, images: list):
format = property.get("format")
pbar = property.get("pbar")
fps = property.get("fps")
hasAlpha = property.get("hasAlpha")
(file_fullName, tmp_fullName) = self.get_output_path(0, format)
if pbar:
save_video(tmp_fullName, images, fps, pbar, hasAlpha)
pbar.update_absolute(100)
elif self.type == GIFTYPE:
duration = 1 / fps * 1000
images[0].save(
tmp_fullName,
save_all=True,
append_images=images[1:],
duration=duration,
loop=0,
disposal=2,
)
else:
images[0].save(tmp_fullName)
if os.path.exists(tmp_fullName):
src, old_file = self.copy_output(tmp_fullName, file_fullName, 0)
self.create_single_item_if_not_exists(src)
self.workflow.workflow_del_material(old_file)
def sync_file_buffer(self, property: dict, buffer: io.BytesIO):
format = property.get("format")
(file_fullName, tmp_fullName) = self.get_output_path(0, format)
with open(tmp_fullName, "wb") as f:
f.write(buffer.getbuffer())
if os.path.exists(tmp_fullName):
src, old_file = self.copy_output(tmp_fullName, file_fullName, 0)
self.create_single_item_if_not_exists(src)
self.workflow.workflow_del_material(old_file)
def sync_file_text(self, text: str):
self.create_single_item_if_not_exists(text)
def get_output_path(self, index: int, ext: str):
if not self.workflow:
raise RuntimeError("No workflow loaded")
return self.workflow.get_output_path(self.node_id, index, ext)
def copy_output(self, tmpFullName: str, fileFullName: str, index: int):
shutil.move(tmpFullName, fileFullName)
old_file = None if self.reserve_file else self.get_asset_file_name(index)
material, src = self.workflow.workflow_add_material(
self.node_name, index, fileFullName, self.type
)
self.set_asset(index, material)
return src, old_file
def get_asset_file_name(self, index: int) -> str:
assets_len = len(self.assets)
if index >= assets_len:
return None
asset = self.assets[index]
if not asset:
return None
return asset.get("file_name")
def get_asset(self, index: int) -> dict:
assets_len = len(self.assets)
if index >= assets_len:
return None
return self.assets[index]
def set_asset(self, index: int, file_meta: dict):
assets_len = len(self.assets)
while index >= assets_len:
self.assets.append(None)
assets_len = len(self.assets)
self.assets[index] = file_meta
def create_single_item_if_not_exists(self, main_content: str):
if not self.single_item:
item = self.create_item()
self.single_item = item
self.single_item.set_main_content(main_content, meta=self.meta)
def create_item(self):
item_id = to_base36_random()
item = LGraphNodeItem(
self,
0,
{
"meta": {"zIndex": self.z_index},
"default": self.default_opt,
},
)
item.item_id = item_id
item.item_index = 0
return item
def Get_item_and_index(self, item_id: str):
for index, item in enumerate(self.items):
if item.item_id == item_id:
return item, index
return None, None
def thumb(self):
if self.single_file_name:
full_path = (
self.graph.owner_project.montagen_material.get_material_full_path(
self.single_asset
)
)
out_put = self.graph.owner_workflow.get_thumb_file_path()
if self.type == GIFTYPE:
extract_gif_middle_frame(full_path, out_put)
return
elif self.type == VIDEOTYPE:
extract_middle_frame_thumbnail(full_path, out_put)
return
elif self.type == IMAGETYPE:
extract_image_thumbnail(full_path, out_put)
return
raise NotImplementedError()
def syn_meta(self, meta: dict):
opt = flat_to_tree(meta, supported_group_config_type[self.type])
self.meta.update(opt)