Ctrl K
ComunidadGuías…

packages/ai/src/transcription.ts

Rama dev · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { Effect, Schema, Stream } from "effect"
import { Generation, ProgressEvent, QueuedEvent, type AwaitOptions } from "./generation.js"
import { Media } from "./media.js"
import { MediaModel, composeRoute, tryRequest } from "./media-model.js"
import { MediaRoute } from "./route/media.js"
import { AIError, MediaUsage, ProviderMetadata, ProviderOptions, type BaseRequest } from "./schema/index.js"
import { TranscriptionClient, Service } from "./transcription-client.js"

// ---------------------------------------------------------------------------
// Model
// ---------------------------------------------------------------------------

export type TranscriptionOptions = MediaModel.Options

export type TranscriptionRoute = MediaRoute.AnyRoute<TranscriptionRequest, TranscriptionEvent, TranscriptionResponse>

export class TranscriptionModel<Options extends TranscriptionOptions = TranscriptionOptions> extends MediaModel<
  TranscriptionRoute,
  Options
> {
  declare protected readonly _TranscriptionModel: void

  /** The number of type arguments selects the kind: `<Options>`, `<Options, Frame, State>`, or `<Options, Token>`. */
  static fromRoute<Options extends TranscriptionOptions>(
    route: MediaModel.InlineRouteInput<TranscriptionRequest<TranscriptionModel<Options>>, TranscriptionResponse>,
    input: MediaRoute.ModelInput,
  ): TranscriptionModel<Options>
  static fromRoute<Options extends TranscriptionOptions, Frame, State>(
    route: MediaModel.StreamRouteInput<
      TranscriptionRequest<TranscriptionModel<Options>>,
      TranscriptionEvent,
      Frame,
      State
    >,
    input: MediaRoute.ModelInput,
  ): TranscriptionModel<Options>
  static fromRoute<Options extends TranscriptionOptions, Token>(
    route: MediaModel.QueuedRouteInput<TranscriptionRequest<TranscriptionModel<Options>>, TranscriptionResponse, Token>,
    input: MediaRoute.ModelInput,
  ): TranscriptionModel<Options>
  static fromRoute<Options extends TranscriptionOptions, Frame, State, Token>(
    route: MediaModel.AnyRouteInput<
      TranscriptionRequest<TranscriptionModel<Options>>,
      TranscriptionEvent,
      TranscriptionResponse,
      Frame,
      State,
      Token
    >,
    input: MediaRoute.ModelInput,
  ) {
    return new TranscriptionModel<Options>({
      id: input.id,
      provider: route.protocol.provider,
      http: input.http,
      providerOptions: input.providerOptions,
      route: composeRoute(route, input, collectResponse) as TranscriptionRoute,
    })
  }
}

export const TranscriptionModelSchema = Schema.declare(
  (value): value is TranscriptionModel => value instanceof TranscriptionModel,
  { expected: "Transcription.Model" },
)

// ---------------------------------------------------------------------------
// Request
// ---------------------------------------------------------------------------

export const TranscriptionTimestamps = Schema.Literals(["none", "segment", "word"])
export type TranscriptionTimestamps = Schema.Schema.Type<typeof TranscriptionTimestamps>

const TranscriptionRequestSchema = Schema.Struct({
  model: TranscriptionModelSchema,
  audio: Media.AssetSchema,
  language: Schema.optional(Schema.String),
  prompt: Schema.optional(Schema.String),
  /** Routes that cannot produce the requested granularity fail typed; routes may return more than asked. */
  timestamps: Schema.optional(TranscriptionTimestamps),
  diarize: Schema.optional(Schema.Boolean),
  speakers: Schema.optional(Schema.Int),
  providerOptions: Schema.optional(ProviderOptions),
})

/** A normalized transcription request, the plain object `Transcription.request(...)` returns. */
export type TranscriptionRequest<Model extends TranscriptionModel = TranscriptionModel> = Omit<
  typeof TranscriptionRequestSchema.Type,
  "model" | "providerOptions"
> &
  BaseRequest<Model, TranscriptionModelOptions<Model>>

export type TranscriptionModelOptions<Model> = Model extends TranscriptionModel<infer Options> ? Options : never

/** Input accepted by `Transcription.request` and the calls; a `TranscriptionRequest` is also a valid input. */
export type TranscriptionRequestInput<Model extends TranscriptionModel = TranscriptionModel> = Omit<
  TranscriptionRequest<Model>,
  "providerOptions"
> & {
  readonly providerOptions?: NoInfer<TranscriptionModelOptions<Model>>
}

// ---------------------------------------------------------------------------
// Response and events
// ---------------------------------------------------------------------------

/** Speaker labels are provider-native (`A`, `0`, `spk:0`, `speaker_0`, or a known speaker name). */
export const TranscriptionSegment = Schema.Struct({
  text: Schema.String,
  startSeconds: Schema.Number,
  endSeconds: Schema.Number,
  speaker: Schema.optional(Schema.String),
}).annotate({ identifier: "Transcription.Segment" })
export type TranscriptionSegment = Schema.Schema.Type<typeof TranscriptionSegment>

