feat(atis): clock-locked ATIS loop tied to tuned frequency

Real ATIS broadcasts run continuously; tuning in mid-broadcast should
drop the pilot into the current spoken position, then loop. The frontend
now generates the full announcement once via TTS, plays it as a looping
HTMLAudioElement, and seeks to ((Date.now() - lastUpdated) / duration)
on metadata-load so all clients tuned to the same ATIS hear it phase-
synced. The loop starts/stops automatically with frequency tuning and
restarts on info-letter change. say.post.ts now caches tag=atis like
tag=flightlab to avoid re-synthesizing identical announcements.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-05-27 14:43:48 +02:00
parent 4ac8c1104a
commit 22f50aa403
2 changed files with 145 additions and 27 deletions

View File

@@ -25,6 +25,14 @@ function isFlightLabTag(tag?: string) {
return (tag || "").trim().toLowerCase() === "flightlab";
}
function isAtisTag(tag?: string) {
return (tag || "").trim().toLowerCase() === "atis";
}
function isCacheableTag(tag?: string) {
return isFlightLabTag(tag) || isAtisTag(tag);
}
type TTSProvider = "openai" | "speaches" | "piper";
function resolveTtsProvider(useSpeaches: boolean, usePiper: boolean): TTSProvider {
@@ -196,7 +204,8 @@ export default defineEventHandler(async (event) => {
const ext = fmtToExt(fmt);
const outputExt = (provider === "openai" || provider === "piper") ? "wav" : ext;
const flightlabRequest = isFlightLabTag(body?.tag);
const flightlabCacheKey = flightlabRequest
const cacheableRequest = isCacheableTag(body?.tag);
const flightlabCacheKey = cacheableRequest
? buildFlightLabCacheKey({
normalized,
level,
@@ -207,7 +216,7 @@ export default defineEventHandler(async (event) => {
model: providerModel
})
: null;
const flightlabCacheBaseDir = flightlabRequest ? flightLabCacheDir() : null;
const flightlabCacheBaseDir = cacheableRequest ? flightLabCacheDir() : null;
const flightlabCachedAudioPath = (flightlabCacheBaseDir && flightlabCacheKey)
? join(flightlabCacheBaseDir, `${flightlabCacheKey}.${outputExt}`)
: null;
@@ -286,7 +295,7 @@ export default defineEventHandler(async (event) => {
{
key: flightlabCacheKey,
createdAt: timestamp,
tag: "flightlab",
tag: (body?.tag || "").trim().toLowerCase() || "flightlab",
voice,
speed,
level,