import type { AudioFrame } from '@livekit/rtc-node';
import type { ReadableStream } from 'node:stream/web';
import type { WordTokenizer } from '../../tokenize/index.js';
import { AudioOutput, type PlaybackFinishedEvent, TextOutput, type TimedString } from '../io.js';
interface TextSyncOptions {
    speed: number;
    hyphenateWord: (word: string) => string[];
    splitWords: (words: string) => [string, number, number][];
    wordTokenizer: WordTokenizer;
    enabled: boolean;
    /**
     * Whether expressive markup may be present in the forwarded text, so pacing should
     * discount it. Evaluated per word, because the session latches expressive on the first
     * turn that injects the markup guide — after this synchronizer is constructed.
     */
    expressiveEnabled: () => boolean;
}
/**
 * Tracks speaking rate data from TTS timing annotations.
 * @internal Exported for testing purposes.
 */
export declare class SpeakingRateData {
    /** Timestamps of the speaking rate. */
    timestamps: number[];
    /** Speed at the timestamp. */
    speakingRate: number[];
    /** Accumulated speaking units up to the timestamp. */
    speakIntegrals: number[];
    /** Buffer for text without timing annotations yet. */
    private textBuffer;
    /**
     * Add by speaking rate estimation.
     */
    addByRate(timestamp: number, speakingRate: number): void;
    /**
     * Add annotation from TimedString with start_time/end_time.
     */
    addByAnnotation(text: string, startTime: number | undefined, endTime: number | undefined): void;
    /**
     * Get accumulated speaking units up to the given timestamp.
     */
    accumulateTo(timestamp: number): number;
    /** Get the last pushed timestamp. */
    get pushedDuration(): number;
}
declare class SegmentSynchronizerImpl {
    private readonly options;
    private readonly nextInChain;
    /**
     * When true, fall back to seeding `startWallTime` / resolving `startFuture`
     * from the first audio frame in `pushAudio` if `onPlaybackStarted` hasn't
     * fired yet. Used for impls created by mid-flow segment rotation, where the
     * wrapped `AudioOutput.firstFrameEmitted` stays true and won't propagate
     * another `playbackStarted` event. The first impl (constructed in the
     * `TranscriptionSynchronizer` constructor) leaves this `false` so it always
     * trusts the chain — required for `waitPlaybackStart=true` on
     * `DataStreamAudioOutput`.
     */
    private readonly seedFromPushAudio;
    private enabled;
    private textData;
    private audioData;
    private speed;
    private outputStream;
    private outputStreamWriter;
    private captureTask;
    private startWallTime?;
    private startFuture;
    private closedFuture;
    private playbackCompleted;
    private interrupted;
    /**
     * Paces against the visible text only; stateful because a markup tag with spaces in its
     * attributes (e.g. `<expr type="expression" label="warm surprise"/>`) is shredded across
     * word tokens and a per-token strip can't recognize the fragments — each would otherwise
     * be paced as if it were spoken.
     */
    private pacingStripper;
    private pausedWallTime?;
    /** Accumulated paused time in milliseconds; subtracted from wall-clock elapsed. */
    private pausedDuration;
    /** Gate held closed while paused; the main task awaits it between words. */
    private outputEnabledFuture;
    private logger;
    constructor(options: TextSyncOptions, nextInChain: TextOutput, 
    /**
     * When true, fall back to seeding `startWallTime` / resolving `startFuture`
     * from the first audio frame in `pushAudio` if `onPlaybackStarted` hasn't
     * fired yet. Used for impls created by mid-flow segment rotation, where the
     * wrapped `AudioOutput.firstFrameEmitted` stays true and won't propagate
     * another `playbackStarted` event. The first impl (constructed in the
     * `TranscriptionSynchronizer` constructor) leaves this `false` so it always
     * trusts the chain — required for `waitPlaybackStart=true` on
     * `DataStreamAudioOutput`.
     */
    seedFromPushAudio?: boolean);
    get closed(): boolean;
    get audioInputEnded(): boolean;
    get textInputEnded(): boolean;
    get hasPendingText(): boolean;
    get readable(): ReadableStream<string | TimedString>;
    onPlaybackStarted(startTime: number): void;
    pushAudio(frame: AudioFrame): void;
    endAudioInput(): void;
    pushText(text: string | TimedString): void;
    endTextInput(): void;
    pause(): void;
    resume(): void;
    markPlaybackFinished(_playbackPosition: number, interrupted: boolean): void;
    get synchronizedTranscript(): string;
    private captureTaskImpl;
    private mainTask;
    /** Elapsed audio-playback time in seconds — wall-clock minus paused time. */
    private synchronizedElapsedSeconds;
    private calcHyphens;
    private sleepIfNotClosed;
    close(): Promise<void>;
}
export interface TranscriptionSynchronizerOptions {
    speed: number;
    hyphenateWord: (word: string) => string[];
    splitWords: (words: string) => [string, number, number][];
    wordTokenizer: WordTokenizer;
    enabled: boolean;
    /** See {@link TextSyncOptions.expressiveEnabled}. Defaults to "never". */
    expressiveEnabled?: () => boolean;
}
export declare const defaultTextSyncOptions: TranscriptionSynchronizerOptions;
export declare class TranscriptionSynchronizer {
    readonly audioOutput: SyncedAudioOutput;
    readonly textOutput: SyncedTextOutput;
    private options;
    private rotateSegmentTask;
    private _outputsAttached;
    private closed;
    /** @internal */
    _impl: SegmentSynchronizerImpl;
    /** @internal */
    _audioAttached: boolean;
    /** @internal */
    _textAttached: boolean;
    /** @internal */
    _warnedAsymmetricDetach: boolean;
    /** Pause state tracked at the synchronizer level so it can be reapplied across segment rotations.
     * @internal */
    _paused: boolean;
    /**
     * Segments that were rotated out by an incoming `capture_text` *before* their own
     * `on_playback_finished` arrived (the "should not happen" path in `_SyncedTextOutput`).
     * Each still owes one `on_playback_finished`; when it arrives it must settle that
     * already-closed segment instead of the freshly-rotated current `_impl` (which now holds
     * the *next* reply's text). Without this, the stale interrupted verdict tears down the new
     * reply's leading segment and drops its transcript.
     *
     * `acceptedDownstream` records whether the next-in-chain audio output counted the segment:
     * accepted segments owe a *real* finish from downstream, dropped segments owe a *synthetic*
     * drift finish from `waitForPlayout()` — pairing by kind keeps a synthetic finish from
     * consuming an entry whose real finish is still in flight.
     * @internal
     */
    _pendingRotatedSegments: {
        impl: SegmentSynchronizerImpl;
        acceptedDownstream: boolean;
    }[];
    private logger;
    constructor(nextInChainAudio: AudioOutput, nextInChainText: TextOutput, options?: TranscriptionSynchronizerOptions);
    get outputsAttached(): boolean;
    get enabled(): boolean;
    set enabled(value: boolean);
    /** @internal */
    _onAttachmentChanged(args: {
        audioAttached?: boolean;
        textAttached?: boolean;
    }): void;
    rotateSegment(): void;
    close(): Promise<void>;
    barrier(): Promise<void>;
    private rotateSegmentTaskImpl;
}
declare class SyncedAudioOutput extends AudioOutput {
    synchronizer: TranscriptionSynchronizer;
    private nextInChainAudio;
    private pushedDuration;
    /** Whether the downstream output counted the segment currently being captured. */
    private segmentAccepted;
    private segmentOpen;
    /** Acceptance verdict of the most recently flushed segment. */
    private lastSegmentAccepted;
    constructor(synchronizer: TranscriptionSynchronizer, nextInChainAudio: AudioOutput);
    /**
     * Whether the segment a rotation is about to queue was accepted downstream — the open
     * segment's live verdict if frames are still flowing, else the last flushed segment's.
     * @internal
     */
    get _rotationCandidateAccepted(): boolean;
    pause(): void;
    resume(): void;
    captureFrame(frame: AudioFrame): Promise<void>;
    flush(): void;
    clearBuffer(): void;
    waitForPlayout(): Promise<PlaybackFinishedEvent>;
    /**
     * Synthetic finish for a segment the downstream output dropped. Routed through the
     * synchronizer like a real finish (mark finished, attach transcript, rotate), but paired
     * only with queue entries that were *not* accepted downstream — an accepted entry's real
     * finish is still in flight and must settle it instead.
     */
    private settleDriftFinish;
    onPlaybackStarted(createdAt: number): void;
    onPlaybackFinished(ev: PlaybackFinishedEvent): void;
    onAttached(): void;
    onDetached(): void;
}
declare class SyncedTextOutput extends TextOutput {
    private readonly synchronizer;
    readonly nextInChain: TextOutput;
    private capturing;
    private logger;
    constructor(synchronizer: TranscriptionSynchronizer, nextInChain: TextOutput);
    captureText(text: string | TimedString): Promise<void>;
    flush(): Promise<void>;
    onAttached(): void;
    onDetached(): void;
}
export {};
//# sourceMappingURL=synchronizer.d.ts.map