File size: 15,323 Bytes
4d1131c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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"]))