@jokerized/decksmith 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1053,24 +1053,55 @@ var ADVANCE = {
1053
1053
  var KERN_SLACK = 1.007;
1054
1054
  var TABULAR_FIGURE = 0.649;
1055
1055
  var TABULAR_SEPARATOR = 0.269;
1056
+ var BLOCK_ADVANCE = [
1057
+ [12288, 12351, 1],
1058
+ // CJK symbols and punctuation
1059
+ [12352, 12447, 1],
1060
+ // hiragana
1061
+ [12448, 12543, 1],
1062
+ // katakana
1063
+ [12592, 12687, 0.92],
1064
+ // Hangul compatibility jamo
1065
+ [13312, 19903, 1],
1066
+ // CJK unified ideographs, extension A
1067
+ [19968, 40959, 1],
1068
+ // CJK unified ideographs
1069
+ [44032, 55203, 0.92],
1070
+ // Hangul syllables
1071
+ [63744, 64255, 1],
1072
+ // CJK compatibility ideographs
1073
+ [65281, 65376, 1]
1074
+ // full-width forms
1075
+ ];
1076
+ var UNMEASURED = 1.02;
1056
1077
  function charUnits(c, tabular) {
1057
1078
  if (tabular) {
1058
- if (c >= "0" && c <= "9") return TABULAR_FIGURE;
1059
- if (c === "." || c === ",") return TABULAR_SEPARATOR;
1079
+ if (c >= "0" && c <= "9") return [TABULAR_FIGURE, true];
1080
+ if (c === "." || c === ",") return [TABULAR_SEPARATOR, true];
1060
1081
  }
1061
- return ADVANCE[c] ?? 1.02;
1082
+ const measured = ADVANCE[c];
1083
+ if (measured !== void 0) return [measured, true];
1084
+ const cp2 = c.codePointAt(0) ?? 0;
1085
+ for (const [lo, hi, units] of BLOCK_ADVANCE) if (cp2 >= lo && cp2 <= hi) return [units, false];
1086
+ return [UNMEASURED, true];
1062
1087
  }
1063
1088
  function weightFactor(weight) {
1064
1089
  return weight >= 700 ? 1.045 : weight >= 600 ? 1.03 : weight >= 500 ? 1.015 : 1;
1065
1090
  }
1066
1091
  function textWidth(text2, fontSize, weight = 400, tracking = 0, tabular = false) {
1067
- let units = 0;
1092
+ let weighted = 0;
1093
+ let emGrid = 0;
1068
1094
  let chars = 0;
1069
1095
  for (const c of text2) {
1070
- units += charUnits(c, tabular);
1096
+ const [units, scalesWithWeight] = charUnits(c, tabular);
1097
+ if (scalesWithWeight) weighted += units;
1098
+ else emGrid += units;
1071
1099
  chars++;
1072
1100
  }
1073
- return units * KERN_SLACK * fontSize * weightFactor(weight) + tracking * fontSize * chars;
1101
+ if (emGrid === 0) {
1102
+ return weighted * KERN_SLACK * fontSize * weightFactor(weight) + tracking * fontSize * chars;
1103
+ }
1104
+ return (weighted * weightFactor(weight) + emGrid) * KERN_SLACK * fontSize + tracking * fontSize * chars;
1074
1105
  }
1075
1106
  function wrap(text2, fontSize, maxWidth, weight = 400, tracking = 0) {
1076
1107
  if (maxWidth <= 0) return [text2];
@@ -4620,7 +4651,7 @@ var SENTENCES_PER_BEAT = 2;
4620
4651
  var FF_BEAT_SECONDS = 8;
4621
4652
  var MIN_BEAT_SECONDS = 4;
4622
4653
  var SHORT_FORM_CPS = 22;
4623
- var CUE_OVERHEAD = 1.28;
4654
+ var CUE_OVERHEAD = 1.3;
4624
4655
  var COMFORTABLE_CPS = 17;
4625
4656
  var MAX_PLAYBACK = 1.25;
4626
4657
  var SPEAKING_STOPS = {
@@ -4630,10 +4661,10 @@ var SPEAKING_STOPS = {
4630
4661
  };
4631
4662
  var RATE_STEPS = [
4632
4663
  ["+0%", 1],
4633
- ["+10%", 1.187],
4634
- ["+20%", 1.252],
4635
- ["+30%", 1.394],
4636
- ["+40%", 1.533]
4664
+ ["+10%", 1.086],
4665
+ ["+20%", 1.182],
4666
+ ["+30%", 1.278],
4667
+ ["+40%", 1.393]
4637
4668
  ];
4638
4669
  function durationPlan(prefs) {
4639
4670
  const speakingStops = SPEAKING_STOPS[prefs.narration.density];
package/dist/index.js CHANGED
@@ -1053,24 +1053,55 @@ var ADVANCE = {
1053
1053
  var KERN_SLACK = 1.007;
1054
1054
  var TABULAR_FIGURE = 0.649;
1055
1055
  var TABULAR_SEPARATOR = 0.269;
1056
+ var BLOCK_ADVANCE = [
1057
+ [12288, 12351, 1],
1058
+ // CJK symbols and punctuation
1059
+ [12352, 12447, 1],
1060
+ // hiragana
1061
+ [12448, 12543, 1],
1062
+ // katakana
1063
+ [12592, 12687, 0.92],
1064
+ // Hangul compatibility jamo
1065
+ [13312, 19903, 1],
1066
+ // CJK unified ideographs, extension A
1067
+ [19968, 40959, 1],
1068
+ // CJK unified ideographs
1069
+ [44032, 55203, 0.92],
1070
+ // Hangul syllables
1071
+ [63744, 64255, 1],
1072
+ // CJK compatibility ideographs
1073
+ [65281, 65376, 1]
1074
+ // full-width forms
1075
+ ];
1076
+ var UNMEASURED = 1.02;
1056
1077
  function charUnits(c, tabular) {
1057
1078
  if (tabular) {
1058
- if (c >= "0" && c <= "9") return TABULAR_FIGURE;
1059
- if (c === "." || c === ",") return TABULAR_SEPARATOR;
1079
+ if (c >= "0" && c <= "9") return [TABULAR_FIGURE, true];
1080
+ if (c === "." || c === ",") return [TABULAR_SEPARATOR, true];
1060
1081
  }
1061
- return ADVANCE[c] ?? 1.02;
1082
+ const measured = ADVANCE[c];
1083
+ if (measured !== void 0) return [measured, true];
1084
+ const cp2 = c.codePointAt(0) ?? 0;
1085
+ for (const [lo, hi, units] of BLOCK_ADVANCE) if (cp2 >= lo && cp2 <= hi) return [units, false];
1086
+ return [UNMEASURED, true];
1062
1087
  }
1063
1088
  function weightFactor(weight) {
1064
1089
  return weight >= 700 ? 1.045 : weight >= 600 ? 1.03 : weight >= 500 ? 1.015 : 1;
1065
1090
  }
1066
1091
  function textWidth(text2, fontSize, weight = 400, tracking = 0, tabular = false) {
1067
- let units = 0;
1092
+ let weighted = 0;
1093
+ let emGrid = 0;
1068
1094
  let chars = 0;
1069
1095
  for (const c of text2) {
1070
- units += charUnits(c, tabular);
1096
+ const [units, scalesWithWeight] = charUnits(c, tabular);
1097
+ if (scalesWithWeight) weighted += units;
1098
+ else emGrid += units;
1071
1099
  chars++;
1072
1100
  }
1073
- return units * KERN_SLACK * fontSize * weightFactor(weight) + tracking * fontSize * chars;
1101
+ if (emGrid === 0) {
1102
+ return weighted * KERN_SLACK * fontSize * weightFactor(weight) + tracking * fontSize * chars;
1103
+ }
1104
+ return (weighted * weightFactor(weight) + emGrid) * KERN_SLACK * fontSize + tracking * fontSize * chars;
1074
1105
  }
1075
1106
  function wrap(text2, fontSize, maxWidth, weight = 400, tracking = 0) {
1076
1107
  if (maxWidth <= 0) return [text2];
@@ -5131,7 +5162,7 @@ var SENTENCES_PER_BEAT = 2;
5131
5162
  var FF_BEAT_SECONDS = 8;
5132
5163
  var MIN_BEAT_SECONDS = 4;
5133
5164
  var SHORT_FORM_CPS = 22;
5134
- var CUE_OVERHEAD = 1.28;
5165
+ var CUE_OVERHEAD = 1.3;
5135
5166
  var COMFORTABLE_CPS = 17;
5136
5167
  var MAX_PLAYBACK = 1.25;
5137
5168
  var SPEAKING_STOPS = {
@@ -5141,10 +5172,10 @@ var SPEAKING_STOPS = {
5141
5172
  };
5142
5173
  var RATE_STEPS = [
5143
5174
  ["+0%", 1],
5144
- ["+10%", 1.187],
5145
- ["+20%", 1.252],
5146
- ["+30%", 1.394],
5147
- ["+40%", 1.533]
5175
+ ["+10%", 1.086],
5176
+ ["+20%", 1.182],
5177
+ ["+30%", 1.278],
5178
+ ["+40%", 1.393]
5148
5179
  ];
5149
5180
  function durationPlan(prefs) {
5150
5181
  const speakingStops = SPEAKING_STOPS[prefs.narration.density];
package/dist/mcp.js CHANGED
@@ -1057,24 +1057,55 @@ var ADVANCE = {
1057
1057
  var KERN_SLACK = 1.007;
1058
1058
  var TABULAR_FIGURE = 0.649;
1059
1059
  var TABULAR_SEPARATOR = 0.269;
1060
+ var BLOCK_ADVANCE = [
1061
+ [12288, 12351, 1],
1062
+ // CJK symbols and punctuation
1063
+ [12352, 12447, 1],
1064
+ // hiragana
1065
+ [12448, 12543, 1],
1066
+ // katakana
1067
+ [12592, 12687, 0.92],
1068
+ // Hangul compatibility jamo
1069
+ [13312, 19903, 1],
1070
+ // CJK unified ideographs, extension A
1071
+ [19968, 40959, 1],
1072
+ // CJK unified ideographs
1073
+ [44032, 55203, 0.92],
1074
+ // Hangul syllables
1075
+ [63744, 64255, 1],
1076
+ // CJK compatibility ideographs
1077
+ [65281, 65376, 1]
1078
+ // full-width forms
1079
+ ];
1080
+ var UNMEASURED = 1.02;
1060
1081
  function charUnits(c, tabular) {
1061
1082
  if (tabular) {
1062
- if (c >= "0" && c <= "9") return TABULAR_FIGURE;
1063
- if (c === "." || c === ",") return TABULAR_SEPARATOR;
1083
+ if (c >= "0" && c <= "9") return [TABULAR_FIGURE, true];
1084
+ if (c === "." || c === ",") return [TABULAR_SEPARATOR, true];
1064
1085
  }
1065
- return ADVANCE[c] ?? 1.02;
1086
+ const measured = ADVANCE[c];
1087
+ if (measured !== void 0) return [measured, true];
1088
+ const cp2 = c.codePointAt(0) ?? 0;
1089
+ for (const [lo, hi, units] of BLOCK_ADVANCE) if (cp2 >= lo && cp2 <= hi) return [units, false];
1090
+ return [UNMEASURED, true];
1066
1091
  }
1067
1092
  function weightFactor(weight) {
1068
1093
  return weight >= 700 ? 1.045 : weight >= 600 ? 1.03 : weight >= 500 ? 1.015 : 1;
1069
1094
  }
1070
1095
  function textWidth(text2, fontSize, weight = 400, tracking = 0, tabular = false) {
1071
- let units = 0;
1096
+ let weighted = 0;
1097
+ let emGrid = 0;
1072
1098
  let chars = 0;
1073
1099
  for (const c of text2) {
1074
- units += charUnits(c, tabular);
1100
+ const [units, scalesWithWeight] = charUnits(c, tabular);
1101
+ if (scalesWithWeight) weighted += units;
1102
+ else emGrid += units;
1075
1103
  chars++;
1076
1104
  }
1077
- return units * KERN_SLACK * fontSize * weightFactor(weight) + tracking * fontSize * chars;
1105
+ if (emGrid === 0) {
1106
+ return weighted * KERN_SLACK * fontSize * weightFactor(weight) + tracking * fontSize * chars;
1107
+ }
1108
+ return (weighted * weightFactor(weight) + emGrid) * KERN_SLACK * fontSize + tracking * fontSize * chars;
1078
1109
  }
1079
1110
  function wrap(text2, fontSize, maxWidth, weight = 400, tracking = 0) {
1080
1111
  if (maxWidth <= 0) return [text2];
@@ -5132,7 +5163,7 @@ var SENTENCES_PER_BEAT = 2;
5132
5163
  var FF_BEAT_SECONDS = 8;
5133
5164
  var MIN_BEAT_SECONDS = 4;
5134
5165
  var SHORT_FORM_CPS = 22;
5135
- var CUE_OVERHEAD = 1.28;
5166
+ var CUE_OVERHEAD = 1.3;
5136
5167
  var COMFORTABLE_CPS = 17;
5137
5168
  var MAX_PLAYBACK = 1.25;
5138
5169
  var SPEAKING_STOPS = {
@@ -5142,10 +5173,10 @@ var SPEAKING_STOPS = {
5142
5173
  };
5143
5174
  var RATE_STEPS = [
5144
5175
  ["+0%", 1],
5145
- ["+10%", 1.187],
5146
- ["+20%", 1.252],
5147
- ["+30%", 1.394],
5148
- ["+40%", 1.533]
5176
+ ["+10%", 1.086],
5177
+ ["+20%", 1.182],
5178
+ ["+30%", 1.278],
5179
+ ["+40%", 1.393]
5149
5180
  ];
5150
5181
  function durationPlan(prefs) {
5151
5182
  const speakingStops = SPEAKING_STOPS[prefs.narration.density];
@@ -212,13 +212,49 @@ export declare const SHORT_FORM_CPS = 22;
212
212
  * takes `+10%` and lands at 21.9. Wrong in the OUTPUT, not in a gate — no gate
213
213
  * looks at this, which is why it survived.
214
214
  *
215
- * SCALE-INVARIANCE IS ASSUMED, and is the one thing here not measured: the ratio
216
- * is taken at `+0%` and applied at every step, on the reasoning that speeding the
217
- * voice up shrinks the cue windows and the breaths between them together.
218
- * Measuring it at `+10%` and `+20%` means synthesising the demo again at those
219
- * rates. `test/duration.test.ts` pins the `+0%` end against the artifact.
215
+ * SCALE-INVARIANCE IS MEASURED, and it holds. The ratio is taken at `+0%` and
216
+ * applied at every step, on the reasoning that speeding the voice up shrinks the
217
+ * cue windows and the breaths between them together. `scripts/measure-cue-rate.mjs`
218
+ * re-synthesised all 37 segments at all five steps `+0%` included, in one
219
+ * session, because edge-tts does not repeat itself and a fresh rate judged
220
+ * against a stored baseline measures the drift instead:
221
+ *
222
+ * ```
223
+ * rate meanCps p95 cue p95/mean median/mean
224
+ * +0% 14.509 18.089 1.2467 1.0601
225
+ * +10% 15.752 20.392 1.2945 1.0653
226
+ * +20% 17.148 21.321 1.2434 1.0567
227
+ * +30% 18.540 23.263 1.2548 1.0572
228
+ * +40% 20.205 25.515 1.2628 1.0663
229
+ * ```
230
+ *
231
+ * No trend in rate — so this stays a constant rather than becoming a function of
232
+ * the step. The p95 column wobbles because p95 over 39 cues IS THE SECOND-HIGHEST
233
+ * CUE, one window wide; `median/mean` is flat to within a percent, and it is the
234
+ * honest read on whether the distribution's shape moves. It does not.
235
+ *
236
+ * IT AND `RATE_STEPS` ARE ONE UNIT, and they moved together — 2026-08-02, from
237
+ * the run above. Read that table's `speedup` note before changing either.
238
+ *
239
+ * WHY 1.30 AND NOT 1.2945. It has to sit above EVERY ratio the run observed,
240
+ * because the number it feeds is a ceiling on caption readability and being
241
+ * under it means shipping captions faster than `SHORT_FORM_CPS`. The old 1.28
242
+ * was below the `+10%` reading and got away with it only because the speedups
243
+ * beside it were inflated by more — two errors cancelling, which is not a margin
244
+ * anyone can reason about. Correcting the speedups removed that cover, so this
245
+ * had to rise with them.
246
+ *
247
+ * IT COSTS A STEP, KNOWINGLY. The demo's real p95 cue at `+20%` is 21.321,
248
+ * inside the ceiling of 22 — but this predicts 22.13 for that step and refuses
249
+ * it, so the deck takes `+10%` and speaks slower than the artifact proves it
250
+ * could. A central estimate of ~1.26 would admit `+20%` and track the artifact
251
+ * to within 0.6%; it would also under-predict on about half of future runs. The
252
+ * trade was made deliberately in favour of never being under, on a statistic
253
+ * measured once over 39 cues.
254
+ *
255
+ * `test/duration.test.ts` pins the `+0%` end against the artifact.
220
256
  */
221
- export declare const CUE_OVERHEAD = 1.28;
257
+ export declare const CUE_OVERHEAD = 1.3;
222
258
  /** Broadcast subtitle practice. Past this the captions stop being readable. */
223
259
  export declare const COMFORTABLE_CPS = 17;
224
260
  /** How far playback may be sped up before the captions are the problem. */
@@ -261,6 +297,29 @@ export declare const SPEAKING_STOPS: Record<Prefs["narration"]["density"], numbe
261
297
  * `+60%` it is 28.5 and the caption is gone before it is read. `+50%` is
262
298
  * excluded for measuring slower than the step below it.
263
299
  *
300
+ * THE SPEEDUPS BELOW ARE NOT FROM THAT TABLE. They were, and they were too high
301
+ * at every step. The table above times ONE 72-character sentence, and a short
302
+ * sentence carries proportionally more silence at its ends, so dividing its
303
+ * total duration overstates what the prosody rate does to speech itself.
304
+ * Re-measured over the anchor deck's 37 segments and 196 seconds:
305
+ *
306
+ * ```
307
+ * step was is from
308
+ * +10% 1.187 1.086 meanCps 15.752 / 14.509
309
+ * +20% 1.252 1.182 meanCps 17.148 / 14.509
310
+ * +30% 1.394 1.278 meanCps 18.540 / 14.509
311
+ * +40% 1.533 1.393 meanCps 20.205 / 14.509
312
+ * ```
313
+ *
314
+ * `CUE_OVERHEAD` rose from 1.28 to 1.30 in the same commit and for this reason:
315
+ * it was below the cue ratio measured at `+10%` and only safe because these
316
+ * numbers were inflated by more. Neither is a safe edit alone — see the
317
+ * paragraph there. One run of `scripts/measure-cue-rate.mjs` produces both.
318
+ *
319
+ * THE TABLE ABOVE STILL EARNS ITS PLACE, because it is the only measurement of
320
+ * `+50%` and `+60%` anyone has taken, and what it says about them is why this
321
+ * list stops at `+40%`. The re-measurement covered these five steps only.
322
+ *
264
323
  * Latin-measured. A CJK deck gets the same steps, which is a guess of the same
265
324
  * kind `SPEECH_CPS.cjk` already is — replace it the first time one is narrated.
266
325
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokerized/decksmith",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Turn a source document into an animated explanation deck.",
5
5
  "type": "module",
6
6
  "license": "MIT",