Compare commits
16
Commits
c137fbae0c
..
webui
No files matched your search
@@ -37,3 +37,6 @@ media-files/video/*.mp4
|
|||||||
|
|
||||||
# Small public-domain demo evidence clip for the auk voice-editing demo
|
# Small public-domain demo evidence clip for the auk voice-editing demo
|
||||||
!auk/*.wav
|
!auk/*.wav
|
||||||
|
|
||||||
|
# VibeVoice 4-speaker spike test clip - committed for remote review
|
||||||
|
!vibevoice-spike/*.mp3
|
||||||
@@ -9,6 +9,7 @@ Audio processing demos: WhisperX, Demucs, and TTS technology proofs-of-concept.
|
|||||||
- `tts/` — text-to-speech service demos (voice cloning via MOSS-TTS)
|
- `tts/` — text-to-speech service demos (voice cloning via MOSS-TTS)
|
||||||
- `auk/` — voice *editing* demos (emotion/pitch/speed/content editing on existing recordings,
|
- `auk/` — voice *editing* demos (emotion/pitch/speed/content editing on existing recordings,
|
||||||
as opposed to `tts/`'s clone-and-generate approach)
|
as opposed to `tts/`'s clone-and-generate approach)
|
||||||
|
- `webui/` — interactive pipeline studio chaining demucs -> AuK -> MOSS-TTS with previews
|
||||||
|
|
||||||
Each subdirectory has its own README with setup and run instructions.
|
Each subdirectory has its own README with setup and run instructions.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
Mastering chain for generated TTS/voice clips: fixes the "tinny" spectral
|
||||||
|
imbalance common to neural vocoder output (excess 3-6kHz presence energy,
|
||||||
|
starved low end relative to a naturally-recorded voice - see the project's
|
||||||
|
spectral comparison: raw recordings skew toward 200-1000Hz, generated clips
|
||||||
|
dump ~35% of their energy into 3-6kHz) and adds the density/space that
|
||||||
|
reads as "produced" rather than dry/synthetic.
|
||||||
|
|
||||||
|
Chain order matters: EQ before compression (so the compressor reacts to
|
||||||
|
the corrected tonal balance, not the raw tinny one), reverb after
|
||||||
|
compression (so the room tail doesn't get squashed), limiter last.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import soundfile as sf
|
||||||
|
from pedalboard import (
|
||||||
|
Compressor,
|
||||||
|
HighpassFilter,
|
||||||
|
HighShelfFilter,
|
||||||
|
Limiter,
|
||||||
|
LowShelfFilter,
|
||||||
|
PeakFilter,
|
||||||
|
Pedalboard,
|
||||||
|
Reverb,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_chain(
|
||||||
|
warmth_db: float,
|
||||||
|
presence_cut_db: float,
|
||||||
|
air_db: float,
|
||||||
|
comp_ratio: float,
|
||||||
|
reverb_wet: float,
|
||||||
|
) -> Pedalboard:
|
||||||
|
return Pedalboard([
|
||||||
|
HighpassFilter(cutoff_frequency_hz=60),
|
||||||
|
LowShelfFilter(cutoff_frequency_hz=150, gain_db=warmth_db, q=0.7),
|
||||||
|
PeakFilter(cutoff_frequency_hz=3800, gain_db=presence_cut_db, q=1.0),
|
||||||
|
HighShelfFilter(cutoff_frequency_hz=6500, gain_db=air_db, q=0.7),
|
||||||
|
Compressor(threshold_db=-20, ratio=comp_ratio, attack_ms=12, release_ms=120),
|
||||||
|
Reverb(room_size=0.08, damping=0.85, wet_level=reverb_wet, dry_level=1.0 - reverb_wet),
|
||||||
|
Limiter(threshold_db=-1.0, release_ms=100),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Apply a studio mastering chain to a voice clip")
|
||||||
|
parser.add_argument("--input", required=True)
|
||||||
|
parser.add_argument("--output", required=True)
|
||||||
|
parser.add_argument("--warmth-db", type=float, default=5.5, help="Low-shelf boost @150Hz")
|
||||||
|
parser.add_argument("--presence-cut-db", type=float, default=-5.0, help="Peak cut @3.8kHz (the tinny band)")
|
||||||
|
parser.add_argument("--air-db", type=float, default=-4.5, help="High-shelf @6.5kHz, for softness")
|
||||||
|
parser.add_argument("--comp-ratio", type=float, default=2.5)
|
||||||
|
parser.add_argument("--reverb-wet", type=float, default=0.025)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
audio, sr = sf.read(args.input, always_2d=True)
|
||||||
|
audio = audio.T.astype(np.float32) # pedalboard wants (channels, samples)
|
||||||
|
|
||||||
|
board = build_chain(
|
||||||
|
args.warmth_db, args.presence_cut_db, args.air_db, args.comp_ratio, args.reverb_wet
|
||||||
|
)
|
||||||
|
processed = board(audio, sr)
|
||||||
|
|
||||||
|
# pedalboard's Limiter is attack/release-based, not a true brick-wall
|
||||||
|
# limiter - fast transients can still punch through its threshold (seen
|
||||||
|
# in practice: in-memory peaks hit 0dBFS despite threshold_db=-1.0,
|
||||||
|
# producing audible clipping distortion on playback). Hard safety
|
||||||
|
# ceiling here guarantees the written file never clips regardless.
|
||||||
|
target_peak = 10 ** (-1.0 / 20)
|
||||||
|
peak = np.max(np.abs(processed))
|
||||||
|
if peak > target_peak:
|
||||||
|
processed = processed * (target_peak / peak)
|
||||||
|
|
||||||
|
sf.write(args.output, processed.T, sr)
|
||||||
|
print(f"[INFO] Saved {args.output}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""
|
||||||
|
Stable Audio 3 Small-SFX CLI: generate a sound effect/ambience clip from a
|
||||||
|
text description. Runs on CPU by design - this checkpoint is the
|
||||||
|
"lightweight, no GPU required" model in Stable Audio 3's lineup, so it
|
||||||
|
never contends with ComfyUI/other models for VRAM (unlike generate_sfx.py's
|
||||||
|
MOSS-SoundEffect, which needs the 3090 and ComfyUI's memory freed first).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
import torchaudio
|
||||||
|
from stable_audio_3 import StableAudioModel
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Stable Audio 3 Small-SFX text-to-sound-effect")
|
||||||
|
parser.add_argument("--text", required=True, help="Description of the sound effect/ambience")
|
||||||
|
parser.add_argument("--output", required=True, help="Output wav path")
|
||||||
|
parser.add_argument("--duration", type=float, default=10.0, help="Clip duration in seconds (max 120)")
|
||||||
|
parser.add_argument("--seed", type=int, default=-1, help="Random seed, or -1 for random")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
model = StableAudioModel.from_pretrained("small-sfx", device="cpu")
|
||||||
|
sample_rate = model.model.sample_rate
|
||||||
|
|
||||||
|
audio = model.generate(prompt=args.text, duration=args.duration, seed=args.seed)
|
||||||
|
torchaudio.save(args.output, audio[0].cpu(), sample_rate)
|
||||||
|
print(f"[INFO] Saved {args.output}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
"""
|
||||||
|
Generic MOSS-TTS zero-shot cloning CLI: clone the voice in --reference and
|
||||||
|
say --text in it. Used directly, and as the final stage of webui/app.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
import soundfile as sf
|
||||||
|
import torch
|
||||||
|
from transformers import AutoModel, AutoProcessor
|
||||||
|
|
||||||
|
PRETRAINED = "OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_attn_implementation(device: str, dtype: torch.dtype) -> str:
|
||||||
|
if (
|
||||||
|
device == "cuda"
|
||||||
|
and importlib.util.find_spec("flash_attn") is not None
|
||||||
|
and dtype in {torch.float16, torch.bfloat16}
|
||||||
|
):
|
||||||
|
major, _ = torch.cuda.get_device_capability()
|
||||||
|
if major >= 8:
|
||||||
|
return "flash_attention_2"
|
||||||
|
return "sdpa" if device == "cuda" else "eager"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="MOSS-TTS zero-shot voice cloning")
|
||||||
|
parser.add_argument("--text", required=True, help="Text to synthesize")
|
||||||
|
parser.add_argument("--reference", required=True, help="Reference audio path")
|
||||||
|
parser.add_argument("--output", required=True, help="Output wav path")
|
||||||
|
parser.add_argument("--language", default="English", help="Language tag")
|
||||||
|
parser.add_argument(
|
||||||
|
"--instruction", default=None,
|
||||||
|
help='Style/delivery instruction (e.g. "Change the emotion to excited."). '
|
||||||
|
"Undocumented MOSS-TTS field for this checkpoint - confirmed to work for "
|
||||||
|
"emotion in testing, passed through as-is.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--seed", type=int, default=None,
|
||||||
|
help="Random seed for reproducibility. Unset = nondeterministic.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--audio_temperature", type=float, default=1.7)
|
||||||
|
parser.add_argument("--audio_top_p", type=float, default=0.8)
|
||||||
|
parser.add_argument("--audio_top_k", type=int, default=25)
|
||||||
|
parser.add_argument("--audio_repetition_penalty", type=float, default=1.0)
|
||||||
|
parser.add_argument("--max_new_tokens", type=int, default=4096)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
dtype = torch.bfloat16 if device == "cuda" else torch.float32
|
||||||
|
|
||||||
|
if args.seed is not None:
|
||||||
|
torch.manual_seed(args.seed)
|
||||||
|
if device == "cuda":
|
||||||
|
torch.cuda.manual_seed_all(args.seed)
|
||||||
|
|
||||||
|
torch.backends.cuda.enable_cudnn_sdp(False)
|
||||||
|
torch.backends.cuda.enable_flash_sdp(True)
|
||||||
|
torch.backends.cuda.enable_mem_efficient_sdp(True)
|
||||||
|
torch.backends.cuda.enable_math_sdp(True)
|
||||||
|
|
||||||
|
attn_implementation = resolve_attn_implementation(device, dtype)
|
||||||
|
print(f"[INFO] attn_implementation={attn_implementation}")
|
||||||
|
|
||||||
|
processor = AutoProcessor.from_pretrained(PRETRAINED, trust_remote_code=True)
|
||||||
|
processor.audio_tokenizer = processor.audio_tokenizer.to(device)
|
||||||
|
|
||||||
|
model = AutoModel.from_pretrained(
|
||||||
|
PRETRAINED,
|
||||||
|
trust_remote_code=True,
|
||||||
|
attn_implementation=attn_implementation,
|
||||||
|
dtype=dtype,
|
||||||
|
).to(device)
|
||||||
|
model.eval()
|
||||||
|
|
||||||
|
conversation = [
|
||||||
|
processor.build_user_message(
|
||||||
|
text=args.text,
|
||||||
|
language=args.language,
|
||||||
|
reference=[args.reference],
|
||||||
|
instruction=args.instruction,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
batch = processor([conversation], mode="generation")
|
||||||
|
outputs = model.generate(
|
||||||
|
input_ids=batch["input_ids"].to(device),
|
||||||
|
attention_mask=batch["attention_mask"].to(device),
|
||||||
|
max_new_tokens=args.max_new_tokens,
|
||||||
|
do_sample=True,
|
||||||
|
audio_temperature=args.audio_temperature,
|
||||||
|
audio_top_p=args.audio_top_p,
|
||||||
|
audio_top_k=args.audio_top_k,
|
||||||
|
audio_repetition_penalty=args.audio_repetition_penalty,
|
||||||
|
)
|
||||||
|
message = processor.decode(outputs)[0]
|
||||||
|
audio = message.audio_codes_list[0]
|
||||||
|
data = audio.detach().cpu().to(torch.float32).numpy()
|
||||||
|
if data.ndim == 2:
|
||||||
|
data = data.T
|
||||||
|
sf.write(args.output, data, processor.model_config.sampling_rate)
|
||||||
|
print(f"[INFO] Saved {args.output}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
"""
|
||||||
|
MOSS-SoundEffect CLI: generate an ambient/sound-effect clip from a text
|
||||||
|
description. Sibling of generate.py (MOSS-TTS voice cloning) - same
|
||||||
|
backend setup and model-loading pattern, different checkpoint and a
|
||||||
|
text-only build_user_message call (no reference audio, no cloning).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
|
||||||
|
# This checkpoint's weights alone use ~22.3GB of this box's ~23.56GB usable
|
||||||
|
# VRAM, leaving generate()'s own small allocations (a vocab-sized mask
|
||||||
|
# tensor, etc.) no room without this - confirmed via OOM otherwise. Must be
|
||||||
|
# set before torch's CUDA allocator initializes, so it's set here rather
|
||||||
|
# than relying on the caller's shell environment.
|
||||||
|
os.environ.setdefault("PYTORCH_ALLOC_CONF", "expandable_segments:True")
|
||||||
|
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
|
||||||
|
|
||||||
|
import soundfile as sf
|
||||||
|
import torch
|
||||||
|
from transformers import AutoModel, AutoProcessor
|
||||||
|
|
||||||
|
PRETRAINED = "OpenMOSS-Team/MOSS-SoundEffect"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_attn_implementation(device: str, dtype: torch.dtype) -> str:
|
||||||
|
if (
|
||||||
|
device == "cuda"
|
||||||
|
and importlib.util.find_spec("flash_attn") is not None
|
||||||
|
and dtype in {torch.float16, torch.bfloat16}
|
||||||
|
):
|
||||||
|
major, _ = torch.cuda.get_device_capability()
|
||||||
|
if major >= 8:
|
||||||
|
return "flash_attention_2"
|
||||||
|
return "sdpa" if device == "cuda" else "eager"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="MOSS-SoundEffect text-to-sound-effect")
|
||||||
|
parser.add_argument("--text", required=True, help="Description of the sound effect/ambience")
|
||||||
|
parser.add_argument("--output", required=True, help="Output wav path")
|
||||||
|
parser.add_argument(
|
||||||
|
"--seed", type=int, default=None,
|
||||||
|
help="Random seed for reproducibility. Unset = nondeterministic.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--max_new_tokens", type=int, default=4096)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
dtype = torch.bfloat16 if device == "cuda" else torch.float32
|
||||||
|
|
||||||
|
if args.seed is not None:
|
||||||
|
torch.manual_seed(args.seed)
|
||||||
|
if device == "cuda":
|
||||||
|
torch.cuda.manual_seed_all(args.seed)
|
||||||
|
|
||||||
|
torch.backends.cuda.enable_cudnn_sdp(False)
|
||||||
|
torch.backends.cuda.enable_flash_sdp(True)
|
||||||
|
torch.backends.cuda.enable_mem_efficient_sdp(True)
|
||||||
|
torch.backends.cuda.enable_math_sdp(True)
|
||||||
|
|
||||||
|
attn_implementation = resolve_attn_implementation(device, dtype)
|
||||||
|
print(f"[INFO] attn_implementation={attn_implementation}")
|
||||||
|
|
||||||
|
processor = AutoProcessor.from_pretrained(PRETRAINED, trust_remote_code=True)
|
||||||
|
processor.audio_tokenizer = processor.audio_tokenizer.to(device)
|
||||||
|
|
||||||
|
model = AutoModel.from_pretrained(
|
||||||
|
PRETRAINED,
|
||||||
|
trust_remote_code=True,
|
||||||
|
attn_implementation=attn_implementation,
|
||||||
|
dtype=dtype,
|
||||||
|
).to(device)
|
||||||
|
model.eval()
|
||||||
|
|
||||||
|
conversation = [processor.build_user_message(ambient_sound=args.text)]
|
||||||
|
|
||||||
|
with torch.no_grad():
|
||||||
|
batch = processor([conversation], mode="generation")
|
||||||
|
# This checkpoint's generate() doesn't accept the TTS model's sampling
|
||||||
|
# kwargs (do_sample/audio_temperature/etc raise TypeError) - matches
|
||||||
|
# the official README example, which only passes these three.
|
||||||
|
outputs = model.generate(
|
||||||
|
input_ids=batch["input_ids"].to(device),
|
||||||
|
attention_mask=batch["attention_mask"].to(device),
|
||||||
|
max_new_tokens=args.max_new_tokens,
|
||||||
|
)
|
||||||
|
message = processor.decode(outputs)[0]
|
||||||
|
audio = message.audio_codes_list[0]
|
||||||
|
data = audio.detach().cpu().to(torch.float32).numpy()
|
||||||
|
if data.ndim == 2:
|
||||||
|
data = data.T
|
||||||
|
sf.write(args.output, data, processor.model_config.sampling_rate)
|
||||||
|
print(f"[INFO] Saved {args.output}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# VibeVoice (community fork) 4-person podcast spike
|
||||||
|
|
||||||
|
Standalone experiment, not integrated into the webui or the production
|
||||||
|
pipeline. Tests whether VibeVoice (https://github.com/vibevoice-community/VibeVoice)
|
||||||
|
can generate a 4-speaker podcast scene in one pass, cloning our four
|
||||||
|
established character voices (Kevin, Marge, Terry, Priya) from their
|
||||||
|
existing approved `reference_audio` in the-service-station repo.
|
||||||
|
|
||||||
|
- `scripts/trailer_promo_4speaker.txt` - the test script: the real
|
||||||
|
"sausage rolls" trailer scene from `the-service-station/scripts/trailer-promo.md`,
|
||||||
|
converted from our `[Character|tags]` format to VibeVoices required
|
||||||
|
`Speaker N: ...` plain-text format (tags dropped - VibeVoice has no
|
||||||
|
per-line emotion/timing control).
|
||||||
|
- `trailer_promo_4speaker_vibevoice-1.5b.mp3` - the generated result
|
||||||
|
(VibeVoice-1.5B, seed 42). 47s of audio generated in 14.5s (RTF 0.31x).
|
||||||
|
|
||||||
|
Setup lives outside this repo (not committed): `.venv/` here is a fresh
|
||||||
|
isolated venv (`pip install -e /root/projects/VibeVoice`, pinned
|
||||||
|
`transformers==4.51.3`), pointed at the already-downloaded
|
||||||
|
`VibeVoice-1.5B` weights under `/root/projects/ComfyUI/models/tts/VibeVoice/`.
|
||||||
|
|
||||||
|
## Findings (2026-09-30)
|
||||||
|
|
||||||
|
Whisper-transcript check against the script: mostly faithful, not
|
||||||
|
word-perfect. Two small unscripted insertions ("I am here!" before Priyas
|
||||||
|
line, "For me," before Marges reply) and one substitution ("He will." ->
|
||||||
|
"You will."). No repetition/collapse like the AuK chain-corruption bug -
|
||||||
|
just minor ad-libbing, not catastrophic.
|
||||||
|
|
||||||
|
No audible AI disclaimer or obvious background music in this run - both
|
||||||
|
are documented risks for this model family (see
|
||||||
|
`the-service-station/research/vibevoice-2026-08-21.md` and
|
||||||
|
`vibevoice-community-fork-2026-08-21.md`), so treat this one clean run as
|
||||||
|
encouraging, not conclusive.
|
||||||
|
|
||||||
|
Steves call after listening: pursue 7B (weights already downloaded, same
|
||||||
|
venv/script, swap `--model_path`) or not worth it given the limitations
|
||||||
|
the prior research already flagged.
|
||||||
|
|
||||||
|
## 7B result (2026-09-30, same script/seed/voices)
|
||||||
|
|
||||||
|
`trailer_promo_4speaker_vibevoice-7b.mp3` - VibeVoice-7B, same seed (42),
|
||||||
|
same script, same reference voices. 34.8s of audio generated in 22.2s
|
||||||
|
(RTF 0.64x - still faster than real-time, just less headroom than 1.5B).
|
||||||
|
|
||||||
|
Whisper-transcript check: cleaner than 1.5B - neither of 1.5Bs unscripted
|
||||||
|
insertions ("I am here!", "For me,") appeared, and "He will." came through
|
||||||
|
exactly right this time (1.5B had drifted it to "You will."). One real
|
||||||
|
deviation: Marges defensive line "I get one fact wrong per episode."
|
||||||
|
came out as a question, "Can I get one fact wrong per episode?" - changes
|
||||||
|
the delivery/meaning, not just wording. Still no audible disclaimer and
|
||||||
|
nothing that reads as injected background music.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
Speaker 1: The Service Station. Three facts, one car park, and absolutely no reason to be in a hurry.
|
||||||
|
Speaker 2: Except for me, obviously. I'm always in a hurry.
|
||||||
|
Speaker 3: I had a Sinclair Spectrum in 1984, and I will find a way to bring it up.
|
||||||
|
Speaker 1: He will.
|
||||||
|
Speaker 4: I still don't understand why the sausage rolls are relevant, but here we are.
|
||||||
|
Speaker 2: They're always relevant, Priya.
|
||||||
|
Speaker 1: Every week: one underdog, one argument, and one fact Marge gets wrong.
|
||||||
|
Speaker 2: I get one fact wrong per episode. Tops.
|
||||||
|
Speaker 3: It's usually about football.
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,56 @@
|
|||||||
|
# webui
|
||||||
|
|
||||||
|
A pipeline studio: strip a voice with **demucs**, edit it with **AuK** (emotion, pitch, speed,
|
||||||
|
free-form instructions — with preview and undo/reset), then generate new speech in that voice
|
||||||
|
with **MOSS-TTS**.
|
||||||
|
|
||||||
|
## Why this exists
|
||||||
|
|
||||||
|
Manually chaining demucs → AuK → MOSS-TTS across three separate venvs (via SSH one-off scripts)
|
||||||
|
worked but made it slow to experiment — especially with AuK's pitch/emotion controls, where you
|
||||||
|
really want to preview a result and back out if it's wrong (see the pitch-overshoot findings in
|
||||||
|
`auk/README.md`) rather than committing to a specific edit blind.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
A single lightweight Gradio app (`app.py`) that **shells out** to each tool's own isolated venv
|
||||||
|
via subprocess, rather than merging their dependencies into one environment — MOSS-TTS pins
|
||||||
|
`transformers==5.0.0`, AuK needs `4.57.6`; they can't coexist in one venv. This app's own venv
|
||||||
|
only needs `gradio` itself.
|
||||||
|
|
||||||
|
Each stage's output becomes the next stage's input by filesystem path; a `history` list backs
|
||||||
|
the undo/reset buttons in the voice editor.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv && . .venv/bin/activate
|
||||||
|
pip install gradio
|
||||||
|
```
|
||||||
|
|
||||||
|
Assumes `demucs/`, `auk/`, and `tts/` (siblings in this repo) already have their own working
|
||||||
|
venvs and, for `auk/`, downloaded model weights — see their READMEs.
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Serves on `0.0.0.0:7860`. The app automatically calls ComfyUI's `/free` endpoint (if its queue
|
||||||
|
is empty) before each heavy step, to reduce GPU contention on a shared box — see the DNS/routing
|
||||||
|
and GPU-contention notes earlier in this project's history for why that matters here.
|
||||||
|
|
||||||
|
## Known limitations (inherited from the underlying tools)
|
||||||
|
|
||||||
|
- **AuK's real usable clip length is ~25-30s**, not several minutes — feeding it a long recording
|
||||||
|
will OOM even with `--cpu_offload` (see `auk/README.md`). This app doesn't chunk long inputs
|
||||||
|
automatically; keep reference clips short.
|
||||||
|
- **AuK's pitch-editing instruction-following is unreliable** — even "1 semitone" has measured
|
||||||
|
out at ~4+ semitones of actual shift in testing. The UI surfaces a warning; preview before
|
||||||
|
trusting a pitch edit.
|
||||||
|
- Each stage is a blocking subprocess call (model load + inference each time) — the UI will
|
||||||
|
appear to hang for anywhere from ~10s to a couple of minutes per click, with no progress bar
|
||||||
|
yet.
|
||||||
|
- No queueing/locking across browser tabs or concurrent users — this is a single-GPU box, so two
|
||||||
|
simultaneous generations will contend for VRAM.
|
||||||
+1106
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user