[object Object]

← back to Allnewsdaily

Vary daily Short layouts and narration with verified media proof

40c4f7be07882b234656274a9a61242077963f68 · 2026-09-11 08:18:12 -0700 · Steve Abrams

Files touched

Diff

commit 40c4f7be07882b234656274a9a61242077963f68
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 11 08:18:12 2026 -0700

    Vary daily Short layouts and narration with verified media proof
---
 scripts/short/VARIATION.md                       |  11 ++
 scripts/short/build-script.js                    |  30 ++---
 scripts/short/fit-caption.js                     |  11 ++
 scripts/short/formats.js                         |  37 +++++++
 scripts/short/formats.test.js                    |  60 ++++++++++
 scripts/short/render-short.js                    |  42 ++++---
 scripts/short/verify-variation.cjs               |  48 ++++++++
 verification/tk11343/bulletin-ffprobe.json       | 134 +++++++++++++++++++++++
 verification/tk11343/bulletin-thumb.jpg          | Bin 0 -> 100480 bytes
 verification/tk11343/centered-ffprobe.json       | 134 +++++++++++++++++++++++
 verification/tk11343/centered-thumb.jpg          | Bin 0 -> 80707 bytes
 verification/tk11343/e2e-proof.json              |  67 ++++++++++++
 verification/tk11343/layout-results.json         |  23 ++++
 verification/tk11343/narrated-narration.txt      |   8 ++
 verification/tk11343/narrated-out.mp4            | Bin 0 -> 9516158 bytes
 verification/tk11343/narrated-script.json        |  58 ++++++++++
 verification/tk11343/narrated-stories.json       |  48 ++++++++
 verification/tk11343/narrated-thumb.jpg          | Bin 0 -> 178401 bytes
 verification/tk11343/negative-render.txt         |   4 +
 verification/tk11343/sidebar-ffprobe.json        | 134 +++++++++++++++++++++++
 verification/tk11343/sidebar-thumb.jpg           | Bin 0 -> 87600 bytes
 verification/tk11343/tk11343-narrated-proof.json |   9 ++
 verification/tk11343/tk11343-narrated-render.txt |  30 +++++
 verification/tk11343/tk11343-render.txt          |  12 ++
 verification/tk11343/tk11343-tests.txt           |  13 +++
 25 files changed, 886 insertions(+), 27 deletions(-)

diff --git a/scripts/short/VARIATION.md b/scripts/short/VARIATION.md
new file mode 100644
index 0000000..747de63
--- /dev/null
+++ b/scripts/short/VARIATION.md
@@ -0,0 +1,11 @@
+# Deterministic Short variation (TK-11343)
+
+Each valid YYYY-MM-DD edition selects centered, sidebar or bulletin using its UTC day number modulo three. Retries choose the same layout and factual opening/closing copy. Headline text, attribution and source order remain intact; the existing trailing-beat trimming policy stays in place. Scripts still exceeding58 seconds after trimming fail instead of producing a truncated narration.
+
+The script now carries format and edition. Run build-script.js before render-short.js; older script artifacts must be rebuilt. Real ImageMagick text measurements shrink long captions to fit their assigned box; impossible fits fail. Render scratch directories are unique and retained for evidence. Media files keep the1080x1920, H264, yuv420p,30fps,AAC contract and58-second mux cap.
+
+Run `node --test scripts/short/formats.test.js`. In a fresh isolated checkout without runtime assets or voiceover, run `node scripts/short/verify-variation.cjs /absolute/fresh/evidence-directory`. It runs a complete five-story edition and two complete one-story preview editions, probes each and exercises invalid renderer input. All headlines are labeled synthetic fixtures, with silent AAC audio. The45–55second editorial target is advisory; these fixtures measure35,14.5 and13.7seconds without shortening their script timing.
+
+Integrated with Steve's approval on September 11, 2026, preserving the newer SVG-image guard. Proof is in `verification/tk11343/e2e-proof.json`: five tests, three complete layout previews and a 43.67-second six-story render with free local narration, existing stock images and source logos. The decoded output preserves the narration with 0.99999 PCM correlation. Card timing retains the existing proportional estimates; this is not word-level synchronization.
+
+The existing orchestrator rebuilds the script before TTS/rendering, so subsequent scheduled editions use the new format metadata. Directly rendering an older script requires rebuilding it first. No upload, paid TTS, stock fetch, scheduler change or remote deployment was performed for this integration. Production voice and channel reach were not tested; layout variation does not establish a reach or monetization improvement.
diff --git a/scripts/short/build-script.js b/scripts/short/build-script.js
index 6eebed6..8c573a3 100644
--- a/scripts/short/build-script.js
+++ b/scripts/short/build-script.js
@@ -10,6 +10,7 @@
 'use strict';
 const fs = require('fs');
 const path = require('path');
+const { editionFormat, wording, validateScript } = require('./formats');
 
 const ROOT = path.resolve(__dirname, '..', '..');
 const IN = path.join(ROOT, 'data', 'short', 'stories.json');
@@ -58,20 +59,13 @@ function assemble(intro, beats, outro) {
   return { intro, beats, outro, totalSec: total, narration };
 }
 
