Spaces:
Running on Zero
Running on Zero
File size: 4,760 Bytes
c7f05a9 46d55e6 c7f05a9 46d55e6 c7f05a9 46d55e6 c7f05a9 46d55e6 c7f05a9 46d55e6 c7f05a9 46d55e6 c7f05a9 550d35b c7f05a9 46d55e6 c7f05a9 46d55e6 c7f05a9 46d55e6 | 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 | # -*- coding: utf-8 -*-
"""
MÚSICA — ACE-Step 1.5 XL Turbo, no formato DIFFUSERS.
É a primeira vez que este app tem MÚSICA de verdade sem provedor pago: a
trilha do `midia_livre` é síntese de acordes (e a tela sempre disse isso);
o ACE-Step compõe — com letra, inclusive. Quando o worker está de pé, é ele
quem responde "music"; quando dorme, a trilha sintetizada volta a valer, e a
diferença de qualidade é o preço anunciado da GPU gratuita.
POR QUE DIFFUSERS E NÃO O PACOTE `acestep`, decidido na primeira validação
real: o pacote do projeto crava `transformers==4.50.0`, e o Z-Image/Qwen3-TTS
exigem >=4.51 — no mesmo processo os dois não se resolvem (pip cai em
ResolutionImpossible). O projeto publica o checkpoint OFICIAL em formato
diffusers (`acestep-v15-xl-turbo-diffusers`), carregável pelo
`AceStepPipeline` que entrou no diffusers 0.38 — e o text_encoder dele é
justamente um Qwen3Model, o mesmo stack dos outros modelos. Um requirements
só, os quatro tipos juntos. (O id antigo `ACE-Step/ACE-Step-v1.5` nem existe
no Hub — 404 medido.)
`lyrics` aceita o formato de seções do projeto ([Verse]/[Chorus]); sem letra
vai "[instrumental]" — mandar string vazia faz alguns checkpoints alucinarem
voz. O turbo é destilado: 8 passos, sem CFG para ajustar.
"""
from __future__ import annotations
import io
import logging
import os
from typing import Dict, Optional
from . import base
logger = logging.getLogger("worker.ace_step")
VRAM_MINIMA_GB = 12
_pipe = None
_modelo_carregado = ""
def _modelo() -> str:
return os.getenv("MUSIC_MODEL",
"ACE-Step/acestep-v15-xl-turbo-diffusers").strip()
def baixar(progresso: base.Progresso) -> None:
base.baixar_repo(_modelo(), progresso)
def carregar(progresso: base.Progresso) -> None:
global _pipe, _modelo_carregado
if base.modo_mock():
_pipe = "mock"
return
if _pipe is not None and _modelo_carregado == _modelo():
return
progresso(None, "Carregando o modelo de música…")
import torch
from diffusers import AceStepPipeline
_pipe = AceStepPipeline.from_pretrained(
_modelo(), torch_dtype=torch.bfloat16,
token=os.getenv("HF_TOKEN") or None)
# Tiling no VAE: decodificar minutos de áudio 48 kHz de uma vez é o pico
# de memória do pipeline — o card do modelo recomenda para áudio longo.
_pipe.vae.enable_tiling()
livre = base.vram_livre_gb()
if livre is not None and livre < 16:
_pipe.enable_model_cpu_offload()
else:
_pipe.to(base.dispositivo())
_modelo_carregado = _modelo()
def gerar(params: Dict, progresso: base.Progresso) -> base.Saida:
if base.modo_mock():
return base.wav_silencio(0.5), "audio/wav"
prompt = (params.get("prompt") or "").strip()
estilo = (params.get("style") or "").strip()
if estilo:
prompt = f"{estilo}, {prompt}"
letra = (params.get("lyrics") or "").strip() or "[instrumental]"
# Duração com teto: cada segundo é tempo de GPU, e um pedido de "3600"
# vindo de um cliente forjado não pode virar uma hora de placa ocupada.
duracao = max(10, min(int(params.get("duration") or 60),
int(os.getenv("MUSIC_MAX_SEGUNDOS", "240"))))
progresso(5, "Compondo…")
saida = _pipe(prompt=prompt, lyrics=letra,
audio_duration=float(duracao),
generator=base.semente(params))
progresso(95, "Finalizando…")
# audios[0] é (canais, amostras) em 48 kHz; o WAV sai estéreo.
return _wav_estereo(saida.audios[0],
getattr(_pipe, "sample_rate", 48000)), "audio/wav"
def descarregar() -> None:
global _pipe
_pipe = None
base.limpar_vram()
def config_menor(params: Dict) -> Optional[Dict]:
p = dict(params)
dur = int(p.get("duration") or 60)
if dur > 30:
p["duration"] = 30
return p
return None
def _wav_estereo(onda, taxa: int) -> bytes:
"""Tensor (canais, amostras) → WAV 16-bit. struct, sem soundfile —
uma dependência a menos, como no TTS."""
import struct
try:
dados = onda.detach().float().cpu().numpy()
except AttributeError:
dados = onda
if dados.ndim == 1:
dados = dados.reshape(1, -1)
canais = dados.shape[0]
# Intercala os canais (LRLR…), que é como o WAV guarda estéreo.
inteiros = (dados.T.clip(-1, 1) * 32767).astype("<i2").tobytes()
bloco = canais * 2
return (b"RIFF" + struct.pack("<I", 36 + len(inteiros)) + b"WAVEfmt "
+ struct.pack("<IHHIIHH", 16, 1, canais, int(taxa),
int(taxa) * bloco, bloco, 16)
+ b"data" + struct.pack("<I", len(inteiros)) + inteiros)
|