修复微软edge-tts报403的bug,优化UI界面
This commit is contained in:
1 parent
e39f9025ce
commit
b081bba33c
6 files changed
+1242
-1170
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user