1111 */
1212import { spliceProblems } from '../../website/src/audiobook/artifacts.ts' ;
1313import { AUDIO_CONFIG } from '../../website/src/audiobook/config.ts' ;
14- import { missingCriticalTerms , normalizeForWer , wordErrorRate } from '../../website/src/audiobook/wer.ts' ;
15- import { medianPitchHz , voiceProblems } from '../../website/src/audiobook/voice.ts' ;
16- import { asInt16 , log , measureChapter , measureLevels , pcmBytesForMs , reportFile , statusLine , table , transcribe , warn , writeJson } from './report.mjs' ;
14+ import { missingCriticalTerms , wordErrorRate } from '../../website/src/audiobook/wer.ts' ;
15+ import { measureChapter , measureLevels , pcmBytesForMs , reportFile , statusLine , table , transcribe , writeJson } from './report.mjs' ;
16+ import { asInt16 , log } from './primitives.mjs' ;
17+ import { attachVoiceProblems , turnPitches , unitRequiredTerms } from './verifyUnits.mjs' ;
1718
1819/**
1920 * Integrated loudness is a chapter-file contract, not a per-segment one: natural
@@ -40,12 +41,6 @@ async function verifySegment(segment, pcm, requiredTerms = []) {
4041 return { segmentId : segment . id , kind : segment . kind , wer, missing, ...levels , transcript, problems : segmentProblems ( wer , missing , levels ) } ;
4142}
4243
43- /** Declared terms this unit owns: those that actually occur in its text (they live in one chunk). */
44- export const unitRequiredTerms = ( text , requiredTerms ) => {
45- const haystack = ` ${ normalizeForWer ( text ) } ` ;
46- return requiredTerms . filter ( ( term ) => haystack . includes ( ` ${ normalizeForWer ( term ) } ` ) ) ;
47- } ;
48-
4944/**
5045 * Loudness is a WINDOW, not a target: the master picks the loudest gain the true peak
5146 * allows, so a chapter may come out quieter than the book, never louder. The floor
@@ -105,36 +100,6 @@ function chapterReport(script, stream, segments) {
105100 } ;
106101}
107102
108- /**
109- * Per-turn pitch, tagged with the render unit that owns the turn.
110- * `unitId` is what the build's re-roll loop discards, so a collapsed take is retried.
111- */
112- export function turnPitches ( stream ) {
113- const unitOfPart = new Map ( ) ;
114- stream . units . forEach ( ( unit ) => unit . partIndexes . forEach ( ( index ) => unitOfPart . set ( index , unit . id ) ) ) ;
115- return stream . parts . map ( ( part , index ) => {
116- const unitId = unitOfPart . get ( index ) ;
117- // A part with no owning unit would verify under `undefined`: warn loudly instead of passing silently.
118- if ( unitId === undefined ) warn ( 'verify' , `part ${ index } (${ part . segment . id } ) has no owning unit` ) ;
119- return {
120- unitId,
121- segmentId : part . segment . id ,
122- speaker : part . segment . speaker ,
123- pitchHz : medianPitchHz ( asInt16 ( part . pcm ) , AUDIO_CONFIG . sourceSampleRate , AUDIO_CONFIG . voice ) ,
124- } ;
125- } ) ;
126- }
127-
128- /** Appends each voice-identity failure to the render unit it belongs to. */
129- export function attachVoiceProblems ( segments , turns ) {
130- voiceProblems ( turns , AUDIO_CONFIG . voice ) . forEach ( ( { unitId, problem } ) => {
131- const segment = segments . find ( ( segment ) => segment . segmentId === unitId ) ;
132- // A problem with no owning segment would vanish: warn loudly instead of dropping it.
133- if ( ! segment ) warn ( 'verify' , `voice problem for unit ${ unitId } has no matching segment: ${ problem } ` ) ;
134- else segment . problems . push ( problem ) ;
135- } ) ;
136- }
137-
138103export async function verifyChapter ( script , stream ) {
139104 if ( ! stream . units ) throw new Error ( `${ script . chapterId } : stream carries no verify units` ) ;
140105 const segments = await verifyUnits ( stream , script . requiredTerms ?? [ ] ) ;
0 commit comments