|
|
import json |
|
|
import tempfile |
|
|
import ffmpeg |
|
|
import os |
|
|
import wave |
|
|
from flask import Flask, request, jsonify |
|
|
from flask_cors import CORS |
|
|
from vosk import Model, KaldiRecognizer |
|
|
from flasgger import Swagger |
|
|
|
|
|
|
|
|
app = Flask(__name__) |
|
|
CORS(app) |
|
|
Swagger(app) |
|
|
|
|
|
|
|
|
MODEL_PATH = "model/vosk-model" |
|
|
print("\u2705 Đang tải model Vosk...") |
|
|
model = Model(MODEL_PATH) |
|
|
|
|
|
@app.route("/") |
|
|
def home(): |
|
|
"""API Home |
|
|
--- |
|
|
responses: |
|
|
200: |
|
|
description: API đang chạy |
|
|
""" |
|
|
return "\u2705 Vosk STT API đang chạy!" |
|
|
|
|
|
@app.route("/stt", methods=["POST"]) |
|
|
def stt(): |
|
|
"""Chuyển đổi giọng nói thành văn bản (Speech-to-Text) |
|
|
--- |
|
|
consumes: |
|
|
- multipart/form-data |
|
|
parameters: |
|
|
- in: formData |
|
|
name: file |
|
|
type: file |
|
|
required: true |
|
|
description: File âm thanh WebM (sẽ được chuyển đổi sang WAV mono PCM) |
|
|
responses: |
|
|
200: |
|
|
description: Kết quả chuyển đổi văn bản |
|
|
schema: |
|
|
type: object |
|
|
properties: |
|
|
text: |
|
|
type: string |
|
|
example: "Xin chào thế giới" |
|
|
400: |
|
|
description: Lỗi nếu file âm thanh không hợp lệ hoặc không tìm thấy |
|
|
500: |
|
|
description: Lỗi server nội bộ |
|
|
""" |
|
|
if "file" not in request.files: |
|
|
return jsonify({"error": "Không tìm thấy file âm thanh! Vui lòng gửi trường 'file'."}), 400 |
|
|
|
|
|
audio_file = request.files["file"] |
|
|
if audio_file.filename == "": |
|
|
return jsonify({"error": "Không có file được chọn!"}), 400 |
|
|
|
|
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".webm", delete=False) as temp_webm_file: |
|
|
webm_path = temp_webm_file.name |
|
|
audio_file.save(webm_path) |
|
|
|
|
|
|
|
|
if os.path.getsize(webm_path) < 100: |
|
|
os.remove(webm_path) |
|
|
return jsonify({"error": "Tệp âm thanh quá nhỏ hoặc rỗng!"}), 400 |
|
|
|
|
|
|
|
|
wav_path = tempfile.mktemp(suffix=".wav") |
|
|
try: |
|
|
|
|
|
try: |
|
|
probe = ffmpeg.probe(webm_path) |
|
|
if 'streams' not in probe or not any(s['codec_type'] == 'audio' for s in probe['streams']): |
|
|
raise ValueError("Tệp không chứa luồng âm thanh hợp lệ!") |
|
|
except ffmpeg.Error as e: |
|
|
error_message = e.stderr.decode('utf-8') if e.stderr else str(e) |
|
|
return jsonify({"error": f"Lỗi kiểm tra tệp WebM: {error_message}"}), 500 |
|
|
|
|
|
|
|
|
ffmpeg.input(webm_path).output( |
|
|
wav_path, acodec="pcm_s16le", ac=1, ar=16000 |
|
|
).run(overwrite_output=True, quiet=True) |
|
|
|
|
|
|
|
|
wf = wave.open(wav_path, "rb") |
|
|
|
|
|
|
|
|
if wf.getnchannels() != 1 or wf.getsampwidth() != 2 or wf.getcomptype() != "NONE": |
|
|
wf.close() |
|
|
os.remove(wav_path) |
|
|
return jsonify({"error": "Định dạng WAV không hợp lệ!"}), 400 |
|
|
|
|
|
|
|
|
rec = KaldiRecognizer(model, wf.getframerate()) |
|
|
result_text = "" |
|
|
|
|
|
while True: |
|
|
data = wf.readframes(4000) |
|
|
if len(data) == 0: |
|
|
break |
|
|
if rec.AcceptWaveform(data): |
|
|
result = json.loads(rec.Result()) |
|
|
result_text += result.get("text", "") + " " |
|
|
else: |
|
|
partial_result = json.loads(rec.PartialResult()) |
|
|
if partial_result.get("partial", ""): |
|
|
result_text += partial_result["partial"] + " " |
|
|
|
|
|
wf.close() |
|
|
|
|
|
final_text = result_text.strip() or "Không nhận diện được nội dung âm thanh." |
|
|
return jsonify({"text": final_text}) |
|
|
|
|
|
except ffmpeg.Error as e: |
|
|
error_message = e.stderr.decode('utf-8') if e.stderr else str(e) |
|
|
return jsonify({"error": f"Lỗi chuyển đổi âm thanh từ WebM sang WAV: {error_message}"}), 500 |
|
|
except Exception as e: |
|
|
return jsonify({"error": f"Lỗi xử lý âm thanh: {str(e)}"}), 500 |
|
|
finally: |
|
|
|
|
|
if os.path.exists(webm_path): |
|
|
os.remove(webm_path) |
|
|
if os.path.exists(wav_path): |
|
|
os.remove(wav_path) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
app.run(host="0.0.0.0", port=7860, debug=True) |
|
|
|