OGG-Comfy / tests /make_workflows.py
supermind's picture
Upload 58 files
4d1131c verified
Raw History Blame Contribute Delete
15.3 kB
import os as _os
HERE = _os.path.dirname(_os.path.abspath(__file__))
ROOT = _os.path.dirname(HERE)
import json, uuid
def inp(name, typ, widget=False, optional=False):
return {"name": name, "type": typ, "widget": widget, "optional": optional}
# node definitions: inputs in the order of the schemas in nodes.py, outputs
DEFS = {
"OggLoader": ([inp("model_name","COMBO",True), inp("dtype","COMBO",True)], [("OGG_MODEL","OGG_MODEL")]),
"OggCodecLoader": ([inp("codec_name","COMBO",True)], [("OGG_CODEC","OGG_CODEC")]),
"OggGenerate": ([inp("model","OGG_MODEL"), inp("text","STRING",True), inp("language","COMBO",True), inp("seed","INT",True),
inp("steps","INT",True), inp("cfg","FLOAT",True), inp("temperature","FLOAT",True), inp("split_lines","BOOLEAN",True),
inp("max_frames","INT",True), inp("speaker","STRING",True), inp("quality","STRING",True),
inp("mtp_temperature","FLOAT",True), inp("mtp_top_k","INT",True), inp("mtp_rounds","INT",True),
inp("voice","OGG_VOICE",optional=True)], [("OGG_CODES","OGG_CODES")]),
"OggDialogue": ([inp("model","OGG_MODEL"), inp("script","STRING",True), inp("language","COMBO",True), inp("mode","COMBO",True),
inp("seed","INT",True), inp("steps","INT",True), inp("cfg","FLOAT",True), inp("temperature","FLOAT",True),
inp("max_frames","INT",True)] + [inp(f"name_{i}","STRING",True) for i in range(1,7)] +
[inp("quality","STRING",True), inp("mtp_temperature","FLOAT",True), inp("mtp_top_k","INT",True), inp("mtp_rounds","INT",True), inp("dialogue_layout","COMBO",True,optional=True)] + [inp(f"speaker_{i}","STRING",True,optional=True) for i in range(1,7)] +
[inp(f"voice_{i}","OGG_VOICE",optional=True) for i in range(1,7)], [("OGG_CODES","OGG_CODES")]),
"OggVoiceFromTake": ([inp("codes","OGG_CODES"), inp("take","INT",True), inp("strip_tags","BOOLEAN",True)], [("OGG_VOICE","OGG_VOICE")]),
"OggDecode": ([inp("codec","OGG_CODEC"), inp("codes","OGG_CODES"), inp("shorten_long_pauses","BOOLEAN",True)], [("AUDIO","AUDIO")]),
"SaveAudio": ([inp("audio","AUDIO"), inp("filename_prefix","STRING",True), inp("audioUI","AUDIO_UI",True)], [("audio","AUDIO")]),
"LoadAudio": ([inp("audio","COMBO",True), inp("audioUI","AUDIO_UI",True), inp("upload","AUDIOUPLOAD",True)], [("AUDIO","AUDIO")]),
"OggQwen3ASRLoader": ([inp("model_name","COMBO",True)], [("OGG_ASR_MODEL","OGG_ASR_MODEL")]),
"OggQwen3ASRTranscribe": ([inp("model","OGG_ASR_MODEL"), inp("audio","AUDIO"), inp("language","COMBO",True),
inp("context","STRING",True), inp("max_new_tokens","INT",True)],
[("text","STRING"),("language","STRING")]),
"OggEncodeVoice": ([inp("codec","OGG_CODEC"), inp("audio","AUDIO"), inp("transcript","STRING",True)], [("OGG_VOICE","OGG_VOICE")]),
"OggSaveVoice": ([inp("voice","OGG_VOICE"), inp("name","STRING",True)], []),
"MarkdownNote": ([], []),
}
def build(nodes, links, last_note=True):
"""nodes: [(id, type, pos, size, widgets_values, mode)], links: [(origin_id, origin_slot, target_id, input_name)]"""
out_nodes, by_id = [], {}
for order, (nid, typ, pos, size, wv, mode) in enumerate(nodes):
ins, outs = DEFS[typ]
n = {"id": nid, "type": typ, "pos": pos, "size": size, "flags": {}, "order": order, "mode": mode,
"inputs": [], "outputs": [], "properties": {} if typ in ("SaveAudio","MarkdownNote") else {"Node name for S&R": typ},
"widgets_values": wv}
for i in ins:
e = {"localized_name": i["name"], "name": i["name"], "type": i["type"]}
if i["widget"]: e["widget"] = {"name": i["name"]}
if i["optional"] and not i["widget"]: e["shape"] = 7
e["link"] = None
n["inputs"].append(e)
for name, typ_ in outs:
n["outputs"].append({"localized_name": name, "name": name, "type": typ_, "links": None})
out_nodes.append(n); by_id[nid] = n
out_links = []
for lid, (o, oslot, t, tname) in enumerate(links, 1):
on, tn = by_id[o], by_id[t]
tslot = next(k for k, e in enumerate(tn["inputs"]) if e["name"] == tname)
typ = on["outputs"][oslot]["type"]
assert tn["inputs"][tslot]["type"] == typ, (o, t, tname, typ, tn["inputs"][tslot]["type"])
tn["inputs"][tslot]["link"] = lid
on["outputs"][oslot]["links"] = (on["outputs"][oslot]["links"] or []) + [lid]
out_links.append([lid, o, oslot, t, tslot, typ])
for n in out_nodes:
for o in n["outputs"]:
if o["links"] is None: o["links"] = None
return {"id": str(uuid.uuid4()), "revision": 0, "last_node_id": max(n["id"] for n in out_nodes), "last_link_id": len(out_links),
"nodes": out_nodes, "links": out_links, "groups": [], "config": {}, "extra": {"ds": {"scale": 0.75, "offset": [0, 0]}}, "version": 0.4}
FILES = ("| File | Folder |\n|---|---|\n"
"| ogg_tts.safetensors (FashionFlora/ogg-tts `model.safetensors`, renamed, about 3 GB) | models/diffusion_models |\n"
"| drama3_codec.safetensors (the Fish codec, 1.6 GB) | models/vae |\n")
NOTE_TTS = ("## OGG TTS · text to speech (no reference voice)\n\n**Files**\n\n" + FILES +
"\nGet them from [FashionFlora/ogg-tts](https://huggingface.co/FashionFlora/ogg-tts) and "
"[Drama-3-ComfyUI](https://huggingface.co/PedroMarinhoDev/Drama-3-ComfyUI) (the codec only). OmniVoice's config.json and tokenizer "
"files are read from the pack's own `omnivoice` folder and fetched there on first use.\n\n"
"**How to use:** write the text in **OGG TTS Generate**, one line per take: each line hears the two lines before and after it as context. "
"Emotion and vocal tags go inline, e.g. [sighing], [whispering], [excited], [pause] (the node's description lists all 24). "
"Pick the language; there are 8: en, pl, es, de, ru, fr, pt, it.\n\n"
"**Settings:** cfg 1-1.5 keeps the natural tempo, 2 speaks faster and more pointedly. Without a reference voice a take varies with the seed: "
"queue again for another.\n\n**Longer takes:** OGG is still training; this is a preview release, so expect some takes to be better than others.")
NOTE_CLONE = ("## OGG TTS · voice cloning\n\n**Files**\n\n" + FILES +
"| qwen3_asr_1.7b_bf16.safetensors (4.1 GB)<br>or qwen3_asr_0.6b_bf16.safetensors (1.6 GB) | models/asr |\n\n"
"Get them from [FashionFlora/ogg-tts](https://huggingface.co/FashionFlora/ogg-tts), "
"[Drama-3-ComfyUI](https://huggingface.co/PedroMarinhoDev/Drama-3-ComfyUI) (the codec) and "
"[Qwen3-ASR-ComfyUI](https://huggingface.co/PedroMarinhoDev/Qwen3-ASR-ComfyUI). OmniVoice's config.json and tokenizer files are read from "
"the pack's own `omnivoice` folder and fetched there on first use.\n\n"
"**How to use:** pick a voice in **Load Audio** (a few seconds of clean speech, cut between sentences: choose a clip of your own). "
"**Qwen3-ASR Transcribe** writes what it says into **OGG Encode Voice**; to type the transcript yourself, delete that link. "
"Write your text in **OGG TTS Generate**, one line per take; tags such as [sighing] or [excited] go where the delivery should change.\n\n"
"**Keep a voice:** un-mute **Save OGG Voice** (select it, Ctrl+M) and queue once: the voice goes to output/ogg_voices. Next time use "
"**Load OGG Voice** in place of Load Audio, Qwen3-ASR and Encode Voice.\n\n"
"**Settings:** cfg 1-1.5 keeps the voice's natural tempo, 2 speaks faster and more pointedly. Do not clone the voice of a real person "
"without their consent.")
def gen_widgets(text, seed=1, language="en", speaker="", quality=""):
# text, language, seed, control_after_generate, steps, cfg, temperature, split_lines, max_frames, speaker, quality, mtp_temperature, mtp_top_k, mtp_rounds
return [text, language, seed, "randomize", 6, 1.5, 0.8, True, 0, speaker, quality, 0, 0, 4]
# ------------------------------------------------------------------ text to speech
tts = build([
(1, "MarkdownNote", [20, 20], [560, 520], [NOTE_TTS], 0),
(2, "OggLoader", [620, 20], [360, 82], ["ogg_tts.safetensors", "float32"], 0),
(3, "OggCodecLoader", [620, 160], [360, 58], ["drama3_codec.safetensors"], 0),
(4, "OggGenerate", [1020, 20], [400, 330],
gen_widgets("[excited] Hello! I am OGG, a text to speech model.\n[sighing] It has been a long day.\n[whispering] But I am glad you are here."), 0),
(5, "OggDecode", [1460, 20], [260, 78], [False], 0),
(6, "SaveAudio", [1460, 140], [360, 112], ["ogg/ogg_tts"], 0),
], [(2, 0, 4, "model"), (3, 0, 5, "codec"), (4, 0, 5, "codes"), (5, 0, 6, "audio")])
# ------------------------------------------------------------------ voice cloning
clone = build([
(1, "MarkdownNote", [20, 20], [560, 660], [NOTE_CLONE], 0),
(2, "OggLoader", [620, 20], [360, 82], ["ogg_tts.safetensors", "float32"], 0),
(3, "OggCodecLoader", [620, 160], [360, 58], ["drama3_codec.safetensors"], 0),
(4, "LoadAudio", [620, 380], [360, 136], ["", ""], 0),
(5, "OggEncodeVoice", [1020, 610], [400, 108], ["Hello! This is a short clip of my voice."], 0),
(6, "OggGenerate", [1020, 20], [400, 330],
gen_widgets("[frustrated] I told you this would happen.\n[sighing] We should have left an hour ago.\n[playful] But fine, a little adventure never hurt anyone."), 0),
(7, "OggDecode", [1460, 20], [260, 78], [False], 0),
(8, "SaveAudio", [1460, 140], [360, 112], ["ogg/ogg_clone"], 0),
(9, "OggQwen3ASRLoader", [620, 560], [360, 58], ["qwen3_asr_1.7b_bf16.safetensors"], 0),
(10, "OggQwen3ASRTranscribe", [1020, 380], [400, 180], ["auto", "", 4096], 0),
(11, "OggSaveVoice", [1460, 610], [360, 58], ["ogg/voice"], 2),
], [(3, 0, 5, "codec"), (4, 0, 5, "audio"), (3, 0, 7, "codec"), (6, 0, 7, "codes"), (7, 0, 8, "audio"),
(2, 0, 6, "model"), (5, 0, 6, "voice"), (9, 0, 10, "model"), (4, 0, 10, "audio"), (10, 0, 5, "transcript"), (5, 0, 11, "voice")])
NOTE_DIALOGUE = ("## OGG TTS · dialogue, a voice for every speaker\n\n**Files**\n\n" + FILES +
"| qwen3_asr_1.7b_bf16.safetensors (4.1 GB)<br>or qwen3_asr_0.6b_bf16.safetensors (1.6 GB) | models/asr |\n\n"
"**How to use:** pick one clip per speaker in the two **Load Audio** nodes (a few seconds of clean speech each; your own clips). Each "
"**Qwen3-ASR Transcribe** writes what its clip says into **OGG Encode Voice**. In **OGG TTS Dialogue** the speakers are named in "
"`name_1`, `name_2`... (without case) and their voices connect to `voice_1`, `voice_2`...; the script is one turn per line, `Name: [tag] text`. "
"A line with no name goes on with the speaker before it. Tags as in OGG TTS Generate.\n\n"
"**Modes:** *line by line* generates every line from its speaker's voice and lets it hear the two lines before and after it; it is the safe one. "
"*one pass* gives the model the whole scene and all the voices at once (its dialogue mode): one take, no line timing in the editor, "
"and less tried; it is experimental: the authors document only line by line. Up to 6 speakers; a speaker used in the script needs a voice.\n\nDo not clone the voice of a real person without their consent.")
dialogue = build([
(1, "MarkdownNote", [20, 20], [560, 620], [NOTE_DIALOGUE], 0),
(2, "OggLoader", [620, 20], [360, 82], ["ogg_tts.safetensors", "float32"], 0),
(3, "OggCodecLoader", [620, 160], [360, 58], ["drama3_codec.safetensors"], 0),
(4, "OggQwen3ASRLoader", [620, 280], [360, 58], ["qwen3_asr_1.7b_bf16.safetensors"], 0),
(5, "LoadAudio", [620, 400], [360, 136], ["", ""], 0),
(6, "LoadAudio", [620, 600], [360, 136], ["", ""], 0),
(7, "OggQwen3ASRTranscribe", [1020, 400], [400, 180], ["auto", "", 4096], 0),
(8, "OggQwen3ASRTranscribe", [1020, 620], [400, 180], ["auto", "", 4096], 0),
(9, "OggEncodeVoice", [1460, 400], [400, 108], ["Hello! This is the first speaker."], 0),
(10, "OggEncodeVoice", [1460, 620], [400, 108], ["Hello! This is the second speaker."], 0),
(11, "OggDialogue", [1020, 20], [400, 330],
["Жан: [excited] Привет, меня зовут Жан и я — авантюрист.\nМария: [sighing] Ты так говоришь каждому встречному.\nЖан: [laughing] Только тем, кто спрашивает.",
"ru", "line by line", 1, "randomize", 6, 1.5, 0.8, 0, "Жан", "Мария", "", "", "", "", "", 0, 0, 4, "as trained", "", "", "", "", "", ""], 0),
(12, "OggDecode", [1900, 20], [260, 78], [False], 0),
(13, "SaveAudio", [1900, 140], [360, 112], ["ogg/ogg_dialogue"], 0),
], [(3, 0, 9, "codec"), (3, 0, 10, "codec"), (5, 0, 9, "audio"), (6, 0, 10, "audio"), (4, 0, 7, "model"), (4, 0, 8, "model"),
(5, 0, 7, "audio"), (6, 0, 8, "audio"), (7, 0, 9, "transcript"), (8, 0, 10, "transcript"), (2, 0, 11, "model"),
(9, 0, 11, "voice_1"), (10, 0, 11, "voice_2"), (3, 0, 12, "codec"), (11, 0, 12, "codes"), (12, 0, 13, "audio")])
json.dump(dialogue, open(ROOT + "/example_workflows/ogg_dialogue.json", "w"), indent=2, ensure_ascii=False)
NOTE_DESIGN = ("## OGG TTS · find a voice\n\nOGG cannot be told a voice in words. What it has: **3157 trained speakers** (`spk_0` ... `spk_3156`, anonymous), "
"**599 recordings** (`q0` ... `q598`, each a book or a game, with its room and manner) and an unknown-speaker token per language (`ru_x`).\n\n"
"**Audition:** `speaker` in OGG TTS Generate is `random`: every run takes another seed, so another speaker (the console says which: `speaker token spk_N`). "
"Every run saves the take (output/ogg/design) and, beside it, its voice (output/ogg_voices/ogg/design): matching numbers. Listen, keep the ones you like, delete the others.\n\n"
"**Keep a voice:** the saved voice is a clip of that take with its transcript. Load it with **Load OGG Voice** in OGG TTS Generate or OGG TTS Dialogue: the speaker then "
"stays the same for any text. To explore recordings instead of speakers, fill `quality` (e.g. q35) and keep `speaker` as it is.\n\n"
"Files: see the other OGG workflows. This one needs no Qwen3-ASR: the transcript is the text you wrote.")
design_wf = build([
(1, "MarkdownNote", [20, 20], [560, 520], [NOTE_DESIGN], 0),
(2, "OggLoader", [620, 20], [360, 82], ["ogg_tts.safetensors", "float32"], 0),
(3, "OggCodecLoader", [620, 160], [360, 58], ["drama3_codec.safetensors"], 0),
(4, "OggGenerate", [1020, 20], [400, 330], gen_widgets("[excited] Hello! This is a short line to hear how I sound.", speaker="random"), 0),
(5, "OggDecode", [1460, 20], [260, 78], [False], 0),
(6, "SaveAudio", [1460, 140], [360, 112], ["ogg/design/take"], 0),
(7, "OggVoiceFromTake", [1020, 400], [400, 100], [0, True], 0),
(8, "OggSaveVoice", [1460, 400], [360, 80], ["ogg/design/voice"], 0),
], [(2, 0, 4, "model"), (3, 0, 5, "codec"), (4, 0, 5, "codes"), (5, 0, 6, "audio"), (4, 0, 7, "codes"), (7, 0, 8, "voice")])
json.dump(design_wf, open(ROOT + "/example_workflows/ogg_voice_design.json", "w"), indent=2, ensure_ascii=False)
json.dump(tts, open(ROOT + "/example_workflows/ogg_text_to_speech.json", "w"), indent=2, ensure_ascii=False)
json.dump(clone, open(ROOT + "/example_workflows/ogg_voice_clone.json", "w"), indent=2, ensure_ascii=False)
print("workflows written", len(tts["nodes"]), len(tts["links"]), len(clone["nodes"]), len(clone["links"]))