-function main() {
-  if (!fs.existsSync(IN)) {
-    console.error(`[build-script] FATAL: stories.json not found at ${IN}. Run pick-stories.js first.`);
-    process.exit(1);
-  }
-  const data = JSON.parse(fs.readFileSync(IN, 'utf8'));
+function buildScript(data) {
+  const format = editionFormat(data.date);
   const stories = Array.isArray(data.stories) ? data.stories : [];
-  if (stories.length === 0) {
-    console.error('[build-script] FATAL: stories.json has no stories.');
-    process.exit(1);
+  if (!stories.length || stories.some(s => !s || typeof s.headline !== 'string' || !s.headline.trim() || typeof s.outlet !== 'string' || !s.outlet.trim())) {
+    throw new Error('Stories require a headline and outlet');
   }
-
-  const introText = `All News Daily. Your headlines for ${monthDay(data.date)}.`;
-  const outroText = `That's your briefing. Full stories and live coverage at all news daily dot com.`;
+  const [introText, outroText] = wording(format.id, monthDay(data.date));
   const intro = { text: introText, estSec: estSec(introText) };
   const outro = { text: outroText, estSec: estSec(outroText) };
 
@@ -90,6 +84,16 @@ function main() {
     console.warn(`[build-script] still over cap — trimmed to ${beats.length} beats.`);
   }
 
+  script.format = format.id;
+  script.edition = data.date;
+  validateScript(script);
+  return script;
+}
+
+function main() {
+  const data = JSON.parse(fs.readFileSync(IN, 'utf8'));
+  const script = buildScript(data);
+  const beats = script.beats;
   fs.mkdirSync(OUT_DIR, { recursive: true });
   fs.writeFileSync(OUT, JSON.stringify(script, null, 2));
 
@@ -105,4 +109,4 @@ if (require.main === module) {
   try { main(); }
   catch (e) { console.error('[build-script] FATAL:', e && e.stack || e); process.exit(1); }
 }
-module.exports = { main };
+module.exports = { main, buildScript };
diff --git a/scripts/short/fit-caption.js b/scripts/short/fit-caption.js
new file mode 100644
index 0000000..f3c5a59
--- /dev/null
+++ b/scripts/short/fit-caption.js
@@ -0,0 +1,11 @@
+"use strict";
+const { execFileSync } = require('child_process');
+function fitCaption(magick, font, textFile, box) {
+  const [width, height] = box.split('x').map(Number);
+  for (let size = 100; size >= 10; size -= 5) {
+    const measured = Number(execFileSync(magick, ['-background','none','-font',font,'-pointsize',String(size),'-size',`${width}x`, `caption:@${textFile}`, '-format','%h','info:'], {encoding:'utf8'}).trim());
+    if (Number.isFinite(measured) && measured > 0 && measured <= height) return size;
+  }
+  throw new Error('Headline cannot fit within its card; refusing to clip text');
+}
+module.exports = { fitCaption };
diff --git a/scripts/short/formats.js b/scripts/short/formats.js
new file mode 100644
index 0000000..ded2264
--- /dev/null
+++ b/scripts/short/formats.js
@@ -0,0 +1,37 @@
+"use strict";
+
+const FORMATS = [
+  { id: 'centered', label: 'DAILY BRIEFING', box: '1120x1040', x: 115, y: 650, gravity: 'center', sourceY: 1900 },
+  { id: 'sidebar', label: 'HEADLINE DESK', box: '940x1120', x: 290, y: 610, gravity: 'NorthWest', sourceY: 1900 },
+  { id: 'bulletin', label: 'NEWS BULLETIN', box: '1080x960', x: 135, y: 940, gravity: 'NorthWest', sourceY: 2000 },
+];
+
+function editionFormat(date) {
+  if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date)) throw new Error('Edition date must be YYYY-MM-DD');
+  const ms = Date.parse(date + 'T00:00:00Z');
+  if (!Number.isFinite(ms) || new Date(ms).toISOString().slice(0, 10) !== date) throw new Error('Invalid edition date');
+  return FORMATS[((ms / 86400000) % 3 + 3) % 3];
+}
+
+function wording(format, dateLabel) {
+  const variants = {
+    centered: [`All News Daily. Your headlines for ${dateLabel}.`, 'That concludes this briefing. Read the source stories at all news daily dot com.'],
+    sidebar: [`All News Daily. Here is the ${dateLabel} news briefing.`, 'Those are the headlines. Find the source reporting at all news daily dot com.'],
+    bulletin: [`All News Daily. The headline bulletin for ${dateLabel}.`, 'This bulletin is complete. Source stories are at all news daily dot com.'],
+  };
+  if (!variants[format]) throw new Error('Unknown Short format');
+  return variants[format];
+}
+
+function validateScript(script) {
+  if (!script || !FORMATS.some(f => f.id === script.format)) throw new Error('Missing or invalid Short format');
+  if (!Array.isArray(script.beats) || !script.beats.length) throw new Error('Script requires beats');
+  const segments = [script.intro, ...script.beats, script.outro];
+  if (segments.some(s => !s || typeof s.text !== 'string' || !s.text.trim() || !Number.isFinite(s.estSec) || s.estSec <= 0)) throw new Error('Invalid text or segment timing');
+  if (script.beats.some((s, i) => s.n !== i + 1 || typeof s.headline !== 'string' || !s.headline.trim() || typeof s.outlet !== 'string' || !s.outlet.trim())) throw new Error('Invalid beat order or attribution');
+  const sum = segments.reduce((a, s) => a + s.estSec, 0);
+  if (!Number.isFinite(script.totalSec) || script.totalSec <= 0 || script.totalSec > 58 || Math.abs(sum - script.totalSec) > 0.11) throw new Error('Script duration must match segments and remain within 58 seconds');
+  return FORMATS.find(f => f.id === script.format);
+}
+
+module.exports = { FORMATS, editionFormat, wording, validateScript };
diff --git a/scripts/short/formats.test.js b/scripts/short/formats.test.js
new file mode 100644
index 0000000..399d358
--- /dev/null
+++ b/scripts/short/formats.test.js
@@ -0,0 +1,60 @@
+"use strict";
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const { buildScript } = require('./build-script');
+const { editionFormat, validateScript } = require('./formats');
+const fixture = date => ({ date, stories: [
+  { headline: 'Fixture: The museum opened a new exhibition on Monday.', outlet: 'Fixture Civic Desk' },
+  { headline: 'Fixture: The library added evening hours for visitors.', outlet: 'Fixture Library Record' },
+  { headline: 'Fixture: The city published its annual transport report.', outlet: 'Fixture City Bulletin' },
+  { headline: 'Fixture: The university announced three public science lectures.', outlet: 'Fixture Campus Desk' },
+  { headline: 'Fixture: The park reopened its walking path on Tuesday.', outlet: 'Fixture Parks Record' },
+] });
+test('same edition is identical and consecutive editions cover three formats', () => {
+  const ids = new Set(); const intros = new Set(); const outros = new Set();
+  for (const date of ['2026-09-10', '2026-09-11', '2026-09-12']) {
+    const data = fixture(date); const a = buildScript(data);
+    assert.deepEqual(a, buildScript(data)); ids.add(a.format); intros.add(a.intro.text); outros.add(a.outro.text);
+    assert.deepEqual(a.beats.map(b => [b.headline, b.outlet]), data.stories.map(b => [b.headline, b.outlet]));
+    assert.ok(a.totalSec <= 58); validateScript(a);
+  }
+  assert.equal(ids.size, 3); assert.equal(intros.size, 3); assert.equal(outros.size, 3);
+});
+test('dates and missing attribution fail closed', () => {
+  for (const d of ['', 'bad', '2026-02-30', '2026-13-01']) assert.throws(() => editionFormat(d));
+  assert.throws(() => buildScript({date:'2026-09-10',stories:[]}));
+  assert.throws(() => buildScript({date:'2026-09-10',stories:[{headline:'Fixture'}]}));
+});
+test('overlong facts are rejected without rewriting them', () => {
+  const data = fixture('2026-09-10'); data.stories = data.stories.slice(0,3).map(s => ({...s, headline: 'Fact '.repeat(160)}));
+  assert.throws(() => buildScript(data), /58 seconds/);
+});
+test('renderer input rejects tampered duration, format, beat order and zero timing', () => {
+  for (const mutate of [s => s.totalSec = 59, s => s.totalSec = 1, s => s.format = 'unknown', s => s.beats[0].n = 4, s => s.intro.estSec = 0]) {
+    const s = buildScript(fixture('2026-09-10')); mutate(s); assert.throws(() => validateScript(s));
+  }
+});
+module.exports = { fixture };
+
+test('real ImageMagick fitting preserves short size and shrinks long accepted headlines', () => {
+ const fs=require('fs'),os=require('os'),path=require('path');
+ const { execFileSync }=require('child_process');
+ const { fitCaption }=require('./fit-caption');
+ const { FORMATS }=require('./formats');
+ const dir=fs.mkdtempSync(path.join(os.tmpdir(),'short-fit-proof-'));
+ const file=path.join(dir,'headline.txt');
+ const font='/System/Library/Fonts/Supplemental/Arial Bold.ttf';
+ const magick='/opt/homebrew/bin/magick';
+ const long='Fixture: '+Array(68).fill('museum').join(' ')+'.';
+ const data=fixture('2026-09-10'); data.stories=data.stories.slice(0,3); data.stories[0].headline=long;
+ assert.ok(buildScript(data).totalSec<=58);
+ fs.writeFileSync(file,long);
+ for(const layout of FORMATS) {
+  const size=fitCaption(magick,font,file,layout.box); assert.ok(size<100);
+  const [w,h]=layout.box.split('x').map(Number);
+  const measured=Number(execFileSync(magick,['-font',font,'-pointsize',String(size),'-size',`${w}x`,`caption:@${file}`,'-format','%h','info:'],{encoding:'utf8'}));
+  assert.ok(measured<=h);
+ }
+ fs.writeFileSync(file,fixture('2026-09-10').stories[0].headline);
+ for(const layout of FORMATS) assert.equal(fitCaption(magick,font,file,layout.box),100);
+});
diff --git a/scripts/short/render-short.js b/scripts/short/render-short.js
index 86faab2..38c518b 100644
--- a/scripts/short/render-short.js
+++ b/scripts/short/render-short.js
@@ -16,13 +16,15 @@
 const fs = require('fs');
 const path = require('path');
 const { execFileSync } = require('child_process');
+const { validateScript } = require('./formats');
+const { fitCaption } = require('./fit-caption');
 
 // ---------------------------------------------------------------------------
 // Paths
 // ---------------------------------------------------------------------------
 const ROOT = path.resolve(__dirname, '..', '..');            // ~/Projects/allnewsdaily
 const DATA = path.join(ROOT, 'data', 'short');
-const TMP = path.join(DATA, '.rtmp');
+let TMP;
 const IMG_DIR = path.join(DATA, 'img');   // stock backgrounds from fetch-stock.mjs (optional)
 const LOGO_DIR = path.join(DATA, 'logo'); // per-network source logos from fetch-stock.mjs (optional)
 
@@ -92,10 +94,8 @@ const HAS_DRAWTEXT = (() => {
   try { return /(^|\s)drawtext(\s|$)/m.test(run(FFMPEG, ['-hide_banner', '-filters'])); }
   catch (_) { return false; }
 })();
-if (!MAGICK && !HAS_DRAWTEXT) {
-  die('no text renderer available: this ffmpeg lacks the drawtext filter AND ImageMagick ' +
-    '(`magick`/`convert`) is not installed. Install one: brew install imagemagick  (or an ' +
-    'ffmpeg built --enable-libfreetype).');
+if (!MAGICK) {
+  die('ImageMagick is required to composite cards. Install: brew install imagemagick');
 }
 if (!HAS_DRAWTEXT) console.warn('[render-short] NOTE: ffmpeg has no drawtext filter — compositing card text with ImageMagick.');
 
@@ -104,8 +104,11 @@ const script = readJSON(SCRIPT_PATH, 'script.json');
 if (!script.beats || !Array.isArray(script.beats) || !script.beats.length) die('script.json has no beats[]');
 if (!script.intro || !script.outro) die('script.json missing intro/outro');
 
-fs.rmSync(TMP, { recursive: true, force: true });
-fs.mkdirSync(TMP, { recursive: true });
+let layout;
+try { layout = validateScript(script); } catch (error) { die(error.message); }
+fs.mkdirSync(DATA, { recursive: true });
+TMP = fs.mkdtempSync(path.join(DATA, '.render-'));
+console.log(`[render-short] format=${layout.id} retained scratch=${TMP}`);
 
 // Resolve the branded font (Arial Bold, Helvetica fallback). Passed as a plain CLI arg to
 // ImageMagick, so the space in the path is fine (no filtergraph escaping needed).
@@ -237,11 +240,22 @@ function buildCardPng(seg, idx) {
     '-fill', HEX_INK, '-pointsize', '56', '-annotate', '+0+215', 'A L L   N E W S   D A I L Y',
     '-fill', HEX_RED, '-draw', 'rectangle 540,352 810,366', png]);
 
-  // 3) headline / main text — auto-fit inside a centered box (the word-wrap)
+  const marker = seg.kind === 'beat' ? String(seg.n).padStart(2, '0') : layout.label;
+  if (layout.id === 'sidebar') {
+    run(MAGICK, [png, '-fill', HEX_RED, '-draw', 'rectangle 85,600 235,1740',
+      '-font', FONT, '-fill', 'white', '-pointsize', seg.kind === 'beat' ? '94' : '34',
+      '-gravity', 'NorthWest', '-annotate', '+95+640', seg.kind === 'beat' ? marker : 'AND', png]);
+  } else if (layout.id === 'bulletin') {
+    run(MAGICK, [png, '-fill', HEX_RED, '-draw', 'rectangle 95,550 1255,810',
+      '-fill', '#24242A', '-draw', 'rectangle 95,875 1255,1925',
+      '-font', FONT, '-fill', 'white', '-pointsize', '100', '-gravity', 'NorthWest',
+      '-annotate', '+140+600', seg.kind === 'beat' ? `STORY ${marker}` : marker, png]);
+  }
   const hlFile = writeRaw(`hl${idx}.txt`, seg.main);
+  const pointSize = fitCaption(MAGICK, FONT, hlFile, layout.box);
   run(MAGICK, [png, '(', '-background', 'none', '-fill', HEX_INK, '-font', FONT,
-    '-size', '1160x1040', '-gravity', 'center', `caption:@${hlFile}`, ')',
-    '-gravity', 'center', '-geometry', '+0+0', '-composite', png]);
+    '-pointsize', String(pointSize), '-size', layout.box, '-gravity', layout.gravity, `caption:@${hlFile}`, ')',
+    '-gravity', 'NorthWest', '-geometry', `+${layout.x}+${layout.y}`, '-composite', png]);
 
   // 4) lower-third source chip — the network's real LOGO + name (beats with a logo),
   //    else the red "VIA {OUTLET}" text chip (intro/outro, or if the logo fetch missed).
