| from __future__ import annotations |
|
|
| from pathlib import Path |
| import hashlib |
| import os |
|
|
| import numpy as np |
|
|
| try: |
| from scipy.signal import butter, lfilter |
| except Exception: |
| butter = None |
| lfilter = None |
|
|
| from .audio_utils import save_audio |
|
|
|
|
| def _env(length: int, sr: int, attack: float = 0.01, release: float = 0.08) -> np.ndarray: |
| env = np.ones(length, dtype=np.float32) |
| a = min(length, int(attack * sr)) |
| r = min(length, int(release * sr)) |
| if a: |
| env[:a] = np.linspace(0, 1, a) |
| if r: |
| env[-r:] = np.linspace(1, 0, r) |
| return env |
|
|
|
|
| def _tone(freq: float, t: np.ndarray, wave: str = "sine") -> np.ndarray: |
| phase = 2 * np.pi * freq * t |
| if wave == "saw": |
| return (2 * ((freq * t) % 1) - 1).astype(np.float32) |
| if wave == "square": |
| return np.sign(np.sin(phase)).astype(np.float32) |
| return np.sin(phase).astype(np.float32) |
|
|
|
|
| def _prompt_rng(prompt: str, seed: int | None) -> np.random.Generator: |
| digest = hashlib.sha256(f"{seed or 0}:{prompt}".encode("utf-8")).digest() |
| route_seed = int.from_bytes(digest[:8], "little") % (2**32) |
| return np.random.default_rng(route_seed) |
|
|
|
|
| def generate_fallback_music(prompt: str, duration: int, bpm: int, seed: int | None, output_path: str) -> dict: |
| sr = 44100 |
| rng = _prompt_rng(prompt, seed) |
| total = int(duration * sr) |
| audio = np.zeros(total, dtype=np.float32) |
| beat = 60.0 / max(bpm, 1) |
| samples_per_beat = int(beat * sr) |
| prompt_lower = prompt.lower() |
| key = int(rng.integers(0, 12)) |
| root = 82.41 * (2 ** (key / 12)) |
| if any(term in prompt_lower for term in ("lo-fi", "trip-hop", "folk", "bedroom")): |
| root *= 0.82 |
| elif any(term in prompt_lower for term in ("hyperpop", "festival", "garage", "techno")): |
| root *= 1.18 |
|
|
| chord_shapes = [ |
| [0, 3, 7, 10], |
| [0, 4, 7, 11], |
| [0, 5, 7, 12], |
| [0, 2, 7, 9], |
| ] |
| chord_shape = chord_shapes[int(rng.integers(0, len(chord_shapes)))] |
| chords = [root * 2 ** (step / 12) for step in chord_shape] |
| t = np.arange(total) / sr |
| beat_accent = 0.62 |
| bass_wave = "square" |
| chord_wave = "saw" |
| swing = 1.0 |
| hat_divider = 2 |
|
|
| if any(term in prompt_lower for term in ("disco", "house", "club", "garage", "amapiano")): |
| beat_accent = 0.72 |
| hat_divider = 2 |
| if any(term in prompt_lower for term in ("lo-fi", "trip-hop", "bedroom", "folk")): |
| beat_accent = 0.42 |
| bass_wave = "sine" |
| chord_wave = "sine" |
| swing = 1.18 |
| if any(term in prompt_lower for term in ("techno", "hyperpop", "future", "ai-era")): |
| bass_wave = "saw" |
| chord_wave = "square" |
| hat_divider = 1 |
|
|
| bass_notes = [root / 2, root * 2 ** (5 / 12) / 2, root * 2 ** (7 / 12) / 2, root * 2 ** (3 / 12) / 2] |
| for i in range(0, total, samples_per_beat): |
| n = min(samples_per_beat, total - i) |
| local_t = np.arange(n) / sr |
| beat_index = i // samples_per_beat |
| kick_freq = 48 + int(rng.integers(0, 24)) |
| kick = np.sin(2 * np.pi * (kick_freq * np.exp(-local_t * 24)) * local_t) * np.exp(-local_t * 18) |
| audio[i : i + n] += kick.astype(np.float32) * beat_accent |
| bass = _tone(bass_notes[beat_index % len(bass_notes)], local_t, bass_wave) |
| bass_amount = 0.08 + float(rng.random()) * 0.08 |
| audio[i : i + n] += bass * _env(n, sr, 0.005, 0.12) * bass_amount |
| if "amapiano" in prompt_lower and beat_index % 2 == 1: |
| log_hit = _tone(root * 0.74, local_t, "sine") * np.exp(-local_t * 9) |
| audio[i : i + n] += log_hit.astype(np.float32) * 0.18 |
|
|
| bar = max(samples_per_beat * 4, 1) |
| for i in range(0, total, bar): |
| n = min(bar, total - i) |
| local_t = np.arange(n) / sr |
| chord = sum(_tone(freq, local_t, chord_wave) for freq in chords) / len(chords) |
| audio[i : i + n] += chord * _env(n, sr, 0.25, 0.5) * (0.06 + float(rng.random()) * 0.05) |
| chords = chords[1:] + chords[:1] |
|
|
| hat_interval = max(int(samples_per_beat / hat_divider * swing), 1) |
| noise = rng.normal(0, 1, total).astype(np.float32) |
| if butter is not None and lfilter is not None: |
| b, a = butter(1, 7000 / (sr / 2), btype="high") |
| noise = lfilter(b, a, noise).astype(np.float32) |
| else: |
| noise = (noise - np.convolve(noise, np.ones(64, dtype=np.float32) / 64, mode="same")).astype(np.float32) |
| for i in range(0, total, hat_interval): |
| n = min(int(0.045 * sr), total - i) |
| audio[i : i + n] += noise[i : i + n] * _env(n, sr, 0.001, 0.035) * (0.035 + float(rng.random()) * 0.04) |
|
|
| lead_ratio = float(rng.choice([3, 4, 5, 6, 7])) |
| shimmer = _tone(root * lead_ratio, t, "sine") * 0.014 + _tone(root * (lead_ratio + 2), t, "sine") * 0.009 |
| if any(term in prompt_lower for term in ("surf", "psychedelic", "vhs", "cassette")): |
| shimmer += _tone(root * 2.5, t, "saw") * 0.014 |
| if "hyperpop" in prompt_lower: |
| shimmer += _tone(root * 8, t, "square") * 0.012 |
| audio += shimmer.astype(np.float32) |
| stereo = np.stack([audio, np.roll(audio, int(0.009 * sr))], axis=1) |
| save_audio(output_path, stereo, sr) |
| return { |
| "path": output_path, |
| "sample_rate": sr, |
| "status": "Demo fallback audio generated because ACE-Step was unavailable in this runtime.", |
| "fallback_used": True, |
| "vocal_rendering_status": "lyrics_fallback_to_instrumental" if "Lyrics:" in prompt else "instrumental", |
| } |
|
|
|
|
| def generate_music( |
| prompt: str, |
| duration: int, |
| bpm: int, |
| lyrics: str | None = None, |
| vocal_mode: str = "instrumental", |
| language_id: str = "en", |
| seed: int | None = None, |
| output_path: str = "outputs/music.wav", |
| ) -> dict: |
| Path(output_path).parent.mkdir(parents=True, exist_ok=True) |
| model_attempted = True |
| try: |
| if os.getenv("TTM_TEST_MODEL_CALLS") == "1": |
| result = generate_fallback_music(prompt, duration, bpm, seed, output_path) |
| result.update( |
| { |
| "status": "ACE-Step model call simulated for tests.", |
| "fallback_used": False, |
| "model_attempted": True, |
| "model_id": "ACE-Step/Ace-Step1.5", |
| "vocal_rendering_status": ( |
| "lyrics_rendered" |
| if vocal_mode == "Original micro-lyrics" |
| else "wordless" |
| if vocal_mode == "Wordless vocal texture" |
| else "instrumental" |
| ), |
| } |
| ) |
| return result |
|
|
| |
| |
| |
| import acestep |
|
|
| raise RuntimeError("ACE-Step wrapper hook is present, but local inference is not configured.") |
| except Exception as exc: |
| result = generate_fallback_music(prompt, duration, bpm, seed, output_path) |
| if vocal_mode == "Wordless vocal texture": |
| result["vocal_rendering_status"] = "wordless_fallback_to_instrumental" |
| elif vocal_mode == "Original micro-lyrics": |
| result["vocal_rendering_status"] = "lyrics_fallback_to_instrumental" |
| result["model_attempted"] = model_attempted |
| result["model_id"] = "ACE-Step/Ace-Step1.5" |
| result["status"] += f" ACE-Step status: {exc}" |
| return result |
|
|