Transcriber: video accept, delete buttons, live status spinner; Files: delete button
This commit is contained in:
@@ -38,7 +38,7 @@ from .database import (
|
||||
get_user_by_username,
|
||||
init_db,
|
||||
)
|
||||
from .tasks import ensure_user_bucket, transcribe_audio, transcribe_sheetmusic
|
||||
from .tasks import celery_app, ensure_user_bucket, transcribe_audio, transcribe_sheetmusic
|
||||
from .s3 import list_prefix, list_recent, s3_bucket_name, s3_client
|
||||
from .tools import section_color, section_label, tools_for_user
|
||||
|
||||
@@ -559,11 +559,12 @@ async def transcriber_transcribe(
|
||||
if user is None:
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
bucket = s3_bucket_name(user.username)
|
||||
task_id = ""
|
||||
try:
|
||||
transcribe_audio.delay(bucket, key, user.username)
|
||||
task_id = transcribe_audio.delay(bucket, key, user.username).id
|
||||
except Exception:
|
||||
logger.warning("could not enqueue transcription (broker down?)", exc_info=True)
|
||||
return RedirectResponse("/transcriber?status=started", status_code=303)
|
||||
return RedirectResponse(f"/transcriber?status=started&task={task_id}", status_code=303)
|
||||
|
||||
|
||||
@app.post("/transcriber/upload")
|
||||
@@ -582,11 +583,46 @@ async def transcriber_upload(
|
||||
await run_in_threadpool(
|
||||
lambda: s3_client().put_object(Bucket=bucket, Key=key, Body=content)
|
||||
)
|
||||
task_id = ""
|
||||
try:
|
||||
transcribe_audio.delay(bucket, key, user.username)
|
||||
task_id = transcribe_audio.delay(bucket, key, user.username).id
|
||||
except Exception:
|
||||
logger.warning("could not enqueue transcription after upload", exc_info=True)
|
||||
return RedirectResponse("/transcriber?status=started", status_code=303)
|
||||
return RedirectResponse(f"/transcriber?status=started&task={task_id}", status_code=303)
|
||||
|
||||
|
||||
@app.get("/api/transcriber/status")
|
||||
async def transcriber_status(task: str):
|
||||
from celery.result import AsyncResult
|
||||
r = AsyncResult(task, app=celery_app)
|
||||
return {"state": r.state, "result": r.result if r.state == "SUCCESS" else None}
|
||||
|
||||
|
||||
@app.post("/transcriber/delete")
|
||||
async def transcriber_delete(request: Request, key: Annotated[str, Form()]) -> HTMLResponse:
|
||||
user = await _current_user(request)
|
||||
if user is None:
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
bucket = s3_bucket_name(user.username)
|
||||
try:
|
||||
await run_in_threadpool(lambda: s3_client().delete_object(Bucket=bucket, Key=key))
|
||||
except Exception:
|
||||
logger.warning("transcriber delete failed", exc_info=True)
|
||||
return RedirectResponse("/transcriber", status_code=303)
|
||||
|
||||
|
||||
@app.post("/files/delete")
|
||||
async def files_delete(request: Request, name: Annotated[str, Form()]) -> HTMLResponse:
|
||||
user = await _current_user(request)
|
||||
if user is None:
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
p = _resolve_shared(user.username, name)
|
||||
if p is not None:
|
||||
try:
|
||||
p.unlink()
|
||||
except Exception:
|
||||
logger.warning("files delete failed", exc_info=True)
|
||||
return RedirectResponse("/files", status_code=303)
|
||||
|
||||
|
||||
@app.get("/transcriber/download")
|
||||
@@ -618,11 +654,12 @@ async def transcriber_sheetmusic(
|
||||
if user is None:
|
||||
return RedirectResponse("/login", status_code=303)
|
||||
bucket = s3_bucket_name(user.username)
|
||||
task_id = ""
|
||||
try:
|
||||
transcribe_sheetmusic.delay(bucket, key, user.username)
|
||||
task_id = transcribe_sheetmusic.delay(bucket, key, user.username).id
|
||||
except Exception:
|
||||
logger.warning("could not enqueue sheet-music transcription", exc_info=True)
|
||||
return RedirectResponse("/transcriber?status=started-mus", status_code=303)
|
||||
return RedirectResponse(f"/transcriber?status=started-mus&task={task_id}", status_code=303)
|
||||
|
||||
|
||||
@app.get("/api/status")
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<span style="flex:1"></span>
|
||||
<a class="button button-utility" href="/files/raw/{{ f.name }}" target="_blank" title="View {{ f.name }}" style="width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;padding:0;font-size:15px">👁</a>
|
||||
<a class="button button-utility" href="/files/dl/{{ f.name }}" title="Download {{ f.name }}" style="width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;padding:0;font-size:17px">↓</a>
|
||||
<form method="post" action="/files/delete" style="display:inline-flex">
|
||||
<input type="hidden" name="name" value="{{ f.name }}">
|
||||
<button class="button button-utility" type="submit" title="Delete {{ f.name }}" style="width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;padding:0;font-size:15px;border-color:#b3261e;color:#b3261e">🗑</button>
|
||||
</form>
|
||||
</div>
|
||||
<div title="{{ f.name }}" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;font:var(--type-body-sm);color:var(--ink)">{{ f.name }}</div>
|
||||
</li>
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
{% block title %}Audio Transcriber — {{ settings.APP_NAME }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page">
|
||||
{% if status == "started" %}<div class="form-error" style="background:#e8f7ec;color:#1aae39;border-color:#b8e6c7">MIDI transcription started — results appear below when done.</div>
|
||||
{% elif status == "started-mus" %}<div class="form-error" style="background:#fff4e5;color:#dd5b00;border-color:#f4d8b0">Sheet-music (MuScriptor) transcription started. This is slow on CPU and needs the HF token set.</div>{% endif %}
|
||||
{% if status == "started" or status == "started-mus" %}
|
||||
<div id="txStatus" class="form-error" style="background:#fff8e6;color:#8a6d00;border-color:#e6d29a">
|
||||
<span class="tx-spinner"></span> <span id="txText">Transcribing…</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1 class="greeting">Audio Transcriber</h1>
|
||||
<p class="muted" style="max-width:640px">
|
||||
Turn an audio file into MIDI (via Spotify Basic Pitch). Upload or pick a file from
|
||||
your bucket, then the transcription worker produces a MIDI you can download.
|
||||
Turn an audio or video file into MIDI (via Spotify Basic Pitch). Upload or pick a file from
|
||||
your bucket, then the worker produces a MIDI you can download. Video files get their audio extracted automatically.
|
||||
</p>
|
||||
|
||||
<div class="admin-panel">
|
||||
@@ -38,15 +41,15 @@
|
||||
</section>
|
||||
|
||||
<section class="linkout-card stack">
|
||||
<h2 class="section-title">Upload audio</h2>
|
||||
<h2 class="section-title">Upload audio / video</h2>
|
||||
<form method="post" action="/transcriber/upload" enctype="multipart/form-data">
|
||||
<div class="form-field">
|
||||
<input class="text-input" type="file" name="file" accept="audio/*,.wav,.mp3,.flac,.m4a,.mp4,.mov,.webm,.mkv" required>
|
||||
<input class="text-input" type="file" name="file" accept="audio/*,video/*,.wav,.mp3,.flac,.m4a,.mp4,.mov,.webm,.mkv" required>
|
||||
</div>
|
||||
<button class="button button-primary width-full" type="submit">Upload & transcribe</button>
|
||||
</form>
|
||||
<p class="faint margin-top" style="font: var(--type-caption)">
|
||||
Saved to <strong>{{ bucket }}</strong> under <code>audio/…</code>. <strong>MIDI</strong> = Basic Pitch (fast); <strong>Sheet music</strong> = MuScriptor (MIDI+MusicXML+PDF, slow, needs HF token).
|
||||
Saved to <strong>{{ bucket }}</strong> under <code>audio/…</code>. <strong>MIDI</strong> = Basic Pitch (fast); <strong>Sheet music</strong> = MuScriptor (MIDI+MusicXML+PDF, slower).
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
@@ -59,7 +62,13 @@
|
||||
{% for t in transcriptions %}
|
||||
<li>
|
||||
<span class="file-name" title="{{ t }}">{{ t }}</span>
|
||||
<a class="button button-utility" href="/transcriber/download?key={{ t }}" >Download</a>
|
||||
<span style="display:flex;gap:8px;align-items:center">
|
||||
<a class="button button-utility" href="/transcriber/download?key={{ t }}">Download</a>
|
||||
<form method="post" action="/transcriber/delete" style="display:flex">
|
||||
<input type="hidden" name="key" value="{{ t }}">
|
||||
<button class="button button-utility" type="submit" title="Delete" style="border-color:#b3261e;color:#b3261e">🗑</button>
|
||||
</form>
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -69,4 +78,37 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
<style>
|
||||
.tx-spinner { display:inline-block; width:14px; height:14px; border:2px solid #d7d7d7; border-top-color:#8a6d00; border-radius:50%; animation:txspin .7s linear infinite; vertical-align:-2px; margin-right:6px; }
|
||||
@keyframes txspin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
{% if task %}
|
||||
<script>
|
||||
(function () {
|
||||
var task = "{{ task }}";
|
||||
var box = document.getElementById('txStatus');
|
||||
var txt = document.getElementById('txText');
|
||||
if (!box || !task) return;
|
||||
function poll() {
|
||||
fetch('/api/transcriber/status?task=' + encodeURIComponent(task))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
if (d.state === 'SUCCESS') {
|
||||
box.style.background = '#e8f7ec'; box.style.color = '#1aae39'; box.style.borderColor = '#b8e6c7';
|
||||
txt.textContent = 'Done ✓ — results below.';
|
||||
setTimeout(function () { location.reload(); }, 1200);
|
||||
} else if (d.state === 'FAILURE') {
|
||||
box.style.background = '#fdecec'; box.style.color = '#b3261e'; box.style.borderColor = '#f5c6c4';
|
||||
txt.textContent = 'Failed — check the worker logs.';
|
||||
} else {
|
||||
txt.textContent = 'Transcribing… (' + d.state.toLowerCase() + ')';
|
||||
setTimeout(poll, 3000);
|
||||
}
|
||||
})
|
||||
.catch(function () { setTimeout(poll, 5000); });
|
||||
}
|
||||
poll();
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user