Update web/core/views_tts.py
This commit is contained in:
parent
72854a1d20
commit
02153083b6
@ -1,87 +1,55 @@
|
|||||||
# core/views_tts.py
|
# core/views_tts.py
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
|
||||||
from django.http import FileResponse, HttpResponseBadRequest, HttpResponseForbidden, Http404
|
|
||||||
from django.utils.encoding import force_str
|
|
||||||
|
|
||||||
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
|
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.views.decorators.http import require_GET
|
||||||
from .models import Entry
|
from .models import Entry
|
||||||
|
|
||||||
# OpenAI client
|
# If you use the OpenAI SDK:
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
|
import os
|
||||||
|
|
||||||
MODEL_NAME = "gpt-4o-mini-tts" # good quality, lower cost
|
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", ""))
|
||||||
VOICE_NAME = "alloy" # easy default
|
|
||||||
AUDIO_FMT = "mp3"
|
|
||||||
|
|
||||||
def _ensure_punctuated(s: str) -> str:
|
def _is_staff(user):
|
||||||
s = (s or "").strip()
|
return user.is_authenticated and (user.is_staff or user.is_superuser)
|
||||||
if not s:
|
|
||||||
return ""
|
|
||||||
return s if s.endswith((".", "!", "?", "…", "—")) else s + "."
|
|
||||||
|
|
||||||
def _combine_text(entry: Entry) -> str:
|
|
||||||
ill = _ensure_punctuated(force_str(entry.illustration or ""))
|
|
||||||
app = force_str(entry.application or "").strip()
|
|
||||||
if ill and app:
|
|
||||||
return ill + " " + app # two spaces
|
|
||||||
return ill or app or ""
|
|
||||||
|
|
||||||
def _tts_cache_dir():
|
|
||||||
# Put cache under media/tts_cache (self-contained folder)
|
|
||||||
media_root = getattr(settings, "MEDIA_ROOT", None)
|
|
||||||
if not media_root:
|
|
||||||
raise RuntimeError("MEDIA_ROOT must be set to use TTS caching")
|
|
||||||
path = os.path.join(media_root, "tts_cache")
|
|
||||||
os.makedirs(path, exist_ok=True)
|
|
||||||
return path
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_passes_test(lambda u: u.is_staff)
|
@user_passes_test(_is_staff)
|
||||||
def api_tts_for_entry(request, entry_id: int):
|
@require_GET
|
||||||
if request.method != "GET":
|
def api_tts_for_entry(request, entry_id):
|
||||||
return HttpResponseBadRequest("GET required")
|
"""
|
||||||
|
Generate MP3 speech for an entry (staff‑only).
|
||||||
|
"""
|
||||||
|
entry = get_object_or_404(Entry, pk=entry_id)
|
||||||
|
|
||||||
|
# ---- Build safe combined text (avoid TypeError) ----
|
||||||
|
ill = (entry.illustration or "").strip()
|
||||||
|
app = (entry.application or "").strip()
|
||||||
|
if ill and not any(ill.endswith(p) for p in ".!?…"):
|
||||||
|
ill = ill + "."
|
||||||
|
combined = " ".join([t for t in (ill, app) if t]).strip()
|
||||||
|
if not combined:
|
||||||
|
return HttpResponseBadRequest("No text available for this entry.")
|
||||||
|
|
||||||
|
# ---- Call OpenAI TTS (MP3) ----
|
||||||
try:
|
try:
|
||||||
entry = Entry.objects.get(pk=entry_id)
|
# gpt-4o-mini-tts → mp3 bytes
|
||||||
except Entry.DoesNotExist:
|
# voices: "alloy", "verse", "aria", etc.
|
||||||
raise Http404("Entry not found")
|
speech = client.audio.speech.create(
|
||||||
|
model="gpt-4o-mini-tts",
|
||||||
text = _combine_text(entry)
|
voice="alloy",
|
||||||
if not text:
|
input=combined,
|
||||||
return HttpResponseBadRequest("No text available for this entry")
|
format="mp3",
|
||||||
|
|
||||||
# Cache key = entry id + hash of text + model/voice
|
|
||||||
h = hashlib.sha256(f"{entry_id}|{MODEL_NAME}|{VOICE_NAME}|{text}".encode("utf-8")).hexdigest()[:16]
|
|
||||||
fname = f"entry{entry_id}_{h}.{AUDIO_FMT}"
|
|
||||||
fpath = os.path.join(_tts_cache_dir(), fname)
|
|
||||||
|
|
||||||
# Serve cached if present
|
|
||||||
if os.path.exists(fpath):
|
|
||||||
return FileResponse(open(fpath, "rb"), content_type="audio/mpeg")
|
|
||||||
|
|
||||||
# Generate via OpenAI and cache
|
|
||||||
api_key = getattr(settings, "OPENAI_API_KEY", None)
|
|
||||||
if not api_key:
|
|
||||||
return HttpResponseBadRequest("OpenAI key not configured")
|
|
||||||
|
|
||||||
client = OpenAI(api_key=api_key)
|
|
||||||
# New SDK returns bytes for TTS:
|
|
||||||
# https://platform.openai.com/docs/guides/tts
|
|
||||||
audio = client.audio.speech.create(
|
|
||||||
model=MODEL_NAME,
|
|
||||||
voice=VOICE_NAME,
|
|
||||||
input=text,
|
|
||||||
format=AUDIO_FMT,
|
|
||||||
)
|
)
|
||||||
|
audio_bytes = speech.read() # returns raw bytes
|
||||||
|
except Exception as e:
|
||||||
|
# Return a plain text 500 so your client can preview it
|
||||||
|
return HttpResponse(f"OpenAI TTS failed: {e}", status=500, content_type="text/plain")
|
||||||
|
|
||||||
# `audio` may be a dict-like with .read() or .content (SDK versions vary).
|
# ---- Serve as audio/mpeg ----
|
||||||
# Handle both safely:
|
resp = HttpResponse(audio_bytes, content_type="audio/mpeg")
|
||||||
audio_bytes = getattr(audio, "read", None)
|
resp["Content-Disposition"] = 'inline; filename="entry-tts.mp3"'
|
||||||
audio_bytes = audio_bytes() if callable(audio_bytes) else getattr(audio, "content", audio)
|
resp["Cache-Control"] = "no-store"
|
||||||
|
return resp
|
||||||
with open(fpath, "wb") as f:
|
|
||||||
f.write(audio_bytes)
|
|
||||||
|
|
||||||
return FileResponse(open(fpath, "rb"), content_type="audio/mpeg")
|
|
||||||
Loading…
Reference in New Issue
Block a user