Skip to content
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
1 change: 1 addition & 0 deletions docs/REFACTOR_ISSUE_INVENTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions docs/review-findings/2026-05-13-refactor-s2-project-params.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 4 additions & 1 deletion scripts/config/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 19 additions & 3 deletions scripts/diarize/diarize-audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ 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 {
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);
Expand All @@ -26,6 +38,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']);
Expand All @@ -37,15 +50,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,
};
}
Expand Down
23 changes: 18 additions & 5 deletions scripts/edit-transcript.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ 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 {
const raw = fs.readFileSync(projectPath, 'utf-8');
return JSON.parse(raw)?.params ?? {};
} catch {
return {};
}
}

// ─── Text helpers ─────────────────────────────────────────────────────────────

// Strip leading/trailing punctuation, keep apostrophes for contractions
Expand Down Expand Up @@ -1548,7 +1560,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;
Expand Down Expand Up @@ -1643,8 +1655,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);
}
}
Expand Down Expand Up @@ -1817,6 +1827,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');
Expand Down Expand Up @@ -1967,8 +1980,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 => ({
Expand Down
18 changes: 17 additions & 1 deletion scripts/transcribe/transcribe-audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ 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 {
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);
Expand All @@ -25,6 +37,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']);
Expand All @@ -35,7 +48,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() {
Expand Down
Loading
Loading