@@ -256,14 +270,14 @@ function buildCardPng(seg, idx) {
       `label:@${nmFile}`, '-gravity', 'center', '-background', 'white', '-extent', 'x104', nameImg]);
     const chip = path.join(TMP, `chip${idx}.png`);
     run(MAGICK, [logoSq, nameImg, '+append', '-bordercolor', 'white', '-border', '26x22', chip]);
-    run(MAGICK, [png, chip, '-gravity', 'North', '-geometry', '+0+1852', '-composite', png]);
+    run(MAGICK, [png, chip, '-gravity', 'North', '-geometry', `+0+${layout.sourceY}`, '-composite', png]);
   } else {
     const chipText = seg.outlet ? ('VIA ' + String(seg.outlet).toUpperCase()) : (seg.tag || null);
     if (chipText) {
       const chFile = writeRaw(`chip${idx}.txt`, chipText);
       run(MAGICK, [png, '(', '-background', HEX_RED, '-fill', 'white', '-font', FONT,
         '-pointsize', '46', `label:@${chFile}`, '-bordercolor', HEX_RED, '-border', '28x18', ')',
-        '-gravity', 'North', '-geometry', '+0+1900', '-composite', png]);
+        '-gravity', 'North', '-geometry', `+0+${layout.sourceY}`, '-composite', png]);
     }
   }
   return png;
@@ -304,13 +318,13 @@ if (!silent) {
   run(FFMPEG, ['-y', '-i', VIDEO, '-i', VO_PATH,
     '-map', '0:v:0', '-map', '1:a:0',
     '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k',
-    '-shortest', '-movflags', '+faststart', OUT_PATH]);
+    '-t', String(HARD_CAP_SEC), '-shortest', '-movflags', '+faststart', OUT_PATH]);
 } else {
   run(FFMPEG, ['-y', '-i', VIDEO,
     '-f', 'lavfi', '-i', 'anullsrc=channel_layout=stereo:sample_rate=44100',
     '-map', '0:v:0', '-map', '1:a:0',
     '-c:v', 'copy', '-c:a', 'aac', '-b:a', '128k',
-    '-shortest', '-movflags', '+faststart', OUT_PATH]);
+    '-t', String(HARD_CAP_SEC), '-shortest', '-movflags', '+faststart', OUT_PATH]);
 }
 
 // ---------------------------------------------------------------------------
