generated from dellevin/template
11111
This commit is contained in:
273
flask-dev-api/blueprints/audio_slicer.py
Normal file
273
flask-dev-api/blueprints/audio_slicer.py
Normal file
@@ -0,0 +1,273 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
audio-slicer 音频分割蓝图
|
||||
基于 RMS 静音检测自动切割音频
|
||||
"""
|
||||
import os
|
||||
import io
|
||||
import json
|
||||
import uuid
|
||||
import zipfile
|
||||
import threading
|
||||
import tempfile
|
||||
from flask import Blueprint, render_template, request, jsonify, send_file, after_this_request
|
||||
import soundfile
|
||||
|
||||
try:
|
||||
from config import BASE_DIR
|
||||
except ImportError:
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from utils.audio_slicer_core import analyze_audio, write_slice_range
|
||||
|
||||
bp = Blueprint('audio_slicer', __name__, url_prefix='/audio-slicer')
|
||||
|
||||
AUDIO_EXTS = ('.wav', '.flac', '.ogg', '.mp3', '.aac', '.m4a', '.wma', '.aiff', '.opus')
|
||||
CONFIG_PATH = os.path.join(BASE_DIR, 'config', 'audio_slicer_config.json')
|
||||
|
||||
DEFAULT_PARAMS = {
|
||||
'threshold': -40, 'min_length': 5000, 'min_interval': 300,
|
||||
'hop_size': 10, 'max_sil_kept': 1000,
|
||||
}
|
||||
|
||||
|
||||
def _load_config():
|
||||
cfg = dict(DEFAULT_PARAMS)
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
try:
|
||||
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
cfg.update(json.load(f))
|
||||
except Exception:
|
||||
pass
|
||||
return cfg
|
||||
|
||||
|
||||
def _save_config(cfg):
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 任务缓存: task_id -> {src, sr, ch, total, duration, ranges, settings, slices, status, progress, error, orig_name}
|
||||
_tasks = {}
|
||||
|
||||
|
||||
def _audio_info(path):
|
||||
with soundfile.SoundFile(path) as f:
|
||||
sr = f.samplerate
|
||||
ch = f.channels
|
||||
total = len(f)
|
||||
duration = total / sr
|
||||
return {'sample_rate': sr, 'channels': ch, 'total_samples': total, 'duration': round(duration, 2)}
|
||||
|
||||
|
||||
@bp.route('/')
|
||||
def page():
|
||||
cfg = _load_config()
|
||||
return render_template('audio_slicer.html', config=cfg)
|
||||
|
||||
|
||||
@bp.route('/config', methods=['GET'])
|
||||
def get_config():
|
||||
return jsonify(_load_config())
|
||||
|
||||
|
||||
@bp.route('/config', methods=['POST'])
|
||||
def save_config():
|
||||
data = request.get_json()
|
||||
cfg = _load_config()
|
||||
for key in DEFAULT_PARAMS:
|
||||
if key in data:
|
||||
cfg[key] = data[key]
|
||||
_save_config(cfg)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@bp.route('/upload', methods=['POST'])
|
||||
def upload():
|
||||
if 'audio' not in request.files:
|
||||
return jsonify({'success': False, 'error': '请上传音频文件'}), 400
|
||||
f = request.files['audio']
|
||||
if not f.filename:
|
||||
return jsonify({'success': False, 'error': '请上传音频文件'}), 400
|
||||
ext = os.path.splitext(f.filename)[1].lower()
|
||||
if ext not in AUDIO_EXTS:
|
||||
return jsonify({'success': False, 'error': f'不支持的格式: {ext}'}), 400
|
||||
|
||||
task_id = uuid.uuid4().hex
|
||||
save_path = os.path.join(tempfile.gettempdir(), f'aslicer_{task_id}{ext}')
|
||||
f.save(save_path)
|
||||
try:
|
||||
info = _audio_info(save_path)
|
||||
except Exception as e:
|
||||
try:
|
||||
os.remove(save_path)
|
||||
except OSError:
|
||||
pass
|
||||
return jsonify({'success': False, 'error': f'无法读取音频: {e}'}), 400
|
||||
|
||||
_tasks[task_id] = {
|
||||
'src': save_path, 'orig_name': f.filename,
|
||||
'sr': info['sample_rate'], 'ch': info['channels'],
|
||||
'total': info['total_samples'], 'duration': info['duration'],
|
||||
'ranges': None, 'slices': None,
|
||||
'status': 'uploaded', 'progress': 0, 'error': None,
|
||||
}
|
||||
return jsonify({'success': True, 'task_id': task_id, 'info': info, 'filename': f.filename})
|
||||
|
||||
|
||||
@bp.route('/analyze', methods=['POST'])
|
||||
def analyze():
|
||||
data = request.get_json()
|
||||
task_id = data.get('task_id')
|
||||
task = _tasks.get(task_id)
|
||||
if not task:
|
||||
return jsonify({'success': False, 'error': '任务不存在'}), 404
|
||||
|
||||
settings = {
|
||||
'threshold': float(data.get('threshold', -40)),
|
||||
'min_length': int(data.get('min_length', 5000)),
|
||||
'min_interval': int(data.get('min_interval', 300)),
|
||||
'hop_size': int(data.get('hop_size', 10)),
|
||||
'max_sil_kept': int(data.get('max_sil_kept', 1000)),
|
||||
}
|
||||
task['settings'] = settings
|
||||
|
||||
try:
|
||||
ranges, sr, ch, total = analyze_audio(task['src'], settings)
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'分析失败: {e}'}), 500
|
||||
|
||||
task['ranges'] = ranges
|
||||
task['sr'] = sr
|
||||
task['ch'] = ch
|
||||
task['status'] = 'analyzed'
|
||||
|
||||
preview = []
|
||||
for i, (begin, end) in enumerate(ranges):
|
||||
b, e = int(begin), int(end)
|
||||
dur = (e - b) / sr
|
||||
preview.append({'index': i, 'duration': round(dur, 2), 'samples': e - b})
|
||||
|
||||
return jsonify({
|
||||
'success': True, 'count': len(ranges), 'preview': preview,
|
||||
'sample_rate': int(sr), 'channels': int(ch),
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/slice', methods=['POST'])
|
||||
def start_slice():
|
||||
data = request.get_json()
|
||||
task_id = data.get('task_id')
|
||||
task = _tasks.get(task_id)
|
||||
if not task:
|
||||
return jsonify({'success': False, 'error': '任务不存在'}), 404
|
||||
if not task.get('ranges'):
|
||||
return jsonify({'success': False, 'error': '请先分析'}), 400
|
||||
|
||||
task['status'] = 'slicing'
|
||||
task['progress'] = 0
|
||||
task['slices'] = []
|
||||
task['error'] = None
|
||||
|
||||
out_dir = os.path.join(tempfile.gettempdir(), f'aslicer_out_{task_id}')
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
def _do_slice():
|
||||
base = os.path.splitext(task['orig_name'])[0]
|
||||
ranges = task['ranges']
|
||||
total = len(ranges)
|
||||
for i, (begin, end) in enumerate(ranges):
|
||||
out_path = os.path.join(out_dir, f'{base}_{i:03d}.wav')
|
||||
try:
|
||||
write_slice_range(task['src'], out_path, task['sr'], task['ch'], begin, end)
|
||||
task['slices'].append({
|
||||
'index': i, 'path': out_path,
|
||||
'filename': f'{base}_{i:03d}.wav',
|
||||
'duration': round((end - begin) / task['sr'], 2),
|
||||
})
|
||||
except Exception as e:
|
||||
task['error'] = f'切片 {i} 写入失败: {e}'
|
||||
task['status'] = 'error'
|
||||
return
|
||||
task['progress'] = round((i + 1) / total * 100)
|
||||
task['status'] = 'done'
|
||||
task['progress'] = 100
|
||||
|
||||
threading.Thread(target=_do_slice, daemon=True).start()
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@bp.route('/status/<task_id>')
|
||||
def status(task_id):
|
||||
task = _tasks.get(task_id)
|
||||
if not task:
|
||||
return jsonify({'success': False, 'error': '任务不存在'}), 404
|
||||
resp = {
|
||||
'success': True, 'status': task['status'], 'progress': task['progress'],
|
||||
}
|
||||
if task['status'] == 'done':
|
||||
resp['slices'] = task['slices']
|
||||
resp['count'] = len(task['slices'])
|
||||
elif task['status'] == 'error':
|
||||
resp['error'] = task.get('error', '未知错误')
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@bp.route('/download/<task_id>/<int:index>')
|
||||
def download_one(task_id, index):
|
||||
task = _tasks.get(task_id)
|
||||
if not task or not task.get('slices'):
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
slices = task['slices']
|
||||
if index < 0 or index >= len(slices):
|
||||
return jsonify({'error': '索引越界'}), 404
|
||||
path = slices[index]['path']
|
||||
if not os.path.exists(path):
|
||||
return jsonify({'error': '文件不存在'}), 404
|
||||
return send_file(path, mimetype='audio/wav', as_attachment=True,
|
||||
download_name=slices[index]['filename'])
|
||||
|
||||
|
||||
@bp.route('/download-all/<task_id>')
|
||||
def download_all(task_id):
|
||||
task = _tasks.get(task_id)
|
||||
if not task or not task.get('slices'):
|
||||
return jsonify({'error': '无切片可下载'}), 404
|
||||
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
for s in task['slices']:
|
||||
if os.path.exists(s['path']):
|
||||
zf.write(s['path'], s['filename'])
|
||||
buf.seek(0)
|
||||
|
||||
base = os.path.splitext(task['orig_name'])[0]
|
||||
return send_file(buf, mimetype='application/zip', as_attachment=True,
|
||||
download_name=f'{base}_slices.zip')
|
||||
|
||||
|
||||
@bp.route('/cleanup/<task_id>', methods=['POST'])
|
||||
def cleanup(task_id):
|
||||
"""清理临时文件"""
|
||||
task = _tasks.pop(task_id, None)
|
||||
if not task:
|
||||
return jsonify({'success': True})
|
||||
try:
|
||||
if task.get('src') and os.path.exists(task['src']):
|
||||
os.remove(task['src'])
|
||||
except OSError:
|
||||
pass
|
||||
out_dir = os.path.join(tempfile.gettempdir(), f'aslicer_out_{task_id}')
|
||||
if os.path.isdir(out_dir):
|
||||
for f in os.listdir(out_dir):
|
||||
try:
|
||||
os.remove(os.path.join(out_dir, f))
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
os.rmdir(out_dir)
|
||||
except OSError:
|
||||
pass
|
||||
return jsonify({'success': True})
|
||||
Reference in New Issue
Block a user