#!/usr/bin/env python3 # -*- coding: UTF-8 -*- """ 阿里云语音合成 TTS - 完整脚本(带签名) """ import base64 import hashlib import hmac import http.client import json import os import sys import time import urllib.parse from urllib import parse import uuid import argparse # ============ 配置区 ============ ALIYUN_APP_KEY = os.getenv("ALIYUN_APP_KEY") ALIYUN_ACCESS_KEY_ID = os.getenv("ALIYUN_ACCESS_KEY_ID") ALIYUN_ACCESS_KEY_SECRET = os.getenv("ALIYUN_ACCESS_KEY_SECRET") if not ALIYUN_APP_KEY or not ALIYUN_ACCESS_KEY_ID or not ALIYUN_ACCESS_KEY_SECRET: print("请设置环境变量: ALIYUN_APP_KEY, ALIYUN_ACCESS_KEY_ID, ALIYUN_ACCESS_KEY_SECRET", file=sys.stderr) sys.exit(1) REGION = "cn-shanghai" # Token 服务只有上海 # ============ 配置区结束 ============ def percent_encode(text): """RFC 3986 URL 编码""" if text is None: return None encoded = parse.quote_plus(text) return encoded.replace('+', '%20').replace('*', '%2A').replace('%7E', '~') def encode_dict(dic): """将字典参数排序并编码""" keys = dic.keys() dic_sorted = [(key, dic[key]) for key in sorted(keys)] encoded = parse.urlencode(dic_sorted) return encoded.replace('+', '%20').replace('*', '%2A').replace('%7E', '~') def get_token(): """获取访问令牌(带签名)""" # 1. 构造请求参数 params = { 'AccessKeyId': ALIYUN_ACCESS_KEY_ID, 'Action': 'CreateToken', 'Format': 'JSON', 'RegionId': REGION, 'SignatureMethod': 'HMAC-SHA1', 'SignatureNonce': str(uuid.uuid1()), 'SignatureVersion': '1.0', 'Timestamp': time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), 'Version': '2019-02-28' } # 2. 构造规范化的请求字符串 query_string = encode_dict(params) # 3. 构造待签名字符串 string_to_sign = 'GET' + '&' + percent_encode('/') + '&' + percent_encode(query_string) # 4. 计算签名 secreted_string = hmac.new( bytes(ALIYUN_ACCESS_KEY_SECRET + '&', encoding='utf-8'), bytes(string_to_sign, encoding='utf-8'), hashlib.sha1 ).digest() signature = base64.b64encode(secreted_string) signature = percent_encode(signature) # 5. 发送请求 full_url = f"http://nls-meta.{REGION}.aliyuncs.com/?Signature={signature}&{query_string}" host = f"nls-meta.{REGION}.aliyuncs.com" conn = http.client.HTTPConnection(host) conn.request("GET", full_url) resp = conn.getresponse() body = resp.read().decode() data = json.loads(body) if "Token" in data and "Id" in data["Token"]: token = data["Token"]["Id"] return token else: print(f"获取 Token 失败: {body}", file=sys.stderr) return None def tts_synthesize(text, voice="siyue", format="mp3", sample_rate=16000, output_file="/tmp/tts_output.mp3"): """语音合成""" token = get_token() if not token: return False host = f"nls-gateway-{REGION}.aliyuncs.com" # URL 编码(RFC 3986) text_encoded = percent_encode(text) url = f"/stream/v1/tts?appkey={ALIYUN_APP_KEY}&token={token}&text={text_encoded}&format={format}&sample_rate={sample_rate}&voice={voice}" conn = http.client.HTTPSConnection(host) conn.request("GET", url) resp = conn.getresponse() content_type = resp.getheader("Content-Type") body = resp.read() if "audio" in content_type: with open(output_file, "wb") as f: f.write(body) print(f"✅ 语音已保存: {output_file}") return True else: print(f"❌ 失败: {body.decode()[:300]}", file=sys.stderr) return False if __name__ == "__main__": parser = argparse.ArgumentParser(description="阿里云 TTS 语音合成") parser.add_argument("text", help="要合成的文本") parser.add_argument("-o", "--output", default="/tmp/tts_output.mp3", help="输出文件路径") parser.add_argument("-v", "--voice", default="siyue", help="声音名称") parser.add_argument("-f", "--format", default="mp3", help="音频格式") parser.add_argument("-r", "--sample-rate", default="16000", help="采样率") args = parser.parse_args() success = tts_synthesize( text=args.text, voice=args.voice, format=args.format, sample_rate=int(args.sample_rate), output_file=args.output ) sys.exit(0 if success else 1)