From 0aa728b1aa24326b82afa2ffd178ade14dbefa82 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Sat, 24 Oct 2026 04:11:25 +0000 Subject: [PATCH] fix(studio-server): decode authored audio attributes for loudness (#5289) --- .../src/helpers/loudness.test.ts | 34 +++++++++++++++++++ .../studio-server/src/helpers/loudness.ts | 10 +++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/studio-server/src/helpers/loudness.test.ts b/packages/studio-server/src/helpers/loudness.test.ts index 898d9093d2..638abb66c6 100644 --- a/packages/studio-server/src/helpers/loudness.test.ts +++ b/packages/studio-server/src/helpers/loudness.test.ts @@ -95,6 +95,40 @@ describe("audioTags with video", () => { }); }); +describe("audioTags with HTML character references", () => { + it.each([ + { src: "Q&A.wav", expected: "Q&A.wav" }, + { src: "Q&A.wav", expected: "Q&A.wav" }, + { src: "Q&A.wav", expected: "Q&A.wav" }, + { src: "an's "reply".wav", expected: 'an\'s "reply".wav' }, + { src: "Q&A.wav", expected: "Q&A.wav" }, + ])("decodes $src once for local file lookup", ({ src, expected }) => { + const html = ``; + expect(audioTags(html)[0]?.src).toBe(expected); + }); + + it("patches an entity-encoded id while preserving every other authored byte", () => { + const html = ``; + expect(audioTags(html)[0]).toEqual(expect.objectContaining({ id: "voice-over", volume: 1 })); + expect(updateAudioVolume(html, "voice-over", 0.5)).toBe( + ``, + ); + }); + + it("decodes video audibility and timing attributes before interpreting them", () => { + const html = ``; + expect(audioTags(html)[0]).toEqual( + expect.objectContaining({ + id: "vo", + src: "Q&A.mp4", + mediaStart: 2, + duration: 3, + playbackRate: 1, + }), + ); + }); +}); + describe("loudnessMeasureArgs with a playback rate", () => { it("measures the source span a sped-up clip actually plays", () => { const args = loudnessMeasureArgs("clip.mp4", { mediaStart: 0, duration: 4, playbackRate: 2 }); diff --git a/packages/studio-server/src/helpers/loudness.ts b/packages/studio-server/src/helpers/loudness.ts index 7f13d8ff06..f104894205 100644 --- a/packages/studio-server/src/helpers/loudness.ts +++ b/packages/studio-server/src/helpers/loudness.ts @@ -3,6 +3,7 @@ import { isAbsolute, relative, resolve } from "node:path"; import { promisify } from "node:util"; import { isAudibleVideoElement } from "@hyperframes/core/audible-video"; import { formatAudioGain, MAX_AUDIO_GAIN_DB } from "@hyperframes/core/audio-gain"; +import { decodeAuthoredAttribute } from "@hyperframes/parsers"; import { findFfBinary } from "@hyperframes/parsers/ff-binaries"; import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration"; import { requestedProjectPath } from "./requestSubPath.js"; @@ -101,7 +102,14 @@ function attributeFromMatch( const whole = match[0]; const localValueStart = attributeValueStart(whole); const valueStart = absoluteOffset + match.index + localValueStart; - return [name, { value, valueStart, valueEnd: valueStart + value.length }]; + return [ + name, + { + value: decodeAuthoredAttribute(value), + valueStart, + valueEnd: valueStart + value.length, + }, + ]; } function matchedAttributeValue(match: RegExpMatchArray): string | undefined {