diff --git a/scripts/short/verify-variation.cjs b/scripts/short/verify-variation.cjs
new file mode 100644
index 0000000..ac865d3
--- /dev/null
+++ b/scripts/short/verify-variation.cjs
@@ -0,0 +1,48 @@
+"use strict";
+const fs = require('fs');
+const path = require('path');
+const { execFileSync, spawnSync } = require('child_process');
+const assert = require('node:assert/strict');
+const { buildScript } = require('./build-script');
+const root = path.resolve(__dirname, '../..');
+const evidence = process.argv[2];
+assert.ok(evidence, 'Pass a fresh evidence directory');
+const data = path.join(root, 'data/short');
+fs.mkdirSync(evidence, {recursive:true}); fs.mkdirSync(data, {recursive:true});
+const headlines = [
+ 'Fixture: The museum opened a new exhibition on Monday.',
+ 'Fixture: The library added evening hours for visitors.',
+ 'Fixture: The city published its annual transport report.',
+ 'Fixture: The university announced three public science lectures.',
+ 'Fixture: The park reopened its walking path on Tuesday.',
+];
+const results = [];
+for (const date of ['2026-09-10','2026-09-11','2026-09-12']) {
+ const stories = {date, stories: (date === '2026-09-10' ? headlines : headlines.slice(0,1)).map((headline,i) => ({n:i+1,headline,outlet:'Fixture Civic Record '+(i+1)}))};
+ fs.writeFileSync(path.join(data,'stories.json'),JSON.stringify(stories,null,2));
+ const buildLog = execFileSync(process.execPath,[path.join(__dirname,'build-script.js')],{encoding:'utf8'});
+ const script = JSON.parse(fs.readFileSync(path.join(data,'script.json')));
+ const dir = path.join(evidence,script.format); fs.mkdirSync(dir,{recursive:true});
+ const reuse = process.argv.includes('--resume') && fs.existsSync(path.join(dir,'out.mp4'));
+ if (reuse) assert.deepEqual(JSON.parse(fs.readFileSync(path.join(dir,'script.json'))),script, 'Refuse stale media from a different script');
+ fs.copyFileSync(path.join(data,'stories.json'),path.join(dir,'stories.json'));
+ fs.copyFileSync(path.join(data,'script.json'),path.join(dir,'script.json'));
+ const log = reuse ? fs.readFileSync(path.join(dir,'render.log'),'utf8') : execFileSync(process.execPath,[path.join(__dirname,'render-short.js')],{encoding:'utf8',maxBuffer:20*1024*1024});
+ fs.writeFileSync(path.join(dir,'render.log'),buildLog+'\n'+log);
+ if (!reuse) for (const file of ['out.mp4','thumb.jpg']) fs.copyFileSync(path.join(data,file),path.join(dir,file));
+ const probe=JSON.parse(execFileSync('ffprobe',['-v','error','-show_streams','-show_format','-of','json',path.join(dir,'out.mp4')],{encoding:'utf8'}));
+ fs.writeFileSync(path.join(dir,'ffprobe.json'),JSON.stringify(probe,null,2));
+ const v=probe.streams.find(s=>s.codec_type==='video'),a=probe.streams.find(s=>s.codec_type==='audio');
+ assert.equal(v.width,1080); assert.equal(v.height,1920); assert.equal(v.codec_name,'h264'); assert.equal(v.pix_fmt,'yuv420p'); assert.equal(v.avg_frame_rate,'30/1'); assert.equal(a.codec_name,'aac');
+ assert.ok(+probe.format.duration < 60 && Math.abs(+probe.format.duration - script.totalSec) < 0.5);
+ const scratch = log.match(/retained scratch=(.+)/)[1];
+ fs.copyFileSync(path.join(scratch,'card1.png'),path.join(dir,'lead-card.png'));
+ results.push({format:script.format,duration:+probe.format.duration,dir,scratch,verdict:'PASS'});
+ console.log(JSON.stringify(results.at(-1)));
+}
+const script=buildScript({date:'2026-09-10',stories:[{headline:'Fixture fact.',outlet:'Fixture Record'}]});
+script.totalSec=61;fs.writeFileSync(path.join(data,'script.json'),JSON.stringify(script));
+const bad=spawnSync(process.execPath,[path.join(__dirname,'render-short.js')],{encoding:'utf8'});
+assert.notEqual(bad.status,0); assert.match(bad.stderr,/58 seconds/);
+fs.writeFileSync(path.join(evidence,'negative-render.log'),bad.stdout+bad.stderr);
+fs.writeFileSync(path.join(evidence,'results.json'),JSON.stringify(results,null,2));
diff --git a/verification/tk11343/bulletin-ffprobe.json b/verification/tk11343/bulletin-ffprobe.json
new file mode 100644
index 0000000..67807e3
--- /dev/null
+++ b/verification/tk11343/bulletin-ffprobe.json
@@ -0,0 +1,134 @@
+{
+  "streams": [
+    {
+      "index": 0,
+      "codec_name": "h264",
+      "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+      "profile": "High",
+      "codec_type": "video",
+      "codec_tag_string": "avc1",
+      "codec_tag": "0x31637661",
+      "mime_codec_string": "avc1.640028",
+      "width": 1080,
+      "height": 1920,
+      "coded_width": 1080,
+      "coded_height": 1920,
+      "has_b_frames": 2,
+      "pix_fmt": "yuv420p",
+      "level": 40,
+      "chroma_location": "left",
+      "field_order": "progressive",
+      "is_avc": "true",
+      "nal_length_size": "4",
+      "id": "0x1",
+      "r_frame_rate": "30/1",
+      "avg_frame_rate": "30/1",
+      "time_base": "1/15360",
+      "start_pts": 0,
+      "start_time": "0.000000",
+      "duration_ts": 210432,
+      "duration": "13.700000",
+      "bit_rate": "1493167",
+      "bits_per_raw_sample": "8",
+      "nb_frames": "411",
+      "extradata_size": 46,
+      "disposition": {
+        "default": 1,
+        "dub": 0,
+        "original": 0,
+        "comment": 0,
+        "lyrics": 0,
+        "karaoke": 0,
+        "forced": 0,
+        "hearing_impaired": 0,
+        "visual_impaired": 0,
+        "clean_effects": 0,
+        "attached_pic": 0,
+        "timed_thumbnails": 0,
+        "non_diegetic": 0,
+        "captions": 0,
+        "descriptions": 0,
+        "metadata": 0,
+        "dependent": 0,
+        "still_image": 0,
+        "multilayer": 0
+      },
+      "tags": {
+        "language": "und",
+        "handler_name": "VideoHandler",
+        "encoder": "Lavc62.28.102 libx264"
+      }
+    },
+    {
+      "index": 1,
+      "codec_name": "aac",
+      "codec_long_name": "AAC (Advanced Audio Coding)",
+      "profile": "LC",
+      "codec_type": "audio",
+      "codec_tag_string": "mp4a",
+      "codec_tag": "0x6134706d",
+      "mime_codec_string": "mp4a.40.2",
+      "sample_fmt": "fltp",
+      "sample_rate": "44100",
+      "channels": 2,
+      "channel_layout": "stereo",
+      "bits_per_sample": 0,
+      "initial_padding": 0,
+      "id": "0x2",
+      "r_frame_rate": "0/0",
+      "avg_frame_rate": "0/0",
+      "time_base": "1/44100",
+      "start_pts": 0,
+      "start_time": "0.000000",
+      "duration_ts": 604126,
+      "duration": "13.699002",
+      "bit_rate": "2096",
+      "nb_frames": "591",
+      "extradata_size": 5,
+      "disposition": {
+        "default": 1,
+        "dub": 0,
+        "original": 0,
+        "comment": 0,
+        "lyrics": 0,
+        "karaoke": 0,
+        "forced": 0,
+        "hearing_impaired": 0,
+        "visual_impaired": 0,
+        "clean_effects": 0,
+        "attached_pic": 0,
+        "timed_thumbnails": 0,
+        "non_diegetic": 0,
+        "captions": 0,
+        "descriptions": 0,
+        "metadata": 0,
+        "dependent": 0,
+        "still_image": 0,
+        "multilayer": 0
+      },
+      "tags": {
+        "language": "und",
+        "handler_name": "SoundHandler"
+      }
+    }
+  ],
+  "format": {
+    "filename": "/private/tmp/tk11343-release-dztfki3_/verification-media/bulletin/out.mp4",
+    "nb_streams": 2,
+    "nb_programs": 0,
+    "nb_stream_groups": 0,
+    "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
+    "format_long_name": "QuickTime / MOV",
+    "start_time": "0.000000",
+    "duration": "13.700000",
+    "size": "2576922",
+    "bit_rate": "1504771",
+    "probe_score": 100,
+    "tags": {
+      "major_brand": "isom",
+      "minor_version": "512",
+      "compatible_brands": "isomiso2avc1mp41",
+      "encoder": "Lavf62.12.102"
+    }
+  }
+}
\ No newline at end of file
diff --git a/verification/tk11343/bulletin-thumb.jpg b/verification/tk11343/bulletin-thumb.jpg
new file mode 100644
index 0000000..135d47d
Binary files /dev/null and b/verification/tk11343/bulletin-thumb.jpg differ
diff --git a/verification/tk11343/centered-ffprobe.json b/verification/tk11343/centered-ffprobe.json
new file mode 100644
index 0000000..a66d6d0
--- /dev/null
+++ b/verification/tk11343/centered-ffprobe.json
@@ -0,0 +1,134 @@
+{
+  "streams": [
+    {
+      "index": 0,
+      "codec_name": "h264",
+      "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+      "profile": "High",
+      "codec_type": "video",
+      "codec_tag_string": "avc1",
+      "codec_tag": "0x31637661",
+      "mime_codec_string": "avc1.640028",
+      "width": 1080,
+      "height": 1920,
+      "coded_width": 1080,
+      "coded_height": 1920,
+      "has_b_frames": 2,
+      "pix_fmt": "yuv420p",
+      "level": 40,
+      "chroma_location": "left",
+      "field_order": "progressive",
+      "is_avc": "true",
+      "nal_length_size": "4",
+      "id": "0x1",
+      "r_frame_rate": "30/1",
+      "avg_frame_rate": "30/1",
+      "time_base": "1/15360",
+      "start_pts": 0,
+      "start_time": "0.000000",
+      "duration_ts": 537600,
+      "duration": "35.000000",
+      "bit_rate": "1228262",
+      "bits_per_raw_sample": "8",
+      "nb_frames": "1050",
+      "extradata_size": 46,
+      "disposition": {
+        "default": 1,
+        "dub": 0,
+        "original": 0,
+        "comment": 0,
+        "lyrics": 0,
+        "karaoke": 0,
+        "forced": 0,
+        "hearing_impaired": 0,
+        "visual_impaired": 0,
+        "clean_effects": 0,
+        "attached_pic": 0,
+        "timed_thumbnails": 0,
+        "non_diegetic": 0,
+        "captions": 0,
+        "descriptions": 0,
+        "metadata": 0,
+        "dependent": 0,
+        "still_image": 0,
+        "multilayer": 0
+      },
+      "tags": {
+        "language": "und",
+        "handler_name": "VideoHandler",
+        "encoder": "Lavc62.28.102 libx264"
+      }
+    },
+    {
+      "index": 1,
+      "codec_name": "aac",
+      "codec_long_name": "AAC (Advanced Audio Coding)",
+      "profile": "LC",
+      "codec_type": "audio",
+      "codec_tag_string": "mp4a",
+      "codec_tag": "0x6134706d",
+      "mime_codec_string": "mp4a.40.2",
+      "sample_fmt": "fltp",
+      "sample_rate": "44100",
+      "channels": 2,
+      "channel_layout": "stereo",
+      "bits_per_sample": 0,
+      "initial_padding": 0,
+      "id": "0x2",
+      "r_frame_rate": "0/0",
+      "avg_frame_rate": "0/0",
+      "time_base": "1/44100",
+      "start_pts": 0,
+      "start_time": "0.000000",
+      "duration_ts": 1543147,
+      "duration": "34.991995",
+      "bit_rate": "2090",
+      "nb_frames": "1508",
+      "extradata_size": 5,
+      "disposition": {
+        "default": 1,
+        "dub": 0,
+        "original": 0,
+        "comment": 0,
+        "lyrics": 0,
+        "karaoke": 0,
+        "forced": 0,
+        "hearing_impaired": 0,
+        "visual_impaired": 0,
+        "clean_effects": 0,
+        "attached_pic": 0,
+        "timed_thumbnails": 0,
+        "non_diegetic": 0,
+        "captions": 0,
+        "descriptions": 0,
+        "metadata": 0,
+        "dependent": 0,
+        "still_image": 0,
+        "multilayer": 0
+      },
+      "tags": {
+        "language": "und",
+        "handler_name": "SoundHandler"
+      }
+    }
+  ],
+  "format": {
+    "filename": "/private/tmp/tk11343-release-dztfki3_/verification-media/centered/out.mp4",
+    "nb_streams": 2,
+    "nb_programs": 0,
+    "nb_stream_groups": 0,
+    "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
+    "format_long_name": "QuickTime / MOV",
+    "start_time": "0.000000",
+    "duration": "35.000000",
+    "size": "5422183",
+    "bit_rate": "1239356",
+    "probe_score": 100,
+    "tags": {
+      "major_brand": "isom",
+      "minor_version": "512",
+      "compatible_brands": "isomiso2avc1mp41",
+      "encoder": "Lavf62.12.102"
+    }
+  }
+}
\ No newline at end of file
diff --git a/verification/tk11343/centered-thumb.jpg b/verification/tk11343/centered-thumb.jpg
new file mode 100644
index 0000000..1c22ba9
Binary files /dev/null and b/verification/tk11343/centered-thumb.jpg differ
diff --git a/verification/tk11343/e2e-proof.json b/verification/tk11343/e2e-proof.json
new file mode 100644
index 0000000..a7aee48
--- /dev/null
+++ b/verification/tk11343/e2e-proof.json
@@ -0,0 +1,67 @@
+{
+  "ticket": "TK-11343",
+  "timestamp": "2026-09-11T15:16:23.120083+00:00",
+  "intent": "Integrate deterministic daily Short layouts and factual intro/outro variation",
+  "risk_tier": "R2 media and user-approved active source integration",
+  "approval": "Steve: go; revised seven-path integration and local narrated verification",
+  "baseline": "ba6d50f6cbbd79622c472679007737efdc5f9224 including SVG-image regression fix",
+  "commands": [
+    "node --test scripts/short/formats.test.js",
+    "node scripts/short/verify-variation.cjs <isolated evidence>",
+    "build-script.js on six real story records, say Samantha 190 wpm, ffmpeg MP3 conversion, render-short.js",
+    "ffprobe all outputs; ffmpeg full decode all four videos; decoded PCM correlation; patch check and apply excluding prior E2E evidence"
+  ],
+  "checks": [
+    {
+      "check": "5 tests including date/attribution, determinism, oversize duration and long-caption fit",
+      "verdict": "PASS"
+    },
+    {
+      "check": "three distinct layouts with complete synthetic media and inspected thumbnails",
+      "verdict": "PASS"
+    },
+    {
+      "check": "six-story narrated render including stock images, logos, and SVG fallbacks",
+      "verdict": "PASS",
+      "details": {
+        "duration": "43.666016",
+        "narration_pcm_correlation": 0.9999907707958047,
+        "audio_length_difference_seconds": 0.016875,
+        "format_checks": "PASS",
+        "four_full_decodes": "PASS",
+        "voice": "macOS Samantha, free local synthesis of exact new narration",
+        "limitation": "word-level caption timing remains the existing proportional timing; paid production voice and channel impact not tested"
+      }
+    },
+    {
+      "check": "Six code files exactly match independently tested snapshot; documentation updated with integration results",
+      "verdict": "PASS"
+    },
+    {
+      "check": "TK-11340 proof retained",
+      "verdict": "PASS"
+    }
+  ],
+  "source_sha256": {
+    "scripts/short/render-short.js": "4d35fd642b156949b5650ba7286b27960cfa9adc5385a1ed3da07cba9c0323a0",
+    "scripts/short/build-script.js": "46b3b14496d238fdbf354b3cc2e97931383e6abcea809ef8dbbe506977955b06",
+    "scripts/short/fit-caption.js": "7445305bb4a248534ba25a9a220e55a85f766fe6b6877e51e5db690e3c98df73",
+    "scripts/short/formats.test.js": "3dd13e84d462f0090d883036473c05236dedbc1cb4366653fe53174f74f63226",
+    "scripts/short/pick-stories.js": "27cdb5044230a7820e510fff02e0f01e68f1c62ab8e81648e67d215f2b148ec3",
+    "scripts/short/formats.js": "b314ac37cdfcad83fddfa32d336bd42fc8fdc025623a26640deabb555eb3f2ea"
+  },
+  "negative_checks": [
+    "Invalid 61-second script rejected",
+    "Bad SVG backgrounds rejected and replaced by gradients",
+    "Sandbox speech initially empty; retried outside sandbox and verified nonempty complete narration"
+  ],
+  "artifacts": "/Users/macstudio3/Projects/allnewsdaily/verification/tk11343",
+  "rollback": "Reverse only the integration commit; preserve later changes and runtime media",
+  "limitations": [
+    "Production ElevenLabs voice and channel reach were not exercised; no improvement in reach is claimed.",
+    "Card durations retain existing proportional estimates, not word-level synchronization.",
+    "Narrated preview is 43.67 seconds; 45\u201355-second editorial target remains advisory."
+  ],
+  "verdict": "PASS_AUTHORIZED_INTEGRATION",
+  "documentation_sha256": "073de923dedb649dcf1a1817c6e2dbcde257547fa093c5dbab22dee7cb95ecf0"
+}
diff --git a/verification/tk11343/layout-results.json b/verification/tk11343/layout-results.json
new file mode 100644
index 0000000..0619d71
--- /dev/null
+++ b/verification/tk11343/layout-results.json
@@ -0,0 +1,23 @@
+[
+  {
+    "format": "centered",
+    "duration": 35,
+    "dir": "/private/tmp/tk11343-release-dztfki3_/verification-media/centered",
+    "scratch": "/private/tmp/tk11343-release-dztfki3_/data/short/.render-VrPsS8",
+    "verdict": "PASS"
+  },
+  {
+    "format": "sidebar",
+    "duration": 14.5,
+    "dir": "/private/tmp/tk11343-release-dztfki3_/verification-media/sidebar",
+    "scratch": "/private/tmp/tk11343-release-dztfki3_/data/short/.render-rhNtXw",
+    "verdict": "PASS"
+  },
+  {
+    "format": "bulletin",
+    "duration": 13.7,
+    "dir": "/private/tmp/tk11343-release-dztfki3_/verification-media/bulletin",
+    "scratch": "/private/tmp/tk11343-release-dztfki3_/data/short/.render-hgQVjV",
+    "verdict": "PASS"
+  }
+]
\ No newline at end of file
diff --git a/verification/tk11343/narrated-narration.txt b/verification/tk11343/narrated-narration.txt
new file mode 100644
index 0000000..d5a9179
--- /dev/null
+++ b/verification/tk11343/narrated-narration.txt
@@ -0,0 +1,8 @@
+All News Daily. Here is the September 11 news briefing.
+Diesel fuel prices rise above $6 a gallon amid concerns over Middle East conflict affecting oil supplies. — via NYT.
+America observes the 25th anniversary of the 9/11 attacks with reflections and remembrances. — via CBS News.
+BRICS nations meet in New Delhi despite disagreements and external challenges. — via NYT World.
+Inflation remained in August, possibly leading to a Fed interest rate increase. — via CNBC.
+Report: august CPI shows inflation held at 3.4%, slightly hotter than expected. — via CBS News.
+Report: from hunger crisis to ‘obesity’. — via Al Jazeera.
+Those are the headlines. Find the source reporting at all news daily dot com.
\ No newline at end of file
diff --git a/verification/tk11343/narrated-out.mp4 b/verification/tk11343/narrated-out.mp4
new file mode 100644
index 0000000..1c6d218
Binary files /dev/null and b/verification/tk11343/narrated-out.mp4 differ
diff --git a/verification/tk11343/narrated-script.json b/verification/tk11343/narrated-script.json
new file mode 100644
index 0000000..ddc13df
--- /dev/null
+++ b/verification/tk11343/narrated-script.json
@@ -0,0 +1,58 @@
+{
+  "intro": {
+    "text": "All News Daily. Here is the September 11 news briefing.",
+    "estSec": 3.7
+  },
+  "beats": [
+    {
+      "n": 1,
+      "headline": "Diesel fuel prices rise above $6 a gallon amid concerns over Middle East conflict affecting oil supplies.",
+      "outlet": "NYT",
+      "text": "Diesel fuel prices rise above $6 a gallon amid concerns over Middle East conflict affecting oil supplies. — via NYT.",
+      "estSec": 7.4
+    },
+    {
+      "n": 2,
+      "headline": "America observes the 25th anniversary of the 9/11 attacks with reflections and remembrances.",
+      "outlet": "CBS News",
+      "text": "America observes the 25th anniversary of the 9/11 attacks with reflections and remembrances. — via CBS News.",
+      "estSec": 6.3
+    },
+    {
+      "n": 3,
+      "headline": "BRICS nations meet in New Delhi despite disagreements and external challenges.",
+      "outlet": "NYT World",
+      "text": "BRICS nations meet in New Delhi despite disagreements and external challenges. — via NYT World.",
+      "estSec": 5.6
+    },
+    {
+      "n": 4,
+      "headline": "Inflation remained in August, possibly leading to a Fed interest rate increase.",
+      "outlet": "CNBC",
+      "text": "Inflation remained in August, possibly leading to a Fed interest rate increase. — via CNBC.",
+      "estSec": 5.6
+    },
+    {
+      "n": 5,
+      "headline": "Report: august CPI shows inflation held at 3.4%, slightly hotter than expected",
+      "outlet": "CBS News",
+      "text": "Report: august CPI shows inflation held at 3.4%, slightly hotter than expected. — via CBS News.",
+      "estSec": 5.9
+    },
+    {
+      "n": 6,
+      "headline": "Report: from hunger crisis to ‘obesity’",
+      "outlet": "Al Jazeera",
+      "text": "Report: from hunger crisis to ‘obesity’. — via Al Jazeera.",
+      "estSec": 3.7
+    }
+  ],
+  "outro": {
+    "text": "Those are the headlines. Find the source reporting at all news daily dot com.",
+    "estSec": 5.2
+  },
+  "totalSec": 43.4,
+  "narration": "All News Daily. Here is the September 11 news briefing.\nDiesel fuel prices rise above $6 a gallon amid concerns over Middle East conflict affecting oil supplies. — via NYT.\nAmerica observes the 25th anniversary of the 9/11 attacks with reflections and remembrances. — via CBS News.\nBRICS nations meet in New Delhi despite disagreements and external challenges. — via NYT World.\nInflation remained in August, possibly leading to a Fed interest rate increase. — via CNBC.\nReport: august CPI shows inflation held at 3.4%, slightly hotter than expected. — via CBS News.\nReport: from hunger crisis to ‘obesity’. — via Al Jazeera.\nThose are the headlines. Find the source reporting at all news daily dot com.",
+  "format": "sidebar",
+  "edition": "2026-09-11"
+}
\ No newline at end of file
diff --git a/verification/tk11343/narrated-stories.json b/verification/tk11343/narrated-stories.json
new file mode 100644
index 0000000..2cd0a98
--- /dev/null
+++ b/verification/tk11343/narrated-stories.json
@@ -0,0 +1,48 @@
+{
+  "date": "2026-09-11",
+  "generatedAt": "2026-09-11T13:00:02.358Z",
+  "stories": [
+    {
+      "n": 1,
+      "headline": "Diesel fuel prices rise above $6 a gallon amid concerns over Middle East conflict affecting oil supplies.",
+      "outlet": "NYT",
+      "link": "https://www.nytimes.com/2026/09/11/business/diesel-fuel-prices.html",
+      "tag": "Top"
+    },
+    {
+      "n": 2,
+      "headline": "America observes the 25th anniversary of the 9/11 attacks with reflections and remembrances.",
+      "outlet": "CBS News",
+      "link": "https://www.cbsnews.com/live-updates/september-11-terrorist-attacks-25-years-memorial-2026-09-11/",
+      "tag": "U.S. / TOP"
+    },
+    {
+      "n": 3,
+      "headline": "BRICS nations meet in New Delhi despite disagreements and external challenges.",
+      "outlet": "NYT World",
+      "link": "https://www.nytimes.com/2026/09/11/world/asia/brics-meeting-summit-delhi.html",
+      "tag": "WORLD"
+    },
+    {
+      "n": 4,
+      "headline": "Inflation remained in August, possibly leading to a Fed interest rate increase.",
+      "outlet": "CNBC",
+      "link": "https://www.cnbc.com/2026/09/11/cpi-inflation-report-august-2026.html",
+      "tag": "MONEY / TECH"
+    },
+    {
+      "n": 5,
+      "headline": "Report: august CPI shows inflation held at 3.4%, slightly hotter than expected",
+      "outlet": "CBS News",
+      "link": "https://www.cbsnews.com/news/august-cpi-report-inflation-fed-rates/",
+      "tag": "U.S. / TOP"
+    },
+    {
+      "n": 6,
+      "headline": "Report: from hunger crisis to ‘obesity’",
+      "outlet": "Al Jazeera",
+      "link": "https://www.aljazeera.com/video/newsfeed/2026/9/11/from-hunger-crisis-to-obesity-how-gazas-un-data-is-being-twisted?traffic_source=rss",
+      "tag": "WORLD"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/verification/tk11343/narrated-thumb.jpg b/verification/tk11343/narrated-thumb.jpg
new file mode 100644
index 0000000..7e8482c
Binary files /dev/null and b/verification/tk11343/narrated-thumb.jpg differ
diff --git a/verification/tk11343/negative-render.txt b/verification/tk11343/negative-render.txt
new file mode 100644
index 0000000..2ce07c7
--- /dev/null
+++ b/verification/tk11343/negative-render.txt
@@ -0,0 +1,4 @@
+[render-short] NOTE: ffmpeg has no drawtext filter — compositing card text with ImageMagick.
+
+[render-short] FATAL: Script duration must match segments and remain within 58 seconds
+
diff --git a/verification/tk11343/sidebar-ffprobe.json b/verification/tk11343/sidebar-ffprobe.json
new file mode 100644
index 0000000..52a5f05
--- /dev/null
+++ b/verification/tk11343/sidebar-ffprobe.json
@@ -0,0 +1,134 @@
+{
+  "streams": [
+    {
+      "index": 0,
+      "codec_name": "h264",
+      "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+      "profile": "High",
+      "codec_type": "video",
+      "codec_tag_string": "avc1",
+      "codec_tag": "0x31637661",
+      "mime_codec_string": "avc1.640028",
+      "width": 1080,
+      "height": 1920,
+      "coded_width": 1080,
+      "coded_height": 1920,
+      "has_b_frames": 2,
+      "pix_fmt": "yuv420p",
+      "level": 40,
+      "chroma_location": "left",
+      "field_order": "progressive",
+      "is_avc": "true",
+      "nal_length_size": "4",
+      "id": "0x1",
+      "r_frame_rate": "30/1",
+      "avg_frame_rate": "30/1",
+      "time_base": "1/15360",
+      "start_pts": 0,
+      "start_time": "0.000000",
+      "duration_ts": 222720,
+      "duration": "14.500000",
+      "bit_rate": "1236825",
+      "bits_per_raw_sample": "8",
+      "nb_frames": "435",
+      "extradata_size": 46,
+      "disposition": {
+        "default": 1,
+        "dub": 0,
+        "original": 0,
+        "comment": 0,
+        "lyrics": 0,
+        "karaoke": 0,
+        "forced": 0,
+        "hearing_impaired": 0,
+        "visual_impaired": 0,
+        "clean_effects": 0,
+        "attached_pic": 0,
+        "timed_thumbnails": 0,
+        "non_diegetic": 0,
+        "captions": 0,
+        "descriptions": 0,
+        "metadata": 0,
+        "dependent": 0,
+        "still_image": 0,
+        "multilayer": 0
+      },
+      "tags": {
+        "language": "und",
+        "handler_name": "VideoHandler",
+        "encoder": "Lavc62.28.102 libx264"
+      }
+    },
+    {
+      "index": 1,
+      "codec_name": "aac",
+      "codec_long_name": "AAC (Advanced Audio Coding)",
+      "profile": "LC",
+      "codec_type": "audio",
+      "codec_tag_string": "mp4a",
+      "codec_tag": "0x6134706d",
+      "mime_codec_string": "mp4a.40.2",
+      "sample_fmt": "fltp",
+      "sample_rate": "44100",
+      "channels": 2,
+      "channel_layout": "stereo",
+      "bits_per_sample": 0,
+      "initial_padding": 0,
+      "id": "0x2",
+      "r_frame_rate": "0/0",
+      "avg_frame_rate": "0/0",
+      "time_base": "1/44100",
+      "start_pts": 0,
+      "start_time": "0.000000",
+      "duration_ts": 638965,
+      "duration": "14.489002",
+      "bit_rate": "2095",
+      "nb_frames": "625",
+      "extradata_size": 5,
+      "disposition": {
+        "default": 1,
+        "dub": 0,
+        "original": 0,
+        "comment": 0,
+        "lyrics": 0,
+        "karaoke": 0,
+        "forced": 0,
+        "hearing_impaired": 0,
+        "visual_impaired": 0,
+        "clean_effects": 0,
+        "attached_pic": 0,
+        "timed_thumbnails": 0,
+        "non_diegetic": 0,
+        "captions": 0,
+        "descriptions": 0,
+        "metadata": 0,
+        "dependent": 0,
+        "still_image": 0,
+        "multilayer": 0
+      },
+      "tags": {
+        "language": "und",
+        "handler_name": "SoundHandler"
+      }
+    }
+  ],
+  "format": {
+    "filename": "/private/tmp/tk11343-release-dztfki3_/verification-media/sidebar/out.mp4",
+    "nb_streams": 2,
+    "nb_programs": 0,
+    "nb_stream_groups": 0,
+    "format_name": "mov,mp4,m4a,3gp,3g2,mj2",
+    "format_long_name": "QuickTime / MOV",
+    "start_time": "0.000000",
+    "duration": "14.500000",
+    "size": "2262676",
+    "bit_rate": "1248372",
+    "probe_score": 100,
+    "tags": {
+      "major_brand": "isom",
+      "minor_version": "512",
+      "compatible_brands": "isomiso2avc1mp41",
+      "encoder": "Lavf62.12.102"
+    }
+  }
+}
\ No newline at end of file
diff --git a/verification/tk11343/sidebar-thumb.jpg b/verification/tk11343/sidebar-thumb.jpg
new file mode 100644
index 0000000..dbd1278
Binary files /dev/null and b/verification/tk11343/sidebar-thumb.jpg differ
diff --git a/verification/tk11343/tk11343-narrated-proof.json b/verification/tk11343/tk11343-narrated-proof.json
new file mode 100644
index 0000000..de5344b
--- /dev/null
+++ b/verification/tk11343/tk11343-narrated-proof.json
@@ -0,0 +1,9 @@
+{
+  "duration": "43.666016",
+  "narration_pcm_correlation": 0.9999907707958047,
+  "audio_length_difference_seconds": 0.016875,
+  "format_checks": "PASS",
+  "four_full_decodes": "PASS",
+  "voice": "macOS Samantha, free local synthesis of exact new narration",
+  "limitation": "word-level caption timing remains the existing proportional timing; paid production voice and channel impact not tested"
+}
diff --git a/verification/tk11343/tk11343-narrated-render.txt b/verification/tk11343/tk11343-narrated-render.txt
new file mode 100644
index 0000000..5f72d34
--- /dev/null
+++ b/verification/tk11343/tk11343-narrated-render.txt
@@ -0,0 +1,30 @@
+[render-short] NOTE: ffmpeg has no drawtext filter — compositing card text with ImageMagick.
+[render-short] format=sidebar retained scratch=/private/tmp/tk11343-release-dztfki3_/narrated/data/short/.render-PMXk6E
+[render-short] VO found: vo.mp3 = 43.67s — cards will sync to real audio.
+[render-short] WARN: 2.jpg is not a real raster image (magic-byte check failed — likely an SVG/HTML mis-saved as .jpg) — using gradient background instead.
+[render-short] WARN: 3.jpg is not a real raster image (magic-byte check failed — likely an SVG/HTML mis-saved as .jpg) — using gradient background instead.
+[render-short] 8 cards, total 43.67s (target 43.67s).
+[render-short] rendering cards…
+  card 1/8 (intro) 3.72s
+  card 2/8 (beat) 7.45s
+  card 3/8 (beat) 6.34s
+  card 4/8 (beat) 5.63s
+  card 5/8 (beat) 5.63s
+  card 6/8 (beat) 5.94s
+  card 7/8 (beat) 3.72s
+  card 8/8 (outro) 5.23s
+[render-short] muxing audio → out.mp4…
+
+[render-short] ==== out.mp4 ffprobe ====
+codec_name=h264
+width=1080
+height=1920
+pix_fmt=yuv420p
+avg_frame_rate=670720/22357
+duration=43.666016
+size=9516158
+[render-short] ==========================
+
+[render-short] OK — out.mp4: 1080x1920, 43.67s (< 60s), VO AAC.
+[render-short] out:   /private/tmp/tk11343-release-dztfki3_/narrated/data/short/out.mp4
+[render-short] thumb: /private/tmp/tk11343-release-dztfki3_/narrated/data/short/thumb.jpg
diff --git a/verification/tk11343/tk11343-render.txt b/verification/tk11343/tk11343-render.txt
new file mode 100644
index 0000000..0d16e7d
--- /dev/null
+++ b/verification/tk11343/tk11343-render.txt
@@ -0,0 +1,12 @@
+[build-script] WARN: narration under 45s target.
+[render-short] NOTE: ffmpeg has no drawtext filter — compositing card text with ImageMagick.
+[render-short] WARN: no vo.mp3 — rendering SILENT. Using script timing (~35.0s). (TTS is skipped to avoid spend; STAGE 5 orchestrator supplies real VO.)
+{"format":"centered","duration":35,"dir":"/private/tmp/tk11343-release-dztfki3_/verification-media/centered","scratch":"/private/tmp/tk11343-release-dztfki3_/data/short/.render-VrPsS8","verdict":"PASS"}
+[build-script] WARN: narration under 45s target.
+[render-short] NOTE: ffmpeg has no drawtext filter — compositing card text with ImageMagick.
+[render-short] WARN: no vo.mp3 — rendering SILENT. Using script timing (~14.5s). (TTS is skipped to avoid spend; STAGE 5 orchestrator supplies real VO.)
+{"format":"sidebar","duration":14.5,"dir":"/private/tmp/tk11343-release-dztfki3_/verification-media/sidebar","scratch":"/private/tmp/tk11343-release-dztfki3_/data/short/.render-rhNtXw","verdict":"PASS"}
+[build-script] WARN: narration under 45s target.
+[render-short] NOTE: ffmpeg has no drawtext filter — compositing card text with ImageMagick.
+[render-short] WARN: no vo.mp3 — rendering SILENT. Using script timing (~13.7s). (TTS is skipped to avoid spend; STAGE 5 orchestrator supplies real VO.)
+{"format":"bulletin","duration":13.7,"dir":"/private/tmp/tk11343-release-dztfki3_/verification-media/bulletin","scratch":"/private/tmp/tk11343-release-dztfki3_/data/short/.render-hgQVjV","verdict":"PASS"}
diff --git a/verification/tk11343/tk11343-tests.txt b/verification/tk11343/tk11343-tests.txt
new file mode 100644
index 0000000..9d2cb2c
--- /dev/null
+++ b/verification/tk11343/tk11343-tests.txt
@@ -0,0 +1,13 @@
+✔ same edition is identical and consecutive editions cover three formats (14.330125ms)
+✔ dates and missing attribution fail closed (0.195583ms)
+✔ overlong facts are rejected without rewriting them (0.315542ms)
+✔ renderer input rejects tampered duration, format, beat order and zero timing (0.866541ms)
+✔ real ImageMagick fitting preserves short size and shrinks long accepted headlines (21079.734167ms)
+ℹ tests 5
+ℹ suites 0
+ℹ pass 5
+ℹ fail 0
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 21151.836834

← ba6d50f Fix daily-short crash: reject SVG stock images instead of cr  ·  back to Allnewsdaily  ·  Record cutover approval and reverify missing proxy credentia f1a4b4e →