From 127ff4329059c3738e18bc43fc07c2f2c3e90ceb Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Wed, 13 May 2026 17:37:00 +0800 Subject: [PATCH 1/8] feat(config): type PipelineParams with deterministic pipeline fields Replace the open-ended index signature with specific optional fields: timestamp_offset, diarization_seed, num_speakers, sync_window_seconds. These are the three params the refactor plan calls out as needing to live in project.json rather than transient CLI flags. Downstream pipeline scripts can now read readProject().params and get a typed, predictable shape. AC: PipelineParams shape matches docs/PRODUCTION_REFACTOR_PLAN.md spec. --- scripts/config/project.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/config/project.ts b/scripts/config/project.ts index 33ab23d..65dbd13 100644 --- a/scripts/config/project.ts +++ b/scripts/config/project.ts @@ -18,7 +18,10 @@ export interface ToolVersions { } export interface PipelineParams { - [key: string]: unknown; + timestamp_offset?: number; + diarization_seed?: number; + num_speakers?: number; + sync_window_seconds?: number | null; } export interface ArtifactRefs { From 3ed06e4eada9a71eca465aeeb4eda8602568fb27 Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Wed, 13 May 2026 17:37:11 +0800 Subject: [PATCH 2/8] test(config): update params roundtrip tests for typed PipelineParams Update SAMPLE fixture to use the new typed fields (timestamp_offset, diarization_seed, num_speakers, sync_window_seconds) replacing the old open-ended seed key that no longer satisfies the typed interface. Add two new integration tests: - roundtrip preserves all deterministic pipeline params: verifies each field survives a write/read cycle with correct value and type - roundtrip with partial params: verifies optional fields are preserved as undefined when omitted (partial project files must stay valid) AC: read/write params roundtrip verified for all typed fields. --- tests/integration/project.test.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/integration/project.test.ts b/tests/integration/project.test.ts index 966f530..2ed7073 100644 --- a/tests/integration/project.test.ts +++ b/tests/integration/project.test.ts @@ -9,7 +9,12 @@ const SAMPLE: ProjectFile = { episode: { id: 'ep-01', title: 'Pilot', number: 1 }, brandId: 'ragtech', tools: { node: '20.0.0', ffmpeg: '6.0' }, - params: { seed: 42 }, + params: { + timestamp_offset: 0, + diarization_seed: 42, + num_speakers: 3, + sync_window_seconds: null, + }, artifacts: {}, }; @@ -69,4 +74,22 @@ describe('readProject', () => { expect(result.params).toEqual(SAMPLE.params); expect(result.artifacts).toEqual(SAMPLE.artifacts); }); + + it('roundtrip preserves all deterministic pipeline params', () => { + writeProject(SAMPLE, tmpDir); + const result = readProject(tmpDir); + expect(result.params.timestamp_offset).toBe(0); + expect(result.params.diarization_seed).toBe(42); + expect(result.params.num_speakers).toBe(3); + expect(result.params.sync_window_seconds).toBeNull(); + }); + + it('roundtrip with partial params preserves only set fields', () => { + const partial: ProjectFile = { ...SAMPLE, params: { num_speakers: 2 } }; + writeProject(partial, tmpDir); + const result = readProject(tmpDir); + expect(result.params.num_speakers).toBe(2); + expect(result.params.timestamp_offset).toBeUndefined(); + expect(result.params.diarization_seed).toBeUndefined(); + }); }); From 7507b4d8875f1b14c5801d3218152838ee4d08a9 Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Wed, 13 May 2026 17:37:51 +0800 Subject: [PATCH 3/8] feat(diarize): read num_speakers from project file with CLI fallback Add readProjectParams() helper that reads .ragtech/project.json and returns the params object, returning {} on any error (file absent or malformed). This keeps the script self-contained without importing TS. Resolution order: --num-speakers CLI flag > project.params.num_speakers. If neither is present, the script exits with a clear error message pointing the user to the wizard or the flag. This makes num_speakers reproducible: once the wizard writes it to the project file, reruns don't need the flag explicitly. AC: rerunning diarize uses persisted num_speakers from project file. --- scripts/diarize/diarize-audio.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/scripts/diarize/diarize-audio.js b/scripts/diarize/diarize-audio.js index d20b0f8..2a8e7e4 100644 --- a/scripts/diarize/diarize-audio.js +++ b/scripts/diarize/diarize-audio.js @@ -17,6 +17,16 @@ function parseArgs() { return result; } +function readProjectParams(cwd) { + const projectPath = path.join(cwd, '.ragtech', 'project.json'); + try { + const raw = fs.readFileSync(projectPath, 'utf-8'); + return JSON.parse(raw)?.params ?? {}; + } catch { + return {}; + } +} + async function autoDetectFile(dir, extensions) { if (!await fs.pathExists(dir)) return null; const files = await fs.readdir(dir); @@ -26,6 +36,7 @@ async function autoDetectFile(dir, extensions) { async function resolveArgs(cwd) { const cli = parseArgs(); + const projectParams = readProjectParams(cwd); const audioPath = cli.audioPath || await autoDetectFile(path.join(cwd, 'public', 'transcribe', 'input'), ['.mp3', '.aac', '.wav', '.m4a']); @@ -37,15 +48,18 @@ async function resolveArgs(cwd) { process.exit(1); } - if (!cli.numSpeakers) { - console.error('❌ --num-speakers is required. Example: npm run diarize -- --num-speakers 2'); + // CLI flag takes precedence; project file is the persisted default. + const numSpeakers = cli.numSpeakers ?? projectParams.num_speakers ?? null; + + if (!numSpeakers) { + console.error('❌ num_speakers not set. Run the wizard or pass --num-speakers 2'); process.exit(1); } return { audioPath, diarizationJsonPath, - numSpeakers: cli.numSpeakers, + numSpeakers, pythonBin: cli.pythonBin, }; } From ce6fec81b7272c88e8b0f77a7f7957dba466d59c Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Wed, 13 May 2026 17:38:20 +0800 Subject: [PATCH 4/8] feat(transcribe): read timestamp_offset from project file with CLI fallback Add readProjectParams() helper (same pattern as diarize-audio.js) that reads .ragtech/project.json, returning {} when the file is absent. Resolution order: --timestamp-offset CLI flag > project.params.timestamp_offset > 0. This removes CLI ownership of the value: once the wizard stores it in the project file, reruns pick it up automatically without needing the flag. AC: rerunning transcribe uses persisted timestamp_offset from project file. --- scripts/transcribe/transcribe-audio.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/transcribe/transcribe-audio.js b/scripts/transcribe/transcribe-audio.js index bb2afc0..d13b108 100644 --- a/scripts/transcribe/transcribe-audio.js +++ b/scripts/transcribe/transcribe-audio.js @@ -16,6 +16,16 @@ function parseArgs() { return result; } +function readProjectParams(cwd) { + const projectPath = path.join(cwd, '.ragtech', 'project.json'); + try { + const raw = fs.readFileSync(projectPath, 'utf-8'); + return JSON.parse(raw)?.params ?? {}; + } catch { + return {}; + } +} + async function autoDetectFile(dir, extensions) { if (!await fs.pathExists(dir)) return null; const files = await fs.readdir(dir); @@ -25,6 +35,7 @@ async function autoDetectFile(dir, extensions) { async function resolveArgs(cwd) { const cli = parseArgs(); + const projectParams = readProjectParams(cwd); const audioPath = cli.audioPath || await autoDetectFile(path.join(cwd, 'public', 'transcribe', 'input'), ['.mp3', '.aac', '.wav', '.m4a']); @@ -35,7 +46,10 @@ async function resolveArgs(cwd) { process.exit(1); } - return { audioPath, outputDir, model: cli.model, timestampOffset: cli.timestampOffset || 0 }; + // CLI flag takes precedence; project file is the persisted default. + const timestampOffset = cli.timestampOffset ?? projectParams.timestamp_offset ?? 0; + + return { audioPath, outputDir, model: cli.model, timestampOffset }; } async function main() { From b365ec6db73976c5c4ecda18d7231c2437bc8916 Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Wed, 13 May 2026 17:40:26 +0800 Subject: [PATCH 5/8] feat(transcript): read timestamp_offset from project file in edit-transcript Add readProjectParams() helper that reads .ragtech/project.json, returning {} when the file is absent or malformed. Resolve timestampOffset using the same precedence as other entrypoints: CLI flag > project.params.timestamp_offset > 0. The resolved value is stored in a local variable so all downstream checks and log statements use the project-sourced value rather than the raw CLI object. Also remove two dead variable assignments (actualEnd, baseStart) and an unused constant (OUTRO_DURATION_SECS) that were pre-existing ESLint warnings blocking the pre-commit hook. AC: rerunning edit-transcript uses persisted timestamp_offset from project file. --- scripts/edit-transcript.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/scripts/edit-transcript.js b/scripts/edit-transcript.js index 025781e..c4de13a 100644 --- a/scripts/edit-transcript.js +++ b/scripts/edit-transcript.js @@ -20,6 +20,16 @@ function parseArgs() { return result; } +function readProjectParams(cwd) { + const projectPath = path.join(cwd, '.ragtech', 'project.json'); + try { + const raw = fs.readFileSync(projectPath, 'utf-8'); + return JSON.parse(raw)?.params ?? {}; + } catch { + return {}; + } +} + // ─── Text helpers ───────────────────────────────────────────────────────────── // Strip leading/trailing punctuation, keep apostrophes for contractions @@ -1548,7 +1558,7 @@ function buildSentencesSrt(segments, meta = {}) { // Must match remotion/components/PodcastIntro.tsx and PodcastOutro.tsx const INTRO_DURATION_SECS = 7; // 420 frames @ 60fps -const OUTRO_DURATION_SECS = 7; // 420 frames @ 60fps +// OUTRO_DURATION_SECS mirrors INTRO_DURATION_SECS but is not yet consumed here. const HOOK_TAIL_PAD_UNBOUNDED = 0.16; const HOOK_TAIL_PAD_BOUNDED = 0.02; const HOOK_BRIDGE_MAX_GAP = 1.0; @@ -1643,8 +1653,6 @@ function buildYouTubeSubtitles(transcript) { // Ensure we account for any bridging/extension time if (nextStart) { - const actualEnd = getHookEffectiveEnd(seg, nextStart); - const baseStart = seg.hookFrom ?? seg.start; currentOffset = Math.max(currentOffset, hookEndTime); } } @@ -1817,6 +1825,9 @@ function buildPrevTokensByTdtw(tokens) { async function main() { const cwd = process.cwd(); const cli = parseArgs(); + const projectParams = readProjectParams(cwd); + // CLI flag takes precedence; project file is the persisted default. + const timestampOffset = cli.timestampOffset ?? projectParams.timestamp_offset ?? 0; const rawPath = cli.rawPath || path.join(cwd, 'public', 'transcribe', 'output', 'raw', 'transcript.raw.json'); const outputPath = cli.outputPath || path.join(cwd, 'public', 'edit', 'transcript.json'); @@ -1967,8 +1978,8 @@ async function main() { } // Apply timestamp offset to all t_dtw values and segment boundaries - if (cli.timestampOffset > 0) { - const off = cli.timestampOffset; + if (timestampOffset > 0) { + const off = timestampOffset; transcript = { ...transcript, segments: transcript.segments.map(seg => ({ From f6fc7b58b714029063a7e6219cd0020ec5eb0c6e Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Wed, 13 May 2026 17:41:32 +0800 Subject: [PATCH 6/8] docs(inventory): mark issue #3 complete; update CLAUDE.md for typed params Inventory: annotate Sprint 1 issue #3 as done on refactor/s2-project-params. CLAUDE.md: update scripts/config/project.ts row to reflect the typed PipelineParams interface added in this sprint (timestamp_offset, diarization_seed, num_speakers, sync_window_seconds). --- CLAUDE.md | 2 +- docs/REFACTOR_ISSUE_INVENTORY.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 83225f3..03c57f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -197,7 +197,7 @@ ty = (0.5 - vp.cy) × 100% | `remotion/types/transcript.ts` | `Segment`, `Token`, `TimeCut`, `Transcript` | Will import from `scripts/types/` (Phase 6) | | `remotion/types/camera.ts` | `CameraProfiles`, `CameraShot`, `CropViewport` | Will import from `scripts/types/` (Phase 6) | | `remotion/types/brand.ts` | Brand design tokens + extended identity/hosts/mascot/audio; `id: string` field required by registry | Move overlays + parameterize (Phase 0.5 Steps 3, 6–7) | -| `scripts/config/project.ts` | `ProjectFile` type, `readProject`/`writeProject`, `ProjectNotFoundError` | Sprint 1 Issue #1 | +| `scripts/config/project.ts` | `ProjectFile` type, `readProject`/`writeProject`, `ProjectNotFoundError`; typed `PipelineParams` (`timestamp_offset`, `diarization_seed`, `num_speakers`, `sync_window_seconds`) | Sprint 1 Issues #1, #3 | | `scripts/edit-transcript.js` | Sentence merging, `deriveCuts`, doc generation | Migrate to .ts (Phase 3) | | `scripts/sync/AudioSyncer.js` | FFT sync, `syncMultiple` | Add FFT tie-breaking (Phase 0) | | `scripts/wizard.js` | Interactive pipeline runner (60KB) | Replace with DAG runner (Phase 2) | diff --git a/docs/REFACTOR_ISSUE_INVENTORY.md b/docs/REFACTOR_ISSUE_INVENTORY.md index fccb1db..04b1e32 100644 --- a/docs/REFACTOR_ISSUE_INVENTORY.md +++ b/docs/REFACTOR_ISSUE_INVENTORY.md @@ -10,6 +10,7 @@ Create the `.ragtech/project.json` foundation and typed helpers for reading/writ Create the artifact storage system that writes outputs into `.ragtech/artifacts/` using SHA-256-derived filenames. Identical content must resolve to identical artifact IDs. ### 3. Persist pipeline parameters in project file +✅ Done — `refactor/s2-project-params` — typed `PipelineParams` interface; `readProjectParams()` in `diarize-audio.js`, `transcribe-audio.js`, `edit-transcript.js` reads from project file with CLI fallback Move runtime parameters such as diarization seed, timestamp offset, and number of speakers out of CLI flags and into the project file so pipeline runs are reproducible. ### 4. Add deterministic metadata to generated artifacts From 40b723056548c61b7db8566b8dfd2cb97941f613 Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Wed, 13 May 2026 17:47:23 +0800 Subject: [PATCH 7/8] test(config): integration tests for project-file param reading in entrypoints Pre-push audit identified that readProjectParams() in diarize-audio.js and transcribe-audio.js was untested. These helpers are private to their entrypoints so they're verified via observable stdout behavior: the scripts log speaker count and timestamp offset before attempting diarization/transcription. Tests cover three cases each: - param read from project file (no CLI flag present) - default behavior when project file is absent - CLI flag takes precedence over project file value --- .../project-params-entrypoints.test.ts | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 tests/integration/project-params-entrypoints.test.ts diff --git a/tests/integration/project-params-entrypoints.test.ts b/tests/integration/project-params-entrypoints.test.ts new file mode 100644 index 0000000..e356161 --- /dev/null +++ b/tests/integration/project-params-entrypoints.test.ts @@ -0,0 +1,124 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { spawnSync } from 'child_process'; + +function mkTmpDir(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), 'deckcreate-params-test-')); +} + +function writeProjectJson(dir: string, params: Record): void { + const ragtech = path.join(dir, '.ragtech'); + fs.mkdirSync(ragtech, { recursive: true }); + fs.writeFileSync( + path.join(ragtech, 'project.json'), + JSON.stringify({ + version: '1.0.0', + episode: { id: 'ep-test' }, + brandId: 'ragtech', + tools: {}, + params, + artifacts: {}, + }), + 'utf-8', + ); +} + +const REPO_ROOT = path.resolve(__dirname, '../..'); + +describe('diarize-audio.js reads num_speakers from project file', () => { + let tmpDir: string; + + beforeEach(() => { tmpDir = mkTmpDir(); }); + afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); + + it('prints locked speaker count from project file when --num-speakers is absent', () => { + writeProjectJson(tmpDir, { num_speakers: 3 }); + + const result = spawnSync( + process.execPath, + [path.join(REPO_ROOT, 'scripts/diarize/diarize-audio.js'), '--audio', '/fake/audio.mp3'], + { cwd: tmpDir, encoding: 'utf-8', timeout: 10_000 }, + ); + + // The script logs speaker count before attempting diarization. + // Absence of the "num_speakers not set" error and presence of the + // locked-count line proves the project file param was consumed. + expect(result.stdout).toContain('Speakers: 3 (locked)'); + expect(result.stderr).not.toContain('num_speakers not set'); + }); + + it('exits with num_speakers error when project file is absent and no CLI flag', () => { + // No project.json written — should fall through to the error. + const result = spawnSync( + process.execPath, + [path.join(REPO_ROOT, 'scripts/diarize/diarize-audio.js'), '--audio', '/fake/audio.mp3'], + { cwd: tmpDir, encoding: 'utf-8', timeout: 10_000 }, + ); + + expect(result.status).toBe(1); + expect(result.stderr).toContain('num_speakers not set'); + }); + + it('CLI --num-speakers overrides project file value', () => { + writeProjectJson(tmpDir, { num_speakers: 3 }); + + const result = spawnSync( + process.execPath, + [ + path.join(REPO_ROOT, 'scripts/diarize/diarize-audio.js'), + '--audio', '/fake/audio.mp3', + '--num-speakers', '2', + ], + { cwd: tmpDir, encoding: 'utf-8', timeout: 10_000 }, + ); + + expect(result.stdout).toContain('Speakers: 2 (locked)'); + }); +}); + +describe('transcribe-audio.js reads timestamp_offset from project file', () => { + let tmpDir: string; + + beforeEach(() => { tmpDir = mkTmpDir(); }); + afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); + + it('prints offset from project file when --timestamp-offset is absent', () => { + writeProjectJson(tmpDir, { timestamp_offset: 0.5 }); + + const result = spawnSync( + process.execPath, + [path.join(REPO_ROOT, 'scripts/transcribe/transcribe-audio.js'), '--audio', '/fake/audio.mp3'], + { cwd: tmpDir, encoding: 'utf-8', timeout: 10_000 }, + ); + + expect(result.stdout).toContain('Offset: -0.5s'); + }); + + it('uses offset of 0 when project file is absent', () => { + // No project.json — timestamp_offset defaults to 0, so the offset line is not printed. + const result = spawnSync( + process.execPath, + [path.join(REPO_ROOT, 'scripts/transcribe/transcribe-audio.js'), '--audio', '/fake/audio.mp3'], + { cwd: tmpDir, encoding: 'utf-8', timeout: 10_000 }, + ); + + expect(result.stdout).not.toContain('Offset:'); + }); + + it('CLI --timestamp-offset overrides project file value', () => { + writeProjectJson(tmpDir, { timestamp_offset: 0.5 }); + + const result = spawnSync( + process.execPath, + [ + path.join(REPO_ROOT, 'scripts/transcribe/transcribe-audio.js'), + '--audio', '/fake/audio.mp3', + '--timestamp-offset', '1.2', + ], + { cwd: tmpDir, encoding: 'utf-8', timeout: 10_000 }, + ); + + expect(result.stdout).toContain('Offset: -1.2s'); + }); +}); From 3f8763e6f128771047d045ffc240d5c0295c321b Mon Sep 17 00:00:00 2001 From: Natasha Ann Date: Wed, 13 May 2026 18:15:10 +0800 Subject: [PATCH 8/8] review(s2): resolve W1/W2 from PR review W1: add Phase 3 migration comment to readProjectParams in all three .js scripts pointing to scripts/config/project.ts as the canonical source. W2: add 3 integration tests for edit-transcript.js covering project-file timestamp_offset, absent-file fallback, and CLI override resolution paths. Co-Authored-By: Claude Sonnet 4.6 --- .../2026-05-13-refactor-s2-project-params.md | 45 +++++++++++++ scripts/diarize/diarize-audio.js | 2 + scripts/edit-transcript.js | 2 + scripts/transcribe/transcribe-audio.js | 2 + .../project-params-entrypoints.test.ts | 67 +++++++++++++++++++ 5 files changed, 118 insertions(+) create mode 100644 docs/review-findings/2026-05-13-refactor-s2-project-params.md diff --git a/docs/review-findings/2026-05-13-refactor-s2-project-params.md b/docs/review-findings/2026-05-13-refactor-s2-project-params.md new file mode 100644 index 0000000..e6f1b72 --- /dev/null +++ b/docs/review-findings/2026-05-13-refactor-s2-project-params.md @@ -0,0 +1,45 @@ +# Review: refactor/s2-project-params +Date: 2026-05-13 +Reviewer: AI (review-pr skill) — session bias: CLEAN +PR: NONE (no open PR; description provided in /review-pr invocation) + +## Verdict +APPROVED WITH SUGGESTIONS + +## Summary +This PR closes Issue #3: it replaces the `[key: string]: unknown` index signature in `PipelineParams` with four typed, optional fields that match the spec in `docs/PRODUCTION_REFACTOR_PLAN.md` exactly, and wires `diarize-audio.js`, `transcribe-audio.js`, and `edit-transcript.js` to read the relevant param from `.ragtech/project.json` before falling back to CLI flags. Dead code (`OUTRO_DURATION_SECS`, `actualEnd`, `baseStart`) is also removed from `edit-transcript.js`. The main risk area is the duplicated `readProjectParams` helper; the only coverage gap is the missing integration test for `edit-transcript.js`. + +## Blockers (must fix before merge) +_None._ + +## Warnings (should address) + +### W1 — `readProjectParams` duplicated across three `.js` scripts +- **Type:** QUALITY +- **File:** `scripts/diarize/diarize-audio.js:20`, `scripts/transcribe/transcribe-audio.js:19`, `scripts/edit-transcript.js:23` +- **Finding:** All three files define an identical `readProjectParams(cwd)` function body that directly reads `.ragtech/project.json` and returns `params ?? {}`. The canonical `readProject()` in `scripts/config/project.ts` cannot be imported from `.js` files (no compiled output, `noEmit: true` tsconfig), so duplication is pragmatically unavoidable until Phase 3. However, if `PROJECT_DIR` (`.ragtech`) or `PROJECT_FILENAME` is ever renamed in `project.ts`, all three scripts would silently stop reading the project file with no compile error. +- **Suggestion:** Add a brief comment in each copy pointing to `project.ts` as the canonical source (`// matches PROJECT_DIR / PROJECT_FILENAME in scripts/config/project.ts`), so Phase 3 migration has a clear signal to consolidate. No code change required before merge. + +### W2 — `edit-transcript.js` entrypoint lacks integration test coverage +- **Type:** COVERAGE +- **File:** `tests/integration/project-params-entrypoints.test.ts` +- **Finding:** `diarize-audio.js` and `transcribe-audio.js` each have 3 integration tests covering all three resolution paths (project-file, absent-file fallback, CLI override). `edit-transcript.js` received the same `timestamp_offset` wiring but has zero integration tests in this PR. The only coverage is the manual verification step in the test plan. The pattern is low-risk because it's identical to the tested scripts, but the entrypoint itself is untested. +- **Suggestion:** Add 2–3 integration tests for `edit-transcript.js` to `project-params-entrypoints.test.ts` mirroring the `transcribe-audio.js` block. Note that spawning `edit-transcript.js` requires a `transcript.raw.json` fixture; if that's too heavy, a lighter approach is to test `readProjectParams` as a shared helper once it's extracted in Phase 3. + +## Suggestions (optional improvements) + +- The `diarization_seed` and `sync_window_seconds` fields are typed in `PipelineParams` but not yet consumed by any script. No action needed — this is intentional (scope of this issue is `num_speakers` + `timestamp_offset`). Phase 2 DAG runner will wire `sync_window_seconds`; diarize seed wiring can follow. +- `transcribe-audio.js` changed the guard from `cli.timestampOffset || 0` to `cli.timestampOffset ?? projectParams.timestamp_offset ?? 0`. This is a subtle but correct improvement: `||` coerces the valid value `0` to the default, while `??` correctly treats `0` as an explicit user choice. No action needed — just noting it as a correctness fix bundled in the PR. + +## Test plan verification + +| Item | Status | Notes | +|------|--------|-------| +| `npm test` passes (15 suites, 272 tests) | PASS | 2 tests skipped (pre-existing); no failures | +| `tsc --noEmit` clean | PASS | No TypeScript errors | +| `[PIPELINE]` transcribe with project `timestamp_offset: 0.5` prints `Offset: -0.5s` | NOT RUN | Manual step; marked done by developer in PR description | +| `[PIPELINE]` diarize with project `num_speakers: 3` prints `Speakers: 3 (locked)` | NOT RUN | Manual step; marked done by developer in PR description | + +## Patterns observed +- W1 above is a variant of a pattern already tracked ("Implementation diverges from documented spec…") but applies specifically to `.js`↔`.ts` boundary duplication pre-Phase 3. Not a new pattern — existing `readProject` vs inline copy is an expected transitional state. +- No new patterns identified. diff --git a/scripts/diarize/diarize-audio.js b/scripts/diarize/diarize-audio.js index 2a8e7e4..724d7f6 100644 --- a/scripts/diarize/diarize-audio.js +++ b/scripts/diarize/diarize-audio.js @@ -17,6 +17,8 @@ function parseArgs() { return result; } +// Inline copy of project-file reading — mirrors PROJECT_DIR/PROJECT_FILENAME in scripts/config/project.ts. +// Consolidate into a shared helper during Phase 3 (.js → .ts migration). function readProjectParams(cwd) { const projectPath = path.join(cwd, '.ragtech', 'project.json'); try { diff --git a/scripts/edit-transcript.js b/scripts/edit-transcript.js index c4de13a..d1c1e01 100644 --- a/scripts/edit-transcript.js +++ b/scripts/edit-transcript.js @@ -20,6 +20,8 @@ function parseArgs() { return result; } +// Inline copy of project-file reading — mirrors PROJECT_DIR/PROJECT_FILENAME in scripts/config/project.ts. +// Consolidate into a shared helper during Phase 3 (.js → .ts migration). function readProjectParams(cwd) { const projectPath = path.join(cwd, '.ragtech', 'project.json'); try { diff --git a/scripts/transcribe/transcribe-audio.js b/scripts/transcribe/transcribe-audio.js index d13b108..c957eec 100644 --- a/scripts/transcribe/transcribe-audio.js +++ b/scripts/transcribe/transcribe-audio.js @@ -16,6 +16,8 @@ function parseArgs() { return result; } +// Inline copy of project-file reading — mirrors PROJECT_DIR/PROJECT_FILENAME in scripts/config/project.ts. +// Consolidate into a shared helper during Phase 3 (.js → .ts migration). function readProjectParams(cwd) { const projectPath = path.join(cwd, '.ragtech', 'project.json'); try { diff --git a/tests/integration/project-params-entrypoints.test.ts b/tests/integration/project-params-entrypoints.test.ts index e356161..fd51a54 100644 --- a/tests/integration/project-params-entrypoints.test.ts +++ b/tests/integration/project-params-entrypoints.test.ts @@ -122,3 +122,70 @@ describe('transcribe-audio.js reads timestamp_offset from project file', () => { expect(result.stdout).toContain('Offset: -1.2s'); }); }); + +describe('edit-transcript.js reads timestamp_offset from project file', () => { + let tmpDir: string; + + const MINIMAL_RAW = { + meta: { fps: 60, duration: 5 }, + segments: [{ + id: 1, start: 1.0, end: 3.0, + speaker: 'SPEAKER_01', text: 'Hello world.', + cut: false, cutReason: null, + tokens: [ + { t_dtw: 1.0, text: 'Hello', cut: false, cutReason: null }, + { t_dtw: 2.0, text: 'world.', cut: false, cutReason: null }, + ], + }], + }; + + function writeRawTranscript(dir: string): void { + const rawDir = path.join(dir, 'public', 'transcribe', 'output', 'raw'); + fs.mkdirSync(rawDir, { recursive: true }); + fs.writeFileSync(path.join(rawDir, 'transcript.raw.json'), JSON.stringify(MINIMAL_RAW), 'utf-8'); + } + + beforeEach(() => { tmpDir = mkTmpDir(); }); + afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); + + it('logs offset from project file when --timestamp-offset is absent', () => { + writeProjectJson(tmpDir, { timestamp_offset: 0.5 }); + writeRawTranscript(tmpDir); + + const result = spawnSync( + process.execPath, + [path.join(REPO_ROOT, 'scripts/edit-transcript.js')], + { cwd: tmpDir, encoding: 'utf-8', timeout: 15_000 }, + ); + + expect(result.stdout).toContain('Applied timestamp offset: -0.5s'); + }); + + it('does not apply offset when project file is absent', () => { + writeRawTranscript(tmpDir); + + const result = spawnSync( + process.execPath, + [path.join(REPO_ROOT, 'scripts/edit-transcript.js')], + { cwd: tmpDir, encoding: 'utf-8', timeout: 15_000 }, + ); + + expect(result.stdout).not.toContain('Applied timestamp offset'); + }); + + it('CLI --timestamp-offset overrides project file value', () => { + writeProjectJson(tmpDir, { timestamp_offset: 0.5 }); + writeRawTranscript(tmpDir); + + const result = spawnSync( + process.execPath, + [ + path.join(REPO_ROOT, 'scripts/edit-transcript.js'), + '--timestamp-offset', '1.2', + ], + { cwd: tmpDir, encoding: 'utf-8', timeout: 15_000 }, + ); + + expect(result.stdout).toContain('Applied timestamp offset: -1.2s'); + }); +});