AI 音乐生成完成后,音频 URL 为什么会过期?一套可恢复的交付方案

AI 音乐生成完成后,音频 URL 为什么会过期?一套可恢复的交付方案

更新时间:2026-11-09

作者:Suno-API 工程团队

Suno-API 使用 Suno 官方 V6 模型。

直接答案

AI 音乐生成接口返回的音频地址,是为了尽快把文件交给调用方而生成的短期传输地址。它适合播放、下载和临时预览,但不适合作为产品的永久媒体资产。

可靠的媒体交付至少包含五个阶段:

  1. 确认歌曲任务已经完成;
  2. 获取对应类型的媒体地址;
  3. 下载并检查响应状态、文件类型和文件大小;
  4. 计算 SHA-256 并转存到自己的对象存储;
  5. 回读文件并验证哈希,再向用户标记交付完成。

如果旧地址过期,正确做法是重新获取下载地址,而不是重新生成歌曲。

为什么媒体 URL 会过期

公开文档说明,MP3、WAV、视频和封面等媒体地址默认 7 天有效。到期后,文件可能被回收,旧 URL 会返回 404。

URL 过期只表示临时传输地址失效,不代表:

  • 歌曲生成失败;
  • 原歌曲被删除;
  • 用户必须再次付费生成;
  • song_id 已经无效。

因此,业务数据库只保存一个 audio_url 是不够的。更好的做法是把"歌曲记录"和"媒体资产记录"分开。

一套推荐的数据结构

text 复制代码
generation_request
  request_id
  idempotency_key
  request_status
  created_at

song
  song_id
  request_id
  task_index
  song_status
  title
  model
  completed_at

media_asset
  asset_id
  song_id
  kind
  object_key
  content_type
  byte_size
  checksum_sha256
  source_url_fetched_at
  stored_at
  verified_at

其中:

  • song_id 用于向平台查询歌曲;
  • asset_id 是业务系统自己的稳定资产 ID;
  • object_key 指向自己的对象存储;
  • checksum_sha256 用于判断文件是否完整;
  • 上游 URL 只保留获取时间和刷新状态,不建议记录完整签名参数。

获取地址和刷新地址

获取音频地址使用公开接口:

bash 复制代码
curl -X POST https://www.suno-api.io/api/music/download-url \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"song_id":"SONG_ID","kind":"song","refresh":true}'

refresh=true 会要求重新准备媒体资源。地址过期后,可以再次调用该接口获取新地址。

需要区分两种失败:

状态 处理方式
URL 404 刷新媒体地址,不重新生成歌曲
歌曲仍在 processing 有界轮询,不重复提交生成
401 / 403 修正 Key 或权限,不自动重试
429 降低并发,等待后重试
5xx / 网络中断 指数退避,有最大次数
文件类型错误 拒绝入库并记录响应信息

下载和校验流程

不要直接从远端地址把文件流式转发给终端用户,也不要用户点击一次就重新下载一次。更稳妥的方式是:

  1. 从媒体地址下载到临时文件;
  2. 检查 HTTP 状态和 Content-Type;
  3. 一边下载一边计算 SHA-256;
  4. 先写入 .part 临时文件;
  5. 校验完成后再原子改名;
  6. 上传到自己的对象存储;
  7. 回读并比较文件大小和哈希;
  8. 写入稳定的 asset_id 和对象存储 Key。

Python 参考实现

python 复制代码
import hashlib
import os
from pathlib import Path

import requests

