Add audiowidget endpoint: Blueprint renders audiowidget.tmpl, PaneCP fetches it on synthesis completion

This commit is contained in:
c0dev0id 2026-06-24 18:52:26 +02:00
parent 06c077b7f8
commit be338c2de0
5 changed files with 60 additions and 16 deletions

View File

@ -1,4 +1,5 @@
from flask import Blueprint, render_template, request from flask import Blueprint, render_template, request
from jinja2 import TemplateNotFound
blueprint = Blueprint( blueprint = Blueprint(
'vue3_neusik', 'vue3_neusik',
@ -14,3 +15,14 @@ def index():
'vue3_neusik/index.html', 'vue3_neusik/index.html',
import_on_load='true' if request.args.get('import') == '1' else 'false', import_on_load='true' if request.args.get('import') == '1' else 'false',
) )
@blueprint.route('/audiowidget', methods=['GET'])
def audiowidget():
try:
return render_template(
'vue3_neusik/audiowidget.tmpl',
result_url=request.args.get('result_url', ''),
errors=request.args.get('errors', ''),
)
except TemplateNotFound:
return '', 204

View File

@ -44,3 +44,12 @@ export async function fetchStatus(credentials) {
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json(); return res.json();
} }
export async function fetchAudioWidget(resultUrl, errors) {
const params = new URLSearchParams();
if (resultUrl) params.set('result_url', resultUrl);
if (errors) params.set('errors', errors);
const res = await fetch(`${U.audiowidget}?${params}`);
if (!res.ok || res.status === 204) return '';
return res.text();
}

View File

@ -1,5 +1,5 @@
import { h, ref } from 'vue'; import { h, ref, watch } from 'vue';
import { fetchScoreText, putScoreText, URLS } from '../api.js'; import { fetchScoreText, putScoreText, fetchAudioWidget, URLS } from '../api.js';
import { patchScore } from '../exporter.js'; import { patchScore } from '../exporter.js';
import { StatusPoller } from './StatusPoller.js'; import { StatusPoller } from './StatusPoller.js';
@ -40,6 +40,18 @@ export const PaneCP = {
setup(props) { setup(props) {
const exporting = ref(false); const exporting = ref(false);
const exportError = ref(''); const exportError = ref('');
const audioWidgetHtml = ref('');
watch(
() => props.store.synthesisStatus,
async (s) => {
if (!s?.frozen) { audioWidgetHtml.value = ''; return; }
audioWidgetHtml.value = await fetchAudioWidget(
s.file_accomplished ? URLS.result : null,
s.errors ?? null,
);
},
);
async function doExport() { async function doExport() {
exportError.value = ''; exportError.value = '';
@ -118,18 +130,10 @@ export const PaneCP = {
store.synthesisStatus && !store.synthesisStatus.frozen store.synthesisStatus && !store.synthesisStatus.frozen
? h(StatusPoller, { store }) : null, ? h(StatusPoller, { store }) : null,
// Synthesis error // Audio widget (rendered server-side from audiowidget.tmpl)
store.synthesisStatus?.frozen && store.synthesisStatus?.errors audioWidgetHtml.value
? h('div', { class: 'se-error', style: 'margin-top:0.5rem' }, ? h('div', { innerHTML: audioWidgetHtml.value, style: 'margin-top:0.5rem' })
store.synthesisStatus.errors) : null, : null,
// Audio result
store.synthesisStatus?.file_accomplished
? h('audio', {
controls: true,
src: URLS.result,
style: 'display:block;width:100%;margin-top:0.5rem',
}) : null,
]); ]);
}; };
}, },

View File

@ -0,0 +1,18 @@
{# audiowidget.tmpl.stub — copy to audiowidget.tmpl in the same directory to activate.
Customise to match your site's look and feel.
Context variables (passed as query params by the score editor):
result_url URL of the rendered MP3, or empty string
errors synthesis error message, or empty string
#}
{% if errors %}
<div class="se-error">{{ errors }}</div>
{% elif result_url %}
<figure class="audio-result">
<figcaption>Rendered result</figcaption>
<audio controls preload="metadata" style="display:block;width:100%">
<source src="{{ result_url }}" type="audio/mpeg">
<a href="{{ result_url }}">Download MP3</a>
</audio>
</figure>
{% endif %}

View File

@ -14,8 +14,9 @@ window.NEUSICIAN_URLS = {
astlog: "{{ url_for('astlog') }}", astlog: "{{ url_for('astlog') }}",
score: "{{ url_for('score') }}", score: "{{ url_for('score') }}",
status: "{{ url_for('statusjson') }}", status: "{{ url_for('statusjson') }}",
result: "{{ url_for('send_audio_generated') }}", result: "{{ url_for('send_audio_generated') }}",
submit: "{{ url_for('public-yaml-acceptor') }}" submit: "{{ url_for('public-yaml-acceptor') }}",
audiowidget: "{{ url_for('vue3_neusik.audiowidget') }}"
}; };
</script> </script>
<script type="importmap"> <script type="importmap">