初始提交:3 款纯音频游戏 + 共享引擎 + 完整文档与资产管线

- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频
  (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine
  状态机接口、MainActivity 全屏封面 + TalkBack
- 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局
- 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速
- 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链
- 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/
  asset-pipeline(资产流水线)/ games/*(三款游戏设计文档)
- 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账,
  全部 ogg 资产入库可复现
- env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
This commit is contained in:
SoundGame Dev
2026-08-29 13:00:34 +08:00
commit f0f0a9b50e
446 changed files with 2627836 additions and 0 deletions
@@ -0,0 +1,670 @@
import gc
import numpy as np
import gradio as gr
import re
import subprocess
import torch
import torchaudio
import threading
import os, time, math
from einops import rearrange
from stable_audio_3.interface.aeiou import audio_spectrogram_image
from stable_audio_3.inference.distribution_shift import LogSNRShift, FluxDistributionShift, DistributionShift, IdentityDistributionShift
from stable_audio_3.models.lora import has_lora
from stable_audio_3.interface.reprompt import reprompt as _reprompt_fn, get_model as _reprompt_get_model, is_model_cached as _reprompt_is_model_cached
stable_audio_3_model = None
sample_size = 5324800
sample_rate = 44100
n_loras = 0
_LENGTH_EXTRACT_RE = re.compile(r' Length: (\d+) seconds\.?\s*$')
def parse_inpaint_regions(starts_csv, ends_csv):
"""Parse matching comma-separated inpaint start/end regions."""
def parse(value):
values = [item.strip() for item in str(value or "").split(",") if item.strip()]
try:
return [float(item) for item in values]
except ValueError as exc:
raise gr.Error("Inpaint regions must be comma-separated numbers.") from exc
starts = parse(starts_csv)
ends = parse(ends_csv)
if not starts and not ends:
return None, None
if not starts or not ends:
raise gr.Error("Inpaint starts and ends must both be provided.")
if len(starts) != len(ends):
raise gr.Error("Inpaint starts and ends must contain the same number of regions.")
if any(start < 0 or end <= start for start, end in zip(starts, ends)):
raise gr.Error("Each inpaint region must have a non-negative start before its end.")
return (
starts[0] if len(starts) == 1 else starts,
ends[0] if len(ends) == 1 else ends,
)
# when using a prompt in a filename
def condense_prompt(prompt):
pattern = r'[\\/:*?"<>|]'
# Replace special characters with hyphens
prompt = re.sub(pattern, '-', prompt)
# set a character limit
prompt = prompt[:150]
# zero length prompts may lead to filenames (ie ".wav") which seem cause problems with gradio
if len(prompt)==0:
prompt = "_"
return prompt
def generate_cond(
prompt,
negative_prompt=None,
seconds_total=30,
cfg_scale=6.0,
steps=250,
preview_every=None,
seed=-1,
sampler_type="dpmpp-3m-sde",
sigma_max=1000,
cfg_interval_min=0.0,
cfg_interval_max=1.0,
cfg_rescale=0.0,
cfg_norm_threshold=0.0,
apg_scale=1.0,
file_format="wav",
file_naming="verbose",
cut_to_seconds_total=False,
init_audio=None,
init_noise_level=1.0,
mask_maskstart=None,
mask_maskend=None,
inpaint_audio=None,
init_audio_type="Init audio",
inversion_steps=100,
inversion_gamma=0.3,
inversion_unconditional=False,
duration_padding_sec=6.0,
batch_size=1,
dist_shift=None,
*lora_args
):
if torch.cuda.is_available():
torch.cuda.empty_cache()
gc.collect()
print(f"Prompt: {prompt}")
global preview_images
preview_images = []
if preview_every == 0:
preview_every = None
# Parse per-LoRA controls from trailing args
# Each LoRA has 5 controls: strength, interval_min, interval_max, layer_filter
lora_configs = None
if n_loras > 0 and len(lora_args) >= n_loras * 4:
lora_configs = []
for i in range(n_loras):
off = i * 4
strength = lora_args[off]
interval_min = lora_args[off + 1]
interval_max = lora_args[off + 2]
layer_filter = lora_args[off + 3]
stable_audio_3_model.set_lora_strength(strength, lora_index=i)
lora_configs.append({
"lora_index": i,
"interval": (interval_min, interval_max),
"layer_filter": layer_filter,
})
input_sample_size = sample_size
def progress_callback(callback_info):
global preview_images
denoised = callback_info["denoised"]
current_step = callback_info["i"]
t = callback_info["t"]
sigma = callback_info["sigma"]
# Extract scalar from tensor if needed (samplers pass tensors to avoid GPU sync)
if isinstance(t, torch.Tensor):
t = t[0].item() if t.dim() > 0 else t.item()
if isinstance(sigma, torch.Tensor):
sigma = sigma[0].item() if sigma.dim() > 0 else sigma.item()
log_snr = math.log(((1 - sigma) / sigma) + 1e-6)
if (current_step - 1) % preview_every == 0:
if stable_audio_3_model.model.pretransform is not None:
denoised = stable_audio_3_model.model.pretransform.decode(denoised)
denoised = rearrange(denoised, "b d n -> d (b n)")
denoised = denoised.clamp(-1, 1).mul(32767).to(torch.int16).cpu()
audio_spectrogram = audio_spectrogram_image(denoised, sample_rate=sample_rate)
preview_images.append((audio_spectrogram, f"Step {current_step} sigma={sigma:.3f} logSNR={log_snr:.3f}"))
if init_audio_type == "RF-Inversion":
inversion_params = {
"inversion_steps": inversion_steps,
"inversion_gamma": inversion_gamma,
"inversion_unconditional": inversion_unconditional,
"inversion_cfg_scale": 1.0,
"inversion_sigma_max": 1.0
}
else:
inversion_params = None
mask_maskstart, mask_maskend = parse_inpaint_regions(
mask_maskstart, mask_maskend
)
if inpaint_audio is not None and mask_maskstart is None:
raise gr.Error("Add at least one inpaint start/end region.")
generate_args = {
"prompt": prompt,
"negative_prompt": negative_prompt,
"duration": seconds_total,
"steps": steps,
"cfg_scale": cfg_scale,
"cfg_interval": (cfg_interval_min, cfg_interval_max),
"lora_configs": lora_configs,
"batch_size": int(batch_size),
"sample_size": input_sample_size,
"seed": seed,
"sampler_type": sampler_type,
"sigma_max": sigma_max,
"init_audio": init_audio,
"init_noise_level": init_noise_level,
"callback": progress_callback if preview_every is not None else None,
"scale_phi": cfg_rescale,
"cfg_norm_threshold": cfg_norm_threshold,
"apg_scale": apg_scale,
"duration_padding_sec": duration_padding_sec,
"dist_shift": dist_shift,
}
# If inpainting, send mask args
# This will definitely change in the future
if inpaint_audio is not None:
generate_args.update({
"inpaint_audio": inpaint_audio,
"inpaint_mask_start_seconds": mask_maskstart,
"inpaint_mask_end_seconds": mask_maskend,
})
audio = stable_audio_3_model.generate(**generate_args)
# Filenaming convention
prompt_condensed = condense_prompt(prompt)
if file_naming=="verbose":
basename = prompt_condensed
if negative_prompt:
basename += ".neg-%s" % condense_prompt(negative_prompt)
basename += ".cfg%s" % (cfg_scale)
if sigma_max not in [1.0, 100.0]:
# this is a common parameter to tweak, if it's not a default value, put it in the verbose filename
basename += ".smx%s" % sigma_max
basename += ".%s" % seed
elif file_naming=="prompt":
basename = prompt_condensed
else:
# simple e.g. "output.wav"
basename = "output"
if file_format:
filename_extension = file_format.split(" ")[0].lower()
else:
filename_extension = "wav"
output_filename = "%s.%s" % (basename, filename_extension)
output_wav = "%s.wav" % basename
# Cut the extra silence off the end, if the user requested a smaller seconds_total
if cut_to_seconds_total:
audio = audio[:,:,:seconds_total*sample_rate]
# Encode the audio to WAV format
audio = rearrange(audio, "b d n -> d (b n)")
audio = audio.to(torch.float32).clamp(-1, 1).mul(32767).to(torch.int16).cpu()
# save as wav file
torchaudio.save(output_wav, audio, sample_rate)
# If file_format is other than wav, convert to other file format
cmd = ""
if file_format == "m4a aac_he_v2 32k":
# note: need to compile ffmpeg with --enable-libfdk_aac
cmd = f"ffmpeg -i \"{output_wav}\" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 32k -y \"{output_filename}\""
elif file_format == "m4a aac_he_v2 64k":
cmd = f"ffmpeg -i \"{output_wav}\" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 64k -y \"{output_filename}\""
elif file_format == "flac":
cmd = f"ffmpeg -i \"{output_wav}\" -y \"{output_filename}\""
elif file_format == "mp3 320k":
cmd = f"ffmpeg -i \"{output_wav}\" -b:a 320k -y \"{output_filename}\""
elif file_format == "mp3 128k":
cmd = f"ffmpeg -i \"{output_wav}\" -b:a 128k -y \"{output_filename}\""
elif file_format == "mp3 v0":
cmd = f"ffmpeg -i \"{output_wav}\" -q:a 0 -y \"{output_filename}\""
else: # wav
pass
if cmd:
cmd += " -loglevel error" # make output less verbose in the cmd window
subprocess.run(cmd, shell=True, check=True)
# Let's look at a nice spectrogram too
audio_spectrogram = audio_spectrogram_image(audio, sample_rate=sample_rate)
# Asynchronously delete the files after returning the output file, so as to prevent clutter in the directory
delete_files_async([output_wav, output_filename], 30)
return (output_filename, [audio_spectrogram, *preview_images])
# Asynchronously delete the given list of filenames after delay seconds. Sets up thread that sleeps for delay then deletes.
def delete_files_async(filenames, delay):
def delete_files_after_delay(filenames, delay):
time.sleep(delay) # Wait for the specified delay
for filename in filenames:
if os.path.exists(filename):
os.remove(filename) # Delete the file
threading.Thread(target=delete_files_after_delay, args=(filenames, delay)).start()
def create_sampling_ui(stable_audio_3_model, default_prompt=None):
global n_loras
diffusion_objective = stable_audio_3_model.model.diffusion_objective
is_rf = diffusion_objective == "rectified_flow"
is_rf_denoiser = diffusion_objective == "rf_denoiser" # includes ARC models
# Extract default dist_shift params from model's sampling_dist_shift
default_sampling_dist_shift = getattr(stable_audio_3_model.model, 'sampling_dist_shift', None)
default_dist_shift_type = "LogSNR"
default_logsnr_params = {"anchor_length": 2000, "anchor_logsnr": -6.2, "rate": 0.0, "logsnr_end": 2.0}
default_flux_params = {"min_length": 256, "max_length": 4096, "alpha_min": 6.93, "alpha_max": 6.93}
default_full_params = {"base_shift": 0.5, "max_shift": 1.15, "min_length": 256, "max_length": 4096}
if isinstance(default_sampling_dist_shift, LogSNRShift):
default_dist_shift_type = "LogSNR"
default_logsnr_params = {
"anchor_length": getattr(default_sampling_dist_shift, 'anchor_length', 2000),
"anchor_logsnr": getattr(default_sampling_dist_shift, 'anchor_logsnr', -6.2),
"rate": getattr(default_sampling_dist_shift, 'rate', 0.0),
"logsnr_end": getattr(default_sampling_dist_shift, 'logsnr_end', 2.0),
}
elif isinstance(default_sampling_dist_shift, FluxDistributionShift):
default_dist_shift_type = "Flux"
default_flux_params = {
"min_length": default_sampling_dist_shift.min_length,
"max_length": default_sampling_dist_shift.max_length,
"alpha_min": default_sampling_dist_shift.alpha_min,
"alpha_max": default_sampling_dist_shift.alpha_max,
}
elif isinstance(default_sampling_dist_shift, DistributionShift):
default_dist_shift_type = "Full"
default_full_params = {
"base_shift": default_sampling_dist_shift.base_shift,
"max_shift": default_sampling_dist_shift.max_shift,
"min_length": default_sampling_dist_shift.min_length,
"max_length": default_sampling_dist_shift.max_length,
}
elif default_sampling_dist_shift is None:
default_dist_shift_type = "None"
has_seconds_total = True
use_lora = has_lora(stable_audio_3_model.model)
lora_names = getattr(stable_audio_3_model.model, 'lora_names', [])
n_loras = len(lora_names)
if default_prompt is None:
default_prompt = ""
_reprompt_model_id = "Qwen/Qwen3.5-2B"
_reprompt_cached = _reprompt_is_model_cached(_reprompt_model_id)
with gr.Row():
with gr.Column(scale=6):
prompt = gr.Textbox(show_label=False, placeholder="Prompt", value=default_prompt)
negative_prompt = gr.Textbox(show_label=False, placeholder="Negative prompt")
prompt_assistant_button = gr.Button(
"Prompt Assistant" if _reprompt_cached else "Download Prompt Assistant (~4.2 GB)",
scale=1
)
generate_button = gr.Button("Generate", variant='primary', scale=1)
with gr.Row(equal_height=False):
with gr.Column():
with gr.Row(visible = True):
# Timing controls
seconds_total_slider = gr.Slider(minimum=0, maximum=sample_size//sample_rate, step=1, value=sample_size//sample_rate, label="Seconds total", visible=has_seconds_total)
with gr.Row():
# Steps slider
if is_rf:
default_steps = 50
elif is_rf_denoiser:
default_steps = 8
steps_slider = gr.Slider(minimum=1, maximum=500, step=1, value=default_steps, label="Steps")
# CFG scale
default_cfg_scale = 1.0 if is_rf_denoiser else 7.0
cfg_scale_slider = gr.Slider(minimum=0.0, maximum=25.0, step=0.1, value=default_cfg_scale, label="CFG scale")
# Per-LoRA controls (dynamic based on number of loaded LoRAs)
lora_ui_inputs = []
if use_lora and lora_names:
for i, lora_name in enumerate(lora_names):
with gr.Accordion("LoRA {}: {}".format(i + 1, lora_name), open=(i == 0)):
with gr.Row():
strength = gr.Slider(minimum=0.0, maximum=10.0, step=0.1, value=1.0, label="strength")
with gr.Row():
int_min = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, value=0.0, label="Interval min")
int_max = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, value=1.0, label="Interval max")
lyr_filt = gr.Textbox(label="Layer filter", placeholder="")
lora_ui_inputs.extend([strength, int_min, int_max, lyr_filt])
with gr.Accordion("Sampler params", open=False):
with gr.Row():
# Seed
seed_textbox = gr.Number(label="Seed (set to -1 for random seed)", value=-1, precision=0)
cfg_interval_min_slider = gr.Slider(minimum=0.0, maximum=1, step=0.01, value=0.0, label="CFG interval min")
cfg_interval_max_slider = gr.Slider(minimum=0.0, maximum=1, step=0.01, value=1.0, label="CFG interval max")
with gr.Row():
cfg_rescale_slider = gr.Slider(minimum=0.0, maximum=1, step=0.01, value=0.0, label="CFG rescale amount")
cfg_norm_threshold = gr.Slider(minimum=0.0, maximum=100, step=0.1, value=0.0, label="CFG norm threshold")
apg_scale_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.1, value=1.0, label="APG scale", info="1.0=full APG, 0.0=vanilla CFG")
with gr.Row():
# Sampler params
if is_rf:
sampler_types = ["euler", "rk4", "dpmpp"]
default_sampler_type = "euler"
sigma_max_max = 1.0
sigma_max_default = 1.0
elif is_rf_denoiser:
sampler_types = ["pingpong"]
default_sampler_type = "pingpong"
sigma_max_max = 1.0
sigma_max_default = 1.0
else:
sampler_types = ["dpmpp-2m-sde", "dpmpp-3m-sde", "dpmpp-2m", "k-heun", "k-lms", "k-dpmpp-2s-ancestral", "k-dpm-2", "k-dpm-adaptive", "k-dpm-fast", "v-ddim", "v-ddim-cfgpp"]
default_sampler_type = "dpmpp-3m-sde"
sigma_max_max = 1000.0
sigma_max_default = 100.0
sampler_type_dropdown = gr.Dropdown(sampler_types, label="Sampler type", value=default_sampler_type)
sigma_max_slider = gr.Slider(minimum=0.0, maximum=sigma_max_max, step=0.1, value=sigma_max_default, label="Sigma max", visible=True)
with gr.Row():
duration_padding_slider = gr.Slider(minimum=0.0, maximum=30.0, step=0.5, value=6.0, label="Duration padding (sec)")
def build_dist_shift(shift_type, p1, p2, p3, p4):
"""Build dist_shift from type + 4 params (meaning depends on type)."""
if shift_type == "LogSNR":
return LogSNRShift(anchor_length=int(p1), anchor_logsnr=p2, rate=p3, logsnr_end=p4)
elif shift_type == "Flux":
return FluxDistributionShift(min_length=int(p1), max_length=int(p2), alpha_min=p3, alpha_max=p4)
elif shift_type == "Full":
return DistributionShift(base_shift=p1, max_shift=p2, min_length=int(p3), max_length=int(p4))
return IdentityDistributionShift() # "None" = no shift
dist_shift_state = gr.State(value=default_sampling_dist_shift)
with gr.Row(visible=is_rf or is_rf_denoiser):
dist_shift_type_dropdown = gr.Dropdown(
["LogSNR", "Flux", "Full", "None"],
label="Sampling schedule shift",
value=default_dist_shift_type,
info="Distribution shift applied to sampling timesteps"
)
with gr.Row(visible=(is_rf or is_rf_denoiser) and default_dist_shift_type == "LogSNR") as logsnr_params_row:
logsnr_anchor_length_slider = gr.Slider(minimum=100, maximum=10000, step=100, value=default_logsnr_params["anchor_length"], label="Anchor length")
logsnr_anchor_logsnr_slider = gr.Slider(minimum=-12.0, maximum=0.0, step=0.1, value=default_logsnr_params["anchor_logsnr"], label="Anchor log-SNR")
logsnr_rate_slider = gr.Slider(minimum=-2.0, maximum=2.0, step=0.1, value=default_logsnr_params["rate"], label="Rate")
logsnr_end_slider = gr.Slider(minimum=-2.0, maximum=6.0, step=0.1, value=default_logsnr_params["logsnr_end"], label="log-SNR end")
with gr.Row(visible=(is_rf or is_rf_denoiser) and default_dist_shift_type == "Flux") as flux_params_row:
flux_min_length_slider = gr.Slider(minimum=1, maximum=10000, step=1, value=default_flux_params["min_length"], label="Min seq len")
flux_max_length_slider = gr.Slider(minimum=1, maximum=10000, step=1, value=default_flux_params["max_length"], label="Max seq len")
flux_alpha_min_slider = gr.Slider(minimum=0.1, maximum=20.0, step=0.1, value=default_flux_params["alpha_min"], label="Alpha min")
flux_alpha_max_slider = gr.Slider(minimum=0.1, maximum=20.0, step=0.1, value=default_flux_params["alpha_max"], label="Alpha max")
with gr.Row(visible=(is_rf or is_rf_denoiser) and default_dist_shift_type == "Full") as full_params_row:
full_base_shift_slider = gr.Slider(minimum=0.0, maximum=5.0, step=0.05, value=default_full_params["base_shift"], label="Base shift")
full_max_shift_slider = gr.Slider(minimum=0.0, maximum=5.0, step=0.05, value=default_full_params["max_shift"], label="Max shift")
full_min_length_slider = gr.Slider(minimum=1, maximum=10000, step=1, value=default_full_params["min_length"], label="Min length")
full_max_length_slider = gr.Slider(minimum=1, maximum=10000, step=1, value=default_full_params["max_length"], label="Max length")
# Per-type slider groups for wiring to state
logsnr_sliders = [logsnr_anchor_length_slider, logsnr_anchor_logsnr_slider, logsnr_rate_slider, logsnr_end_slider]
flux_sliders = [flux_min_length_slider, flux_max_length_slider, flux_alpha_min_slider, flux_alpha_max_slider]
full_sliders = [full_base_shift_slider, full_max_shift_slider, full_min_length_slider, full_max_length_slider]
all_dist_shift_inputs = [dist_shift_type_dropdown] + logsnr_sliders + flux_sliders + full_sliders
def update_dist_shift_state(shift_type, *params):
"""Route the 4 relevant params to build_dist_shift based on type."""
type_to_slice = {"LogSNR": params[0:4], "Flux": params[4:8], "Full": params[8:12]}
p = type_to_slice.get(shift_type, (0, 0, 0, 0))
return (
build_dist_shift(shift_type, *p),
gr.update(visible=((is_rf or is_rf_denoiser) and (shift_type == "LogSNR"))),
gr.update(visible=((is_rf or is_rf_denoiser) and (shift_type == "Flux"))),
gr.update(visible=((is_rf or is_rf_denoiser) and (shift_type == "Full"))),
)
for component in all_dist_shift_inputs:
component.change(
update_dist_shift_state,
inputs=all_dist_shift_inputs,
outputs=[dist_shift_state, logsnr_params_row, flux_params_row, full_params_row],
)
with gr.Accordion("Batch", open=False):
batch_size_number = gr.Number(
label="Batch size",
value=1,
minimum=1,
precision=0,
info="Generate multiple variations in one run.",
)
with gr.Accordion("Output params", open=False):
# Output params
with gr.Row():
file_format_dropdown = gr.Dropdown(["wav", "flac", "mp3 320k", "mp3 v0", "mp3 128k", "m4a aac_he_v2 64k", "m4a aac_he_v2 32k"], label="File format", value="wav")
file_naming_dropdown = gr.Dropdown(["verbose", "prompt", "output.wav"], label="File naming", value="verbose") # ,"prompt","verbose"
preview_every_slider = gr.Slider(minimum=0, maximum=100, step=1, value=0, label="Spec Preview Every")
cut_to_seconds_total_checkbox = gr.Checkbox(label="Cut to seconds total", value=True)
autoplay_checkbox = gr.Checkbox(label="Autoplay", value=False, elem_id="autoplay")
infinite_radio_checkbox = gr.Checkbox(label="Infinite Radio", value=False, elem_id="infinite-radio")
automatic_download_checkbox = gr.Checkbox(label="Auto Download", value=False, elem_id="automatic-download")
# Default generation tab
with gr.Accordion("Init audio", open=False):
init_audio_input = gr.Audio(label="Init audio", waveform_options=gr.WaveformOptions(show_recording_waveform=False))
min_noise_level = 0.01
max_noise_level = 1.0
default_noise_level = 0.9 # roughly halfway style transfer values
if is_rf:
choices = ["Init audio","RF-Inversion"]
else:
choices = ["Init audio"]
init_audio_type_radio = gr.Radio(label="Techniques", choices=choices, value=choices[0], visible=len(choices)>1)
with gr.Column(visible=True) as interface_a:
init_noise_level_slider = gr.Slider(minimum=min_noise_level, maximum=max_noise_level, step=0.01, value=default_noise_level, label="Init noise level")
with gr.Column(visible=False) as interface_b:
inversion_steps_slider = gr.Slider(minimum=1, maximum=500, step=1, value=100, label="Inversion Steps")
inversion_gamma_slider = gr.Slider(minimum=0, maximum=1, step=0.1, value=0, label="Gamma", visible=True)
inversion_unconditional_checkbox = gr.Checkbox(label="Unconditional", value=False)
gr.HTML("<div style='opacity: 0.5; padding: 0px'>For reproduction, try empty prompt, cfg 1, gamma .3<br>\
For prompt re-stylization, try cfg 1-7, gamma 0-.15, unconditional</div>")
def init_audio_type_switch(choice):
return (
gr.update(visible=(choice == "Init audio")),
gr.update(visible=(choice == "RF-Inversion"))
)
init_audio_type_radio.change(init_audio_type_switch, inputs=init_audio_type_radio, outputs=[interface_a, interface_b])
with gr.Accordion("Inpainting", open=False):
inpaint_audio_input = gr.Audio(label="Inpaint audio", waveform_options=gr.WaveformOptions(show_recording_waveform=False))
mask_maskstart_slider = gr.Textbox(label="Mask starts (sec)", placeholder="4 or 4, 16", info="Comma-separate values to inpaint multiple regions.")
mask_maskend_slider = gr.Textbox(label="Mask ends (sec)", placeholder="8 or 8, 20")
inputs = [
prompt,
negative_prompt,
seconds_total_slider,
cfg_scale_slider,
steps_slider,
preview_every_slider,
seed_textbox,
sampler_type_dropdown,
sigma_max_slider,
cfg_interval_min_slider,
cfg_interval_max_slider,
cfg_rescale_slider,
cfg_norm_threshold,
apg_scale_slider,
file_format_dropdown,
file_naming_dropdown,
cut_to_seconds_total_checkbox,
init_audio_input,
init_noise_level_slider,
mask_maskstart_slider,
mask_maskend_slider,
inpaint_audio_input,
init_audio_type_radio,
inversion_steps_slider,
inversion_gamma_slider,
inversion_unconditional_checkbox,
duration_padding_slider,
batch_size_number,
dist_shift_state,
] + lora_ui_inputs
with gr.Column():
audio_output = gr.Audio(label="Output audio", interactive=False,
waveform_options=gr.WaveformOptions(show_recording_waveform=False))
audio_spectrogram_output = gr.Gallery(label="Output spectrogram", show_label=False)
send_to_init_button = gr.Button("Send to init audio", scale=1)
send_to_init_button.click(fn=lambda audio: audio, inputs=[audio_output], outputs=[init_audio_input])
send_to_inpaint_button = gr.Button("Send to inpaint audio", scale=1)
send_to_inpaint_button.click(fn=lambda audio: audio, inputs=[audio_output], outputs=[inpaint_audio_input])
generate_button.click(fn=generate_cond,
inputs=inputs,
outputs=[
audio_output,
audio_spectrogram_output
],
api_name="generate")
def _prompt_assistant_or_download(text, progress=gr.Progress(track_tqdm=True)):
if not _reprompt_is_model_cached(_reprompt_model_id):
_reprompt_get_model(_reprompt_model_id)
return text, gr.update(), gr.update(value="Prompt Assistant")
_, result, _ = _reprompt_fn(text, "Auto", "", _reprompt_model_id, 128, 1.11)
m = _LENGTH_EXTRACT_RE.search(result)
if m:
max_seconds = sample_size // sample_rate
seconds = min(int(m.group(1)), max_seconds)
result = result[:m.start()]
else:
seconds = gr.update()
return result, seconds, gr.update()
prompt_assistant_button.click(
fn=_prompt_assistant_or_download,
inputs=[prompt],
outputs=[prompt, seconds_total_slider, prompt_assistant_button],
concurrency_limit=1,
)
def create_diffusion_cond_ui(model, gradio_title="", default_prompt=None):
global sample_size, sample_rate, stable_audio_3_model
sample_size = model.model_config["sample_size"]
sample_rate = model.model_config["sample_rate"]
stable_audio_3_model = model
js ="""function run_javascript_on_page_load(){
const generateBtn = Array.from(document.querySelectorAll('button'))
.find(btn => btn.innerText.trim() === 'Generate');
function getAudioOutputPlayer () {
return [...document.querySelectorAll('label')].find(label => label.textContent.trim() === 'Output audio')?.parentElement.querySelector('audio');
}
const infiniteRadio = document.querySelector('#infinite-radio input[type="checkbox"]');
const autoplay = document.querySelector('#autoplay input[type="checkbox"]');
const automaticDownload = document.querySelector('#automatic-download input[type="checkbox"]');
let radioAutoStart = false;
let listenersSetup = false;
const setupListeners = () => {
const audioEl = getAudioOutputPlayer();
if (!audioEl) return;
audioEl.addEventListener('loadedmetadata', () => {
if(automaticDownload.checked){
downloadAudio(audioEl);
}
if(autoplay.checked || radioAutoStart){
audioEl.play();
radioAutoStart = false;
}
if(infiniteRadio.checked){
audioEl.addEventListener('timeupdate', function checkAudioEnd() {
// Can set window.headstart (seconds) in the dev console if you want to start generating before the song is over
let headstart = 1;
if(window.headstart) headstart = window.headstart;
if (audioEl.duration - audioEl.currentTime <= headstart) {
generateBtn.click();
radioAutoStart = true;
audioEl.removeEventListener('timeupdate', checkAudioEnd);
}
});
}
});
listenersSetup = true;
};
generateBtn.addEventListener('click', () => {
if(listenersSetup) return;
const interval = setInterval(() => {
console.log("...")
const audioEl = document.querySelector('audio');
if (audioEl?.src && audioEl.src !== window.location.href) {
setupListeners();
clearInterval(interval);
}
}, 100);
});
// Respond to >> button on MacBookPro and on steering wheel during CarPlay
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('nexttrack', () => generateBtn.click());
navigator.mediaSession.setActionHandler('play', () => getAudioOutputPlayer()?.play());
navigator.mediaSession.setActionHandler('pause', () => getAudioOutputPlayer()?.pause());
}
// Automatic Download
function downloadAudio(audioEl) {
const audioSrc = audioEl.src;
const link = document.createElement('a');
link.href = audioSrc;
link.download = audioSrc.substring(audioSrc.lastIndexOf('/') + 1);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
"""
with gr.Blocks() as ui:
ui._sao_js = js
ui._sao_theme = gr.themes.Base()
if gradio_title:
gr.Markdown("### %s" % gradio_title)
with gr.Tab("Generation"):
create_sampling_ui(model, default_prompt=default_prompt)
# JavaScript to autoplay audio immediately after generation (if autoplay enabled)
return ui