BASE_URL = "https://www.suno-api.io"
API_KEY = os.environ["SUNO_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}


def refresh_media_url(song_id: str, kind: str = "song") -> str:
    response = requests.post(
        f"{BASE_URL}/api/music/download-url",
        headers={**HEADERS, "Content-Type": "application/json"},
        json={"song_id": song_id, "kind": kind, "refresh": True},
        timeout=60,
    )
    response.raise_for_status()
    return response.json()["data"]["url"]


def download_and_hash(url: str, destination: Path) -> str:
    destination.parent.mkdir(parents=True, exist_ok=True)
    temporary = destination.with_suffix(destination.suffix + ".part")
    digest = hashlib.sha256()

    with requests.get(url, stream=True, timeout=(15, 180)) as response:
        response.raise_for_status()
        if not response.headers.get("Content-Type"):
            raise RuntimeError("missing Content-Type")

        with temporary.open("wb") as output:
            for chunk in response.iter_content(chunk_size=1024 * 1024):
                if chunk:
                    digest.update(chunk)
                    output.write(chunk)

    os.replace(temporary, destination)
    return digest.hexdigest()

生产环境还应限制最大文件大小,记录解析后的媒体类型,并把成功校验的文件上传到自己的对象存储。

Node.js 参考实现

javascript 复制代码
import { createHash } from "node:crypto";
import { createWriteStream } from "node:fs";
import { rename } from "node:fs/promises";
import { Readable, Transform } from "node:stream";
import { pipeline } from "node:stream/promises";

async function downloadAndHash(url, destination) {
  const response = await fetch(url, {
    signal: AbortSignal.timeout(180_000),
  });

  if (!response.ok || !response.body) {
    throw new Error(`media download failed: ${response.status}`);
  }

  const hash = createHash("sha256");
  const temporary = `${destination}.part`;
  const hasher = new Transform({
    transform(chunk, _encoding, callback) {
      hash.update(chunk);
      callback(null, chunk);
    },
  });

  await pipeline(
    Readable.fromWeb(response.body),
    hasher,
    createWriteStream(temporary),
  );

  await rename(temporary, destination);
  return hash.digest("hex");
}

URL 过期后的恢复顺序

推荐按以下顺序处理:

  1. 用相同 song_id 和 kind 请求新的下载地址;
  2. 获取成功后重新下载并校验;
  3. 如果歌曲不存在,检查账号归属和 song_id 来源;
  4. 如果歌曲仍在生成,进入有界轮询;
  5. 如果任务失败,将业务状态改为失败终态;
  6. 不因为媒体地址过期而新建生成订单。

上线验收清单

  • 完成歌曲能获取到对应媒体类型;
  • 下载响应有正确状态码和 Content-Type;
  • 使用临时文件写入,避免半文件入库;
  • 下载过程计算 SHA-256;
  • 文件转存到自有对象存储;
  • 回读文件并核对大小与哈希;
  • 数据库保存稳定资产 ID 和对象存储 Key;
  • URL 过期后能刷新,不重新生成歌曲;
  • 日志不记录完整 Key 和带签名的媒体 URL;
  • 401、403、429、5xx、URL 过期和任务失败能分别处理。

延伸阅读

相关推荐
JonaHu1 小时前
5分钟搭建帮你赚钱的 AI API 网站:从品牌门户到客户 Key 交付
人工智能·api·开发者·ai创业·acedatacloud
Warson_L2 小时前
高性能架构设计
python·go
databook3 小时前
新数据到手,先跑哪几步检查
python·数据分析
czq_26867194873 小时前
Python打卡第46天
开发语言·python
Flynt5 小时前
日涨4000星的OSINT工具,我拿Linus的用户名和一个随机字符串实测了一遍
python·安全
曲径海盗7 小时前
【Python 学习第 53 天】列表基础
python·学习笔记·编程入门
Dumbness7 小时前
群里 @ 机器人发"查订单",指令一条都没命中:微信群机器人在路由前漏掉的几步归一化
python·微信
用户1614840955458 小时前
用 M4 Pro 跑羽毛球视频 AI 分析(二):TrackNet MPS 加速 3.2 倍、卡尔曼滤波补漏与自动回合剪辑
python
用户4019925568618 小时前
基于对话树回溯的社媒评论流买方意图识别与噪音过滤实践
python