修复微软edge-tts报403的bug,优化UI界面

This commit is contained in:
Cx330 committed 2026-10-13 18:13:47 +08:00
1 parent e39f9025ce
commit b081bba33c
6 files changed
+1242 -1170

No files matched your search

+111 -74
View File
@@ -3,6 +3,7 @@ import subprocess
import os
import uuid
import json
import time
app = Flask(__name__)
@@ -32,6 +33,52 @@ voices = {
current_process = None
# 当前播放状态,供 /status 告诉界面「喇叭是不是正在响」。
playback = {'mode': None, 'filename': None, 'interval': None, 'started_at': None}
def _playback_alive():
"""喇叭是否真的在响。以进程为准,避免状态变量和实际情况脱节。"""
if current_process is not None and current_process.poll() is None:
return True
probe = subprocess.run(['pgrep', '-x', 'mpg123'], capture_output=True, text=True)
return probe.returncode == 0
def _stop_playback():
"""停掉当前播放并清空状态。"""
global current_process
if current_process:
try:
current_process.kill()
except OSError:
pass
current_process = None
subprocess.run(['pkill', '-9', 'mpg123'])
playback.update(mode=None, filename=None, interval=None, started_at=None)
def _start_playback(filename, loop, interval):
"""播放 cache 里的某个文件;loop 为真时交给 player.sh 循环。"""
global current_process
_stop_playback()
filepath = os.path.join(CACHE_DIR, filename)
if loop:
current_process = subprocess.Popen(['bash', 'player.sh', filepath, str(interval)])
mode = 'loop'
else:
current_process = subprocess.Popen([
'mpg123', '-o', 'alsa', '-a', 'plughw:audiocodec,0', filepath
])
mode = 'once'
playback.update(mode=mode, filename=filename, interval=interval,
started_at=time.time())
def get_metadata():
if not os.path.exists(METADATA_FILE):
@@ -93,7 +140,6 @@ def index():
@app.route('/speak', methods=['POST'])
def speak():
global current_process
text = request.form.get('text')
voice = request.form.get('voice')
@@ -105,25 +151,55 @@ def speak():
clean_cache()
metadata = get_metadata()
for filename, info in metadata.items():
if info.get('text') == text and info.get('voice') == voice:
filepath = os.path.join(CACHE_DIR, filename)
if os.path.exists(filepath) and os.path.getsize(filepath) > 0:
_start_playback(filename, loop, interval)
return jsonify({'status': 'ok', 'filename': filename, 'message': '使用已存在的文件'})
filename = f"{uuid.uuid4()}.mp3"
filepath = os.path.join(CACHE_DIR, filename)
voice_name = voices.get(voice, 'zh-CN-YunxiNeural')
subprocess.run([
'python3',
'-m',
'edge_tts',
'--voice',
voice_name,
'--text',
text,
'--write-media',
filepath
])
# 合成音频。新端点会对同一 IP 限流(429),故失败后重试几次。
ok = False
err_tail = ''
for attempt in range(3):
proc = subprocess.run([
'python3',
'-m',
'edge_tts',
'--voice',
voice_name,
'--text',
text,
'--write-media',
filepath
], capture_output=True, text=True)
lines = (proc.stderr or '').strip().splitlines()
err_tail = lines[-1] if lines else ''
if proc.returncode == 0 and os.path.exists(filepath) and os.path.getsize(filepath) > 0:
ok = True
break
time.sleep(2 * (attempt + 1))
metadata = get_metadata()
display_name = text[:30] + '...' if len(text) > 30 else text
if not ok:
# 合成失败:清掉可能的空文件、不写元数据,避免把坏文件当正常广播循环播放
if os.path.exists(filepath):
os.remove(filepath)
return jsonify({
'status': 'error',
'message': '语音合成失败,微软接口未返回音频,请稍后重试。' + ((' 详情: ' + err_tail) if err_tail else '')
})
# 名字取前 30 个字;换行等连续空白折叠成一个空格,便于在列表和状态条里显示
flat = ' '.join(text.split())
display_name = flat[:30] + '...' if len(flat) > 30 else flat
metadata[filename] = {
'display_name': display_name,
'text': text,
@@ -132,40 +208,15 @@ def speak():
}
save_metadata(metadata)
if current_process:
current_process.kill()
subprocess.run(['pkill', '-9', 'mpg123'])
if loop:
current_process = subprocess.Popen([
'bash',
'player.sh',
filepath,
str(interval)
])
else:
current_process = subprocess.Popen([
'mpg123',
'-o',
'alsa',
'-a',
'plughw:audiocodec,0',
filepath
])
_start_playback(filename, loop, interval)
return jsonify({'status': 'ok', 'filename': filename})
@app.route('/stop')
def stop():
global current_process
if current_process:
current_process.kill()
current_process = None
subprocess.run(['pkill', '-9', 'mpg123'])
_stop_playback()
return jsonify({'status': 'stopped'})
@@ -195,7 +246,6 @@ def list_files():
@app.route('/play-file', methods=['POST'])
def play_file():
global current_process
filename = request.form.get('filename')
loop = request.form.get('loop') == 'true'
@@ -206,30 +256,10 @@ def play_file():
filepath = os.path.join(CACHE_DIR, filename)
if not os.path.exists(filepath):
return jsonify({'status': 'error', 'message': '文件不存在'})
if not os.path.exists(filepath) or os.path.getsize(filepath) == 0:
return jsonify({'status': 'error', 'message': '文件不存在或已损坏'})
if current_process:
current_process.kill()
subprocess.run(['pkill', '-9', 'mpg123'])
if loop:
current_process = subprocess.Popen([
'bash',
'player.sh',
filepath,
str(interval)
])
else:
current_process = subprocess.Popen([
'mpg123',
'-o',
'alsa',
'-a',
'plughw:audiocodec,0',
filepath
])
_start_playback(filename, loop, interval)
metadata = get_metadata()
display_name = metadata.get(filename, {}).get('display_name', filename)
@@ -280,7 +310,6 @@ def toggle_favorite():
@app.route('/delete-file', methods=['POST'])
def delete_file():
global current_process
filename = request.form.get('filename')
@@ -292,14 +321,7 @@ def delete_file():
if not os.path.exists(filepath):
return jsonify({'status': 'error', 'message': '文件不存在'})
if current_process:
try:
current_process.kill()
current_process = None
except:
pass
subprocess.run(['pkill', '-9', 'mpg123'])
_stop_playback()
os.remove(filepath)
@@ -311,6 +333,21 @@ def delete_file():
return jsonify({'status': 'ok', 'message': '删除成功'})
@app.route('/status')
def playback_status():
"""界面用:喇叭现在是不是在响、在播哪一条、循环间隔多少秒。"""
alive = _playback_alive()
filename = playback['filename'] if alive else None
return jsonify({
'playing': alive,
'mode': playback['mode'] if alive else None,
'filename': filename,
'display_name': get_metadata().get(filename, {}).get('display_name') if filename else None,
'interval': playback['interval'] if alive else None,
'started_at': playback['started_at'] if alive else None,
})
if __name__ == '__main__':
if not os.path.exists(CACHE_DIR):
os.makedirs(CACHE_DIR)