export const TranscriptionWord = Schema.Struct({
  text: Schema.String,
  startSeconds: Schema.Number,
  endSeconds: Schema.Number,
  speaker: Schema.optional(Schema.String),
  confidence: Schema.optional(Schema.Number),
}).annotate({ identifier: "Transcription.Word" })
export type TranscriptionWord = Schema.Schema.Type<typeof TranscriptionWord>

const transcriptFields = {
  text: Schema.String,
  segments: Schema.optional(Schema.Array(TranscriptionSegment)),
  words: Schema.optional(Schema.Array(TranscriptionWord)),
  /** Provider-native language as detected or echoed (`en`, `english`, `en_us`), lowercased but not normalized. */
  language: Schema.optional(Schema.String),
  durationSeconds: Schema.optional(Schema.Number),
  usage: Schema.optional(MediaUsage),
  providerMetadata: Schema.optional(ProviderMetadata),
}

export class TranscriptionResponse extends Schema.Class<TranscriptionResponse>("Transcription.Response")(
  transcriptFields,
) {}

export const TranscriptionTextDeltaEvent = Schema.Struct({
  type: Schema.tag("text-delta"),
  delta: Schema.String,
}).annotate({ identifier: "Transcription.Event.TextDelta" })

export const TranscriptionSegmentEvent = Schema.Struct({
  type: Schema.tag("segment"),
  segment: TranscriptionSegment,
}).annotate({ identifier: "Transcription.Event.Segment" })

export const TranscriptionFinishEvent = Schema.Struct({
  type: Schema.tag("finish"),
  ...transcriptFields,
}).annotate({ identifier: "Transcription.Event.Finish" })

const transcriptionEventTagged = Schema.Union([
  QueuedEvent,
  ProgressEvent,
  TranscriptionTextDeltaEvent,
  TranscriptionSegmentEvent,
  TranscriptionFinishEvent,
]).pipe(Schema.toTaggedUnion("type"))
export const TranscriptionEvent = Object.assign(transcriptionEventTagged, {
  is: {
    generationQueued: transcriptionEventTagged.guards["generation-queued"],
    generationProgress: transcriptionEventTagged.guards["generation-progress"],
    textDelta: transcriptionEventTagged.guards["text-delta"],
    segment: transcriptionEventTagged.guards.segment,
    finish: transcriptionEventTagged.guards.finish,
  },
})
export type TranscriptionEvent = Schema.Schema.Type<typeof transcriptionEventTagged>

const collectResponse = (events: ReadonlyArray<TranscriptionEvent>): Effect.Effect<TranscriptionResponse> => {
  const finish = events.find(TranscriptionEvent.is.finish)
  // Every transcription protocol's `finish` emits the terminal event or fails, so a completed stream always has one.
  if (finish === undefined) return Effect.die(new Error("The transcription stream completed without a finish event"))
  const { type: _type, ...transcript } = finish
  return Effect.succeed(new TranscriptionResponse(transcript))
}

// ---------------------------------------------------------------------------
// Request-shaped call API
// ---------------------------------------------------------------------------

/** Normalize transcription request input into a plain `TranscriptionRequest`. */
export const request = <const Model extends TranscriptionModel>(
  input: TranscriptionRequestInput<Model>,
): TranscriptionRequest<Model> => ({
  ...input,
})

const validate = Schema.decodeUnknownSync(TranscriptionRequestSchema)

const requestEffect = (input: TranscriptionRequestInput) =>
  tryRequest(() => {
    const normalized = request(input)
    validate(normalized)
    return normalized
  })

export function generate<const Model extends TranscriptionModel>(
  input: TranscriptionRequestInput<Model>,
  options?: AwaitOptions,
): Effect.Effect<TranscriptionResponse, AIError, Service>
export function generate(input: TranscriptionRequestInput, options?: AwaitOptions) {
  return requestEffect(input).pipe(Effect.flatMap((request) => TranscriptionClient.generate(request, options)))
}

export function stream<const Model extends TranscriptionModel>(
  input: TranscriptionRequestInput<Model>,
  options?: AwaitOptions,
): Stream.Stream<TranscriptionEvent, AIError, Service>
export function stream(input: TranscriptionRequestInput, options?: AwaitOptions) {
  return Stream.unwrap(requestEffect(input).pipe(Effect.map((request) => TranscriptionClient.stream(request, options))))
}

/** Inline and streaming routes fail with `UnsupportedOperation`. */
export function start<const Model extends TranscriptionModel>(
  input: TranscriptionRequestInput<Model>,
): Effect.Effect<Generation<TranscriptionResponse>, AIError, Service>
export function start(input: TranscriptionRequestInput) {
  return requestEffect(input).pipe(Effect.flatMap((request) => TranscriptionClient.start(request)))
}

export const resume = (
  model: TranscriptionModel,
  token: unknown,
): Effect.Effect<Generation<TranscriptionResponse>, AIError, Service> => TranscriptionClient.resume(model, token)

export const Transcription = {
  request,
  generate,
  stream,
  start,
  resume,
} as const