@@ -12,6 +12,7 @@ import { runWithAiProviders } from "@/lib/ai/run";
1212import { isRateLimited , RATE_LIMIT_IDS } from "@/lib/rate-limit" ;
1313import * as EffectRuntime from "@/lib/server" ;
1414import { runPromise } from "@/lib/server" ;
15+ import { isValidTranscriptTranslation } from "@/lib/transcript-vtt" ;
1516import { decodeStorageVideo } from "@/lib/video-storage" ;
1617import {
1718 type LanguageCode ,
@@ -73,6 +74,19 @@ export async function translateTranscript(
7374
7475 const { video } = query [ 0 ] ;
7576
77+ const originalVtt = await Effect . gen ( function * ( ) {
78+ const [ bucket ] = yield * Storage . getAccessForVideo (
79+ decodeStorageVideo ( video ) ,
80+ ) ;
81+ return yield * bucket . getObject (
82+ `${ video . ownerId } /${ videoId } /transcription.vtt` ,
83+ ) ;
84+ } ) . pipe ( runPromise ) ;
85+
86+ if ( Option . isNone ( originalVtt ) ) {
87+ return { success : false , message : "Original transcript not found" } ;
88+ }
89+
7690 const translatedKey = `${ video . ownerId } /${ videoId } /transcription.${ targetLanguage } .vtt` ;
7791
7892 try {
@@ -83,7 +97,10 @@ export async function translateTranscript(
8397 return yield * bucket . getObject ( translatedKey ) ;
8498 } ) . pipe ( runPromise ) ;
8599
86- if ( Option . isSome ( existingTranslation ) ) {
100+ if (
101+ Option . isSome ( existingTranslation ) &&
102+ isValidTranscriptTranslation ( originalVtt . value , existingTranslation . value )
103+ ) {
87104 return {
88105 success : true ,
89106 translatedVtt : existingTranslation . value ,
@@ -94,19 +111,6 @@ export async function translateTranscript(
94111 console . debug ( "[translateTranscript] No cached translation found:" , e ) ;
95112 }
96113
97- const originalVtt = await Effect . gen ( function * ( ) {
98- const [ bucket ] = yield * Storage . getAccessForVideo (
99- decodeStorageVideo ( video ) ,
100- ) ;
101- return yield * bucket . getObject (
102- `${ video . ownerId } /${ videoId } /transcription.vtt` ,
103- ) ;
104- } ) . pipe ( runPromise ) ;
105-
106- if ( Option . isNone ( originalVtt ) ) {
107- return { success : false , message : "Original transcript not found" } ;
108- }
109-
110114 const translatedVtt = await translateVttContent (
111115 originalVtt . value ,
112116 targetLanguage ,
@@ -167,10 +171,12 @@ ${vttContent}`;
167171 ...( selection . supportsTemperature ? { temperature : 0.3 } : { } ) ,
168172 } ) ;
169173
170- // Validate inside the provider loop so a fulfilled response that
171- // dropped the WEBVTT header falls through to the next provider.
172- if ( ! response . text . includes ( "WEBVTT" ) ) {
173- throw new Error ( "translation response did not contain WEBVTT" ) ;
174+ // Validate inside the provider loop so malformed translations try the
175+ // next provider without poisoning the cached captions.
176+ if ( ! isValidTranscriptTranslation ( vttContent , response . text ) ) {
177+ throw new Error (
178+ "translation changed cue structure or speaker annotations" ,
179+ ) ;
174180 }
175181
176182 return response . text . trim ( ) ;
0 commit comments