Peak Amplitude:{" "}
- {peaks === null ? "measuring…" : peakDb === null ? "None" : dbText(peakDb)}
+ {peaks === null ? "measuring…" : peakDb === null ? "Unavailable" : dbText(peakDb)}
{outOfRange && Peak targets must be 0 dB or lower.
}
diff --git a/packages/studio/src/player/components/timelineClipDragCommit.ts b/packages/studio/src/player/components/timelineClipDragCommit.ts
index a47790c5a5d..f7a2b4a7ea3 100644
--- a/packages/studio/src/player/components/timelineClipDragCommit.ts
+++ b/packages/studio/src/player/components/timelineClipDragCommit.ts
@@ -131,7 +131,7 @@ export function persistMoveEdits(
console.warn(
onMoveElement
? `[Timeline] persistMoveEdits: only single-clip onMoveElement wired, so this ${edits.length}-clip move degrades to a per-clip persist race (no atomic single-undo)`
- : `[Timeline] persistMoveEdits: no move persist handler wired: ${edits.length} edit(s) applied to the store only, not saved`,
+ : `[Timeline] persistMoveEdits: no move persist handler wired, so ${edits.length} edit(s) applied to the store only, not saved`,
);
}
const prev = edits.map((e) => ({
diff --git a/scripts/check-studio-copy-dashes.mjs b/scripts/check-studio-copy-dashes.mjs
index 1c58576a202..432a51f4a1f 100644
--- a/scripts/check-studio-copy-dashes.mjs
+++ b/scripts/check-studio-copy-dashes.mjs
@@ -6,7 +6,8 @@ import { fileURLToPath } from "node:url";
import ts from "typescript";
const ROOT = join(import.meta.dirname, "..");
-const DASH = /[–—]/;
+// JSX keeps HTML entities raw in the AST, and React renders them as dashes.
+const DASH = /[\u2013\u2014]|&[mn]dash;|̵[12];|ȁ[34];/i;
const TEXT_KINDS = new Set([
ts.SyntaxKind.StringLiteral,
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
@@ -31,7 +32,7 @@ export function listDashedText(source, filename = "source.tsx") {
return issues;
}
-const isShippedSource = (path) =>
+export const isShippedSource = (path) =>
/\.tsx?$/.test(path) && !/\.d\.ts$|\.(test|spec)\.tsx?$/.test(path);
export function checkStudioCopyDashes(root = ROOT) {
diff --git a/scripts/check-studio-copy-dashes.test.mjs b/scripts/check-studio-copy-dashes.test.mjs
index abc5ecb8487..dd20961dc9b 100644
--- a/scripts/check-studio-copy-dashes.test.mjs
+++ b/scripts/check-studio-copy-dashes.test.mjs
@@ -1,28 +1,51 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
-import { listDashedText } from "./check-studio-copy-dashes.mjs";
+import { isShippedSource, listDashedText } from "./check-studio-copy-dashes.mjs";
+
+const lines = (source) => listDashedText(source, "x.tsx").map((issue) => issue.split(" ")[0]);
describe("Studio copy dash checker", () => {
- it("reports dashes in strings, template text and JSX text, escaped or not", () => {
+ it("reports dashes in every string, template and JSX text form, literal or escaped", () => {
const source = [
- 'const a = "One — two";',
- "const b = `${a} – ${a}`;",
- 'const c = "Escaped \\u2014 too";',
- "const d = () =>
Range 1–2
;",
+ 'const a = "One \\u2014 two";',
+ "const b = `plain \\u2013 template`;",
+ "const c = `\\u2014 ${a}`;",
+ "const d = `${a} \\u2014 ${b} x`;",
+ "const e = `${a} x \\u2014`;",
+ 'const f = "Literal \u2014 too";',
+ "const g = () =>
Range 1\u20132
;",
+ "const h = () =>
One — two
;",
+ 'const i = () =>
;',
].join("\n");
- assert.deepEqual(
- listDashedText(source, "x.tsx").map((issue) => issue.split(" ")[0]),
- ["x.tsx:1", "x.tsx:2", "x.tsx:3", "x.tsx:4"],
- );
+ assert.deepEqual(lines(source), [
+ "x.tsx:1",
+ "x.tsx:2",
+ "x.tsx:3",
+ "x.tsx:4",
+ "x.tsx:5",
+ "x.tsx:6",
+ "x.tsx:7",
+ "x.tsx:8",
+ "x.tsx:9",
+ ]);
});
it("ignores comments, regexes, hyphens and shortcut glyphs", () => {
const source = [
- "// A comment — never copy",
- "/* block – comment */",
- "const d = /[–—]/;",
- 'const f = "A plain hyphen - is fine, ⌥-click too";',
+ "// A comment \u2014 never copy",
+ "/* block \u2013 comment */",
+ "const d = /[\u2013\u2014]/;",
+ 'const f = "A plain hyphen - is fine, \u2325-click too";',
].join("\n");
- assert.deepEqual(listDashedText(source, "x.tsx"), []);
+ assert.deepEqual(lines(source), []);
+ });
+
+ it("checks shipped .ts and .tsx files, not tests or declarations", () => {
+ assert.deepEqual(
+ ["a.ts", "b.tsx", "c.test.ts", "d.spec.tsx", "e.d.ts", "f.css", "g.js"].filter(
+ isShippedSource,
+ ),
+ ["a.ts", "b.tsx"],
+ );
});
});
From 0f8f7dcdddef42f84e0e5e3836540e292320d0d8 Mon Sep 17 00:00:00 2001
From: Miguel Angel Simon Sierra
Date: Sat, 10 Oct 2026 01:36:07 -0700
Subject: [PATCH 3/5] test(studio): the dash check test covers every entity
form
---
docs/studio/animation.mdx | 2 +-
scripts/check-studio-copy-dashes.test.mjs | 6 ++++++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/docs/studio/animation.mdx b/docs/studio/animation.mdx
index de03369dd63..df3af55d307 100644
--- a/docs/studio/animation.mdx
+++ b/docs/studio/animation.mdx
@@ -122,7 +122,7 @@ runtime expression, but it cannot always rewrite that source safely.
- **Unroll to edit** rewrites supported helper- or loop-generated motion as
explicit tweens that Studio can edit.
-- **Computed value. Edit it in the Code tab.** means the value must be changed
+- **Computed value. Edit it in the Code tab.**: the value must be changed
in source.
- When the intent is easier to state than the implementation, copy the element
context to your agent and describe the result you want preserved.
diff --git a/scripts/check-studio-copy-dashes.test.mjs b/scripts/check-studio-copy-dashes.test.mjs
index dd20961dc9b..b8b48e531e0 100644
--- a/scripts/check-studio-copy-dashes.test.mjs
+++ b/scripts/check-studio-copy-dashes.test.mjs
@@ -16,6 +16,9 @@ describe("Studio copy dash checker", () => {
"const g = () => Range 1\u20132
;",
"const h = () => One — two
;",
'const i = () => ;',
+ "const j = () => One – two
;",
+ "const l = () => One — two
;",
+ 'const k = () => ;',
].join("\n");
assert.deepEqual(lines(source), [
"x.tsx:1",
@@ -27,6 +30,9 @@ describe("Studio copy dash checker", () => {
"x.tsx:7",
"x.tsx:8",
"x.tsx:9",
+ "x.tsx:10",
+ "x.tsx:11",
+ "x.tsx:12",
]);
});
From 49bbeae1cf44f18c7c37f8a2c38660ae10b8905f Mon Sep 17 00:00:00 2001
From: Miguel Angel Simon Sierra
Date: Sat, 10 Oct 2026 02:50:46 -0700
Subject: [PATCH 4/5] fix(studio): messages Studio shows from lint, sdk, core
and the renderer drop dashes too
---
packages/cli/src/utils/checkBrowser.test.ts | 2 +-
packages/core/src/audio/audioFxWorklets.ts | 14 ++---
packages/core/src/audioFx.ts | 10 ++--
packages/core/src/audioFxCopy.ts | 10 ++--
packages/core/src/audioFxEq.ts | 2 +-
packages/core/src/audioFxJobs.ts | 4 +-
packages/core/src/audioFxPresets.ts | 38 ++++++-------
packages/core/src/audioLeveller.ts | 2 +-
packages/core/src/figma/client.ts | 18 +++----
packages/core/src/figma/emitTimelineScript.ts | 2 +-
packages/core/src/fonts/deterministicFonts.ts | 6 +--
packages/core/src/runtime/getVariables.ts | 2 +-
packages/core/src/runtime/init.ts | 2 +-
packages/core/src/runtime/vfx.ts | 4 +-
packages/core/src/runtime/webAudioRoute.ts | 2 +-
packages/core/src/vfx/lumaMatte.frag.ts | 2 +-
packages/core/src/vfx/noise.frag.ts | 2 +-
packages/engine/src/config.ts | 2 +-
.../engine/src/services/browserManager.ts | 4 +-
.../engine/src/services/drawElementService.ts | 2 +-
packages/engine/src/services/frameCapture.ts | 48 ++++++++---------
.../src/services/parallelCoordinator.ts | 4 +-
.../engine/src/services/screenshotService.ts | 2 +-
packages/engine/src/services/systemMemory.ts | 4 +-
.../engine/src/services/threeDProjection.ts | 4 +-
packages/engine/src/utils/gpuParityDiff.ts | 8 +--
packages/engine/src/utils/hdr.test.ts | 2 +-
packages/engine/src/utils/hdr.ts | 2 +-
packages/lint/src/project.ts | 6 +--
packages/lint/src/rules/composition.ts | 28 +++++-----
packages/lint/src/rules/core.ts | 4 +-
packages/lint/src/rules/fonts.ts | 4 +-
packages/lint/src/rules/gsap.ts | 44 +++++++--------
packages/lint/src/rules/media.ts | 22 ++++----
.../src/outputResolutionCompatibility.ts | 2 +-
.../parsers/src/slideshow/parseSlideshow.ts | 2 +-
packages/player/src/hyperframes-player.ts | 4 +-
.../src/slideshow/hyperframes-slideshow.ts | 4 +-
packages/player/src/styles.ts | 2 +-
packages/producer/de-gatecheck.sh | 4 +-
packages/producer/src/server.ts | 2 +-
.../producer/src/services/distributed/plan.ts | 10 ++--
.../distributed/renderConfigValidation.ts | 6 +--
packages/producer/src/services/fileServer.ts | 6 +--
.../producer/src/services/hdrCompositor.ts | 4 +-
.../producer/src/services/htmlCompiler.ts | 12 ++---
.../src/services/render/captureCost.ts | 4 +-
.../src/services/render/hdrMode.test.ts | 8 +--
.../producer/src/services/render/hdrMode.ts | 10 ++--
.../src/services/render/planValidation.ts | 2 +-
.../services/render/stages/compileStage.ts | 8 +--
.../src/services/render/stages/freezePlan.ts | 2 +-
.../src/services/render/stages/probeStage.ts | 10 ++--
.../src/services/render/videoFrameCoverage.ts | 2 +-
.../src/services/renderOrchestrator.ts | 14 ++---
packages/producer/src/transparency-test.ts | 10 ++--
.../producer/src/utils/audioRegression.ts | 2 +-
packages/sdk/src/engine/mutate.ts | 12 ++---
.../shader-transitions/src/hyper-shader.ts | 4 +-
.../studio-server/src/helpers/loudness.ts | 2 +-
.../studio-server/src/routes/thumbnail.ts | 2 +-
.../src/utils/sdkCutover.linkedTiming.test.ts | 2 +-
scripts/check-studio-copy-dashes.mjs | 53 +++++++++++++++----
scripts/check-studio-copy-dashes.test.mjs | 50 ++++++++++++++++-
64 files changed, 320 insertions(+), 247 deletions(-)
diff --git a/packages/cli/src/utils/checkBrowser.test.ts b/packages/cli/src/utils/checkBrowser.test.ts
index c67a35cfa09..60b5df99c72 100644
--- a/packages/cli/src/utils/checkBrowser.test.ts
+++ b/packages/cli/src/utils/checkBrowser.test.ts
@@ -450,7 +450,7 @@ it("surfaces the runtime's web-audio-bypass console.info line as its own info fi
"info",
'[hyperframes] runtime_web_audio_bypass: "https://cdn.example.com/track.mp3" ' +
"(cross_origin_no_cors): Web Audio capture withheld; the track plays through native " +
- "HTMLMediaElement output. Native playback cannot reproduce: fx-chain — proxy or download " +
+ "HTMLMediaElement output. Native playback cannot reproduce: fx-chain. Fix: proxy or download " +
"the asset to a same-origin URL to keep it.",
);
const authorInfo = fakeConsoleMessage("info", "debug runtime_web_audio_bypass lookalike");
diff --git a/packages/core/src/audio/audioFxWorklets.ts b/packages/core/src/audio/audioFxWorklets.ts
index 5aa3d9fc16f..6e89ae8118d 100644
--- a/packages/core/src/audio/audioFxWorklets.ts
+++ b/packages/core/src/audio/audioFxWorklets.ts
@@ -18,7 +18,7 @@ const dbToLin = (db) => Math.pow(10, db / 20);
* Per channel matters: the followers advance once per sample, so a single shared
* follower stepped once per channel per sample. On stereo that ran a 20 ms attack
* as 10 ms, and gave the right channel a gain computed from an envelope that had
- * already traversed the left — so the two channels ducked by different amounts
+ * already traversed the left, so the two channels ducked by different amounts
* from the same input and the image pumped.
*/
class EnvBank {
@@ -230,7 +230,7 @@ function readTap(ring, write, delaySamples) {
return ring[i0] * (1 - frac) + ring[i1] * frac;
}
-/** Equal-power-ish crossfade, zero at a tap's reset point — hides the splice. */
+/** Equal-power-ish crossfade, zero at a tap's reset point, which hides the splice. */
function xfade(phase) {
return Math.sin(Math.PI * phase);
}
@@ -242,7 +242,7 @@ function xfade(phase) {
* out, which hides the splice each tap makes when it wraps.
*
* write/phase are block-level state, advanced once per SAMPLE across all
- * channels together (not once per channel) — advancing them inside the
+ * channels together (not once per channel). Advancing them inside the
* per-channel loop would move the tap 2x/4x too fast on a stereo/quad input.
*/
class HfPitchshift extends AudioWorkletProcessor {
@@ -255,13 +255,13 @@ class HfPitchshift extends AudioWorkletProcessor {
this.phase = 0;
// Samples written so far, capped at one grain. The taps read up to a grain
// behind the write head, so until this fills they would read the ring's
- // zeros — the head of every clip came out attenuated or silent.
+ // zeros, so the head of every clip came out attenuated or silent.
this.filled = 0;
// How much of the wet (pitch-shifted) path is currently in the output, and
// where it is heading. Crossing between dry and wet is a ~50 ms jump in the
// signal, so it is RAMPED rather than switched: a hard swap either way is a
// click. Ramping in both directions is also what lets a node return to true
- // bypass at semitones 0 — a one-way latch left preview stuck with the delay
+ // bypass at semitones 0. A one-way latch left preview stuck with the delay
// that the render, building a fresh node from the attribute, does not have.
this.wet = 0;
this.wetTarget = 0;
@@ -289,7 +289,7 @@ class HfPitchshift extends AudioWorkletProcessor {
// Nothing to shift, or mixed fully out. The grain delay is ~grain/2
// whatever the ratio, so at semitones=0 this degenerated into a pure 50 ms
- // delay of the signal — while the copy for that exact setting reads
+ // delay of the signal, while the copy for that exact setting reads
// "Unchanged pitch".
this.wetTarget = semitones === 0 ? 0 : mix;
@@ -368,7 +368,7 @@ export function ensureAudioFxWorklets(ctx: BaseAudioContext): Promise {
modulePromise = (async () => {
if (!ctx.audioWorklet) {
throw new Error(
- "AudioWorklet is unavailable — the page needs a secure context (https, localhost or file://)",
+ "AudioWorklet is unavailable: the page needs a secure context (https, localhost or file://)",
);
}
// A data: URL rather than a blob:, because a blob inherits the page origin
diff --git a/packages/core/src/audioFx.ts b/packages/core/src/audioFx.ts
index 181ddb67c6c..de344e9b26d 100644
--- a/packages/core/src/audioFx.ts
+++ b/packages/core/src/audioFx.ts
@@ -103,7 +103,7 @@ const freq = (
automatable: true,
});
-const qParam = (def = 0.707, hint = "Bandwidth — higher is narrower."): HfAudioFxNumberParam => ({
+const qParam = (def = 0.707, hint = "Bandwidth: higher is narrower."): HfAudioFxNumberParam => ({
kind: "number",
key: "q",
label: "Q",
@@ -187,7 +187,7 @@ export const HF_AUDIO_FX: readonly HfAudioFxDef[] = [
id: "highpass",
label: "High-pass",
group: "filter",
- description: "Remove low frequencies — the usual fix for rumble on a voice.",
+ description: "Remove low frequencies: the usual fix for rumble on a voice.",
params: [freq("frequency", "Cutoff", 300, 20, 20000), qParam(0.707), poles],
web: "biquad-highpass",
},
@@ -195,7 +195,7 @@ export const HF_AUDIO_FX: readonly HfAudioFxDef[] = [
id: "lowpass",
label: "Low-pass",
group: "filter",
- description: "Remove high frequencies — darkens or muffles a track.",
+ description: "Remove high frequencies, which darkens or muffles a track.",
params: [freq("frequency", "Cutoff", 8000, 100, 20000), qParam(0.707), poles],
web: "biquad-lowpass",
},
@@ -288,7 +288,7 @@ export const HF_AUDIO_FX: readonly HfAudioFxDef[] = [
id: "limiter",
label: "Limiter",
group: "dynamics",
- description: "Hard ceiling — nothing gets past the limit.",
+ description: "Hard ceiling: nothing gets past the limit.",
params: [
{
kind: "number",
@@ -490,7 +490,7 @@ export const HF_AUDIO_FX: readonly HfAudioFxDef[] = [
max: 250,
step: 1,
default: 1,
- hint: "Repeats each sample N times — a crude downsample.",
+ hint: "Repeats each sample N times, a crude downsample.",
},
{
kind: "number",
diff --git a/packages/core/src/audioFxCopy.ts b/packages/core/src/audioFxCopy.ts
index cc3a7759432..ecc1c067dc0 100644
--- a/packages/core/src/audioFxCopy.ts
+++ b/packages/core/src/audioFxCopy.ts
@@ -63,7 +63,7 @@ export const EFFECT_COPY: Record = {
},
highpass: {
title: "Remove Rumble",
- does: "Cuts the very bottom — traffic, footsteps, air conditioning, hands on the mic.",
+ does: "Cuts the very bottom: traffic, footsteps, air conditioning, hands on the mic.",
reachFor: "There's a low hum or thump under everything.",
primary: "frequency",
primaryEnds: { low: "Only the deepest", high: "Thins it out" },
@@ -94,7 +94,7 @@ export const EFFECT_COPY: Record = {
peaking: {
title: "Shape One Range",
does: "Lifts or lowers one part of the sound and leaves the rest alone.",
- reachFor: "One quality is wrong — boomy, boxy, harsh — but the rest is fine.",
+ reachFor: "One quality is wrong (boomy, boxy, harsh) but the rest is fine.",
primary: "gain",
primaryEnds: { low: "Take it out", high: "Bring it forward" },
band: [20, 20000],
@@ -168,7 +168,7 @@ export const EFFECT_COPY: Record = {
},
gate: {
title: "Silence the Gaps",
- does: "Mutes the pauses. Whatever sits underneath stays — this closes the gaps, it does not remove noise.",
+ does: "Mutes the pauses. Whatever sits underneath stays: this closes the gaps, it does not remove noise.",
reachFor: "You can hear the room in the gaps.",
primary: "strength",
primaryEnds: { low: "Only true silence", high: "Cuts quiet parts too" },
@@ -176,7 +176,7 @@ export const EFFECT_COPY: Record = {
threshold: { label: "Quieter than this is a gap" },
range: {
label: "How far to duck the gaps",
- hint: "Not all the way down, usually — total silence sounds broken.",
+ hint: "Not all the way down, usually. Total silence sounds broken.",
},
ratio: { label: "How hard" },
attack: { label: "How fast it opens" },
@@ -394,7 +394,7 @@ export const SUMMARY: Record string> = {
? "Doing nothing yet"
: `${n(p.gain) > 0 ? "More" : "Less"} sparkle above ${hz(p.frequency)}`,
compressor: (p) =>
- `Evening out — ${strength(Math.min(1, (n(p.ratio, 3) - 1) / 7), ["gentle", "moderate", "firm"])}`,
+ `Evening out, ${strength(Math.min(1, (n(p.ratio, 3) - 1) / 7), ["gentle", "moderate", "firm"])}`,
limiter: (p) => `Nothing louder than ${n(p.limit, -1)} dB`,
gate: (p) => `Closing gaps quieter than ${n(p.threshold, -45)} dB`,
saturate: (p) =>
diff --git a/packages/core/src/audioFxEq.ts b/packages/core/src/audioFxEq.ts
index c1fbcea6aaa..ce728d6402b 100644
--- a/packages/core/src/audioFxEq.ts
+++ b/packages/core/src/audioFxEq.ts
@@ -183,7 +183,7 @@ export function removeAudioEq(chain: HfAudioFxChain, eqId: string): HfAudioFxCha
*/
export function audioEqSummary(bands: readonly HfAudioEqBand[]): string {
const moved = bands.filter((b) => Math.abs(b.gain) >= 0.1);
- if (moved.length === 0) return "Flat — nothing changed yet";
+ if (moved.length === 0) return "Flat: nothing changed yet";
return moved
.map((b) => `${b.name} ${b.gain > 0 ? "+" : "−"}${Math.abs(Number(b.gain.toFixed(1)))}`)
.join(", ");
diff --git a/packages/core/src/audioFxJobs.ts b/packages/core/src/audioFxJobs.ts
index bbdc5f4eec9..73d517f0356 100644
--- a/packages/core/src/audioFxJobs.ts
+++ b/packages/core/src/audioFxJobs.ts
@@ -51,7 +51,7 @@ export const HF_AUDIO_FX_JOBS: readonly HfAudioFxJob[] = [
{
id: "tame-boominess",
label: "Tame Boominess",
- does: "Too much low-end body — it booms.",
+ does: "Too much low-end body, so it booms.",
type: "peaking",
params: { frequency: 200, gain: -4, q: 1.4 },
},
@@ -72,7 +72,7 @@ export const HF_AUDIO_FX_JOBS: readonly HfAudioFxJob[] = [
{
id: "add-clarity",
label: "Add Clarity",
- does: "It is hard to make out — it sits back.",
+ does: "It is hard to make out because it sits back.",
type: "peaking",
params: { frequency: 3000, gain: 2.5, q: 1 },
},
diff --git a/packages/core/src/audioFxPresets.ts b/packages/core/src/audioFxPresets.ts
index 2eb36f1f9e1..de72f51ffca 100644
--- a/packages/core/src/audioFxPresets.ts
+++ b/packages/core/src/audioFxPresets.ts
@@ -111,7 +111,7 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
"voice-broadcast",
"voice",
"Broadcast",
- "Denser and more forward — a radio-presenter sound.",
+ "Denser and more forward: a radio-presenter sound.",
[
{ type: "highpass", label: "Remove Rumble", params: { frequency: 90, q: 0.707, poles: "2" } },
{ type: "peaking", label: "Reduce Boxiness", params: { frequency: 400, gain: -3, q: 1.4 } },
@@ -159,7 +159,7 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
"room-gate",
"repair",
"Quiet Between Phrases",
- "Silences the gaps between words. Room tone under speech stays — this closes the pauses, it does not remove noise.",
+ "Silences the gaps between words. Room tone under speech stays: this closes the pauses, it does not remove noise.",
[
{
type: "gate",
@@ -184,23 +184,17 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
),
// ------------------------------------------------------------ character --
- preset(
- "telephone",
- "character",
- "Telephone",
- "Down the line — the narrow band of a phone call.",
- [
- ...steep("highpass", 300, "Strip the Bass"),
- ...steep("lowpass", 3400, "Strip the Treble"),
- { type: "peaking", label: "Phone Honk", params: { frequency: 1200, gain: 6, q: 1.2 } },
- { type: "peaking", label: "De-mud", params: { frequency: 550, gain: -4, q: 1 } },
- {
- type: "saturate",
- label: "Circuit Grit",
- params: { type: "tanh", threshold: -9, output: -2 },
- },
- ],
- ),
+ preset("telephone", "character", "Telephone", "Down the line: the narrow band of a phone call.", [
+ ...steep("highpass", 300, "Strip the Bass"),
+ ...steep("lowpass", 3400, "Strip the Treble"),
+ { type: "peaking", label: "Phone Honk", params: { frequency: 1200, gain: 6, q: 1.2 } },
+ { type: "peaking", label: "De-mud", params: { frequency: 550, gain: -4, q: 1 } },
+ {
+ type: "saturate",
+ label: "Circuit Grit",
+ params: { type: "tanh", threshold: -9, output: -2 },
+ },
+ ]),
preset("radio-am", "character", "AM Radio", "Narrow, gritty and a little crushed.", [
// Narrower than the megaphone at BOTH ends, which is most of the difference
// between them: an AM channel is a few kHz wide and the receiver rolls off
@@ -324,7 +318,7 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
"doofus-worble",
"character",
"Doofus Worble",
- "Seasick and wobbling — no straight signal left.",
+ "Seasick and wobbling, no straight signal left.",
[
{
type: "chorus",
@@ -357,7 +351,7 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
]),
// ---------------------------------------------------------------- space --
- preset("room-tight", "space", "Tight Room", "A small hard room — presence without wash.", [
+ preset("room-tight", "space", "Tight Room", "A small hard room: presence without wash.", [
{
type: "reverb",
label: "Tight Room",
@@ -380,7 +374,7 @@ export const HF_AUDIO_FX_PRESETS: readonly HfAudioFxPreset[] = [
preset("hall", "space", "Hall", "Long and open, for something that should sit far back.", [
{ type: "reverb", label: "Hall", params: { size: 0.9, damping: 0.3, wet: 0.4, dry: 0.75 } },
]),
- preset("slap-echo", "space", "Slap Echo", "One quick repeat — rockabilly vocal, not a wash.", [
+ preset("slap-echo", "space", "Slap Echo", "One quick repeat: rockabilly vocal, not a wash.", [
{ type: "delay", label: "Slap Echo", params: { time: 110, feedback: 0.12, mix: 0.22 } },
]),
preset("dub-throw", "space", "Dub Throw", "Repeats that trail off well behind the beat.", [
diff --git a/packages/core/src/audioLeveller.ts b/packages/core/src/audioLeveller.ts
index 012cb983e84..1c1f77ee807 100644
--- a/packages/core/src/audioLeveller.ts
+++ b/packages/core/src/audioLeveller.ts
@@ -242,7 +242,7 @@ export function removeLevelling(chain: HfAudioFxChain): {
/** What the module says when it is closed. */
export function levellingSummary(points: readonly HfAutomationPoint[]): string {
const moves = points.filter((p) => p.v !== 0);
- if (moves.length === 0) return "Already even — nothing to do";
+ if (moves.length === 0) return "Already even: nothing to do";
const lift = Math.max(...moves.map((p) => p.v));
const cut = Math.min(...moves.map((p) => p.v));
const parts: string[] = [];
diff --git a/packages/core/src/figma/client.ts b/packages/core/src/figma/client.ts
index 8d8b2eed1ff..c62d1d0312a 100644
--- a/packages/core/src/figma/client.ts
+++ b/packages/core/src/figma/client.ts
@@ -200,12 +200,12 @@ export function createFigmaClient(options: FigmaClientOptions): FigmaClient {
[
"FIGMA_TOKEN is missing. One-time setup:",
" 1. figma.com/settings → Security → Personal access tokens → Generate new token",
- " 2. Scopes (read-only is all this integration ever needs — it never writes to figma):",
+ " 2. Scopes (read-only is all this integration ever needs: it never writes to figma):",
" File content: Read-only · File metadata: Read-only",
" Library content: Read-only (needed for the `tokens` published-styles fallback)",
- " Variables: Read-only (optional — brand variables, requires figma Enterprise;",
+ " Variables: Read-only (optional: brand variables, requires figma Enterprise;",
" without it `tokens` falls back to published styles)",
- ' 3. export FIGMA_TOKEN="figd_…" — add it to your shell profile or the project .env',
+ ' 3. export FIGMA_TOKEN="figd_…", then add it to your shell profile or the project .env',
" so future sessions skip this step",
"Then re-run this command.",
].join("\n"),
@@ -237,26 +237,26 @@ export function createFigmaClient(options: FigmaClientOptions): FigmaClient {
if (body && /invalid token/i.test(body))
return new FigmaClientError(
"BAD_TOKEN",
- "figma rejected the token (403 Invalid token) — it is invalid, expired, or revoked. Re-mint at figma.com/settings → Security, then update FIGMA_TOKEN.",
+ "figma rejected the token (403 Invalid token): it is invalid, expired, or revoked. Re-mint at figma.com/settings → Security, then update FIGMA_TOKEN.",
403,
opts.endpoint,
);
if (opts.enterpriseGated)
return new FigmaClientError(
"REQUIRES_ENTERPRISE",
- "figma variables require an Enterprise plan (403) — fall back to styles",
+ "figma variables require an Enterprise plan (403), so fall back to styles",
403,
opts.endpoint,
);
if (body && /scope/i.test(body))
return new FigmaClientError(
"FORBIDDEN",
- `figma denied access (403): ${body} — add the named scope at figma.com/settings → Security → Personal access tokens.`,
+ `figma denied access (403): ${body}. Add the named scope at figma.com/settings → Security → Personal access tokens.`,
403,
opts.endpoint,
);
const scopeLine = opts.scopeHint
- ? `This endpoint needs the "${opts.scopeHint}" scope — add it at figma.com/settings → Security → Personal access tokens.`
+ ? `This endpoint needs the "${opts.scopeHint}" scope. Add it at figma.com/settings → Security → Personal access tokens.`
: "The token is missing a read scope, or your account can't view this file. Check File content: Read-only + File metadata: Read-only at figma.com/settings → Security.";
return new FigmaClientError(
"FORBIDDEN",
@@ -272,7 +272,7 @@ export function createFigmaClient(options: FigmaClientOptions): FigmaClient {
if (res.status === 401)
throw new FigmaClientError(
"BAD_TOKEN",
- "figma rejected the token (401) — it is expired or revoked. Re-mint at figma.com/settings → Security, then update FIGMA_TOKEN.",
+ "figma rejected the token (401): it is expired or revoked. Re-mint at figma.com/settings → Security, then update FIGMA_TOKEN.",
401,
opts.endpoint,
);
@@ -280,7 +280,7 @@ export function createFigmaClient(options: FigmaClientOptions): FigmaClient {
if (res.status === 429)
throw new FigmaClientError(
"RATE_LIMITED",
- `figma rate limit hit (429) and still limited after ${maxRetries} retries — wait a minute and re-run, or import fewer nodes per call.`,
+ `figma rate limit hit (429) and still limited after ${maxRetries} retries. Wait a minute and re-run, or import fewer nodes per call.`,
429,
opts.endpoint,
);
diff --git a/packages/core/src/figma/emitTimelineScript.ts b/packages/core/src/figma/emitTimelineScript.ts
index 9ec93d4ec19..564838caf87 100644
--- a/packages/core/src/figma/emitTimelineScript.ts
+++ b/packages/core/src/figma/emitTimelineScript.ts
@@ -36,7 +36,7 @@ export function emitTimelineScript(spec: TimelineSpec): string {
: 'typeof gsap === "undefined"';
const libs = needsCustomEase ? "gsap + CustomEase" : "gsap";
lines.push(
- `if (${missing}) { console.warn(${lit(`figma timeline ${spec.timelineId}: ${libs} not loaded — add the CDN