@tanstack/ai 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/activities/chat/index.js +24 -3
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +7 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.d.ts +25 -1
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +26 -2
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +7 -0
- package/dist/esm/activities/generateAudio/index.js +26 -1
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/adapter.d.ts +8 -4
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +26 -3
- package/dist/esm/activities/generateImage/index.js +38 -2
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +7 -0
- package/dist/esm/activities/generateSpeech/index.js +26 -1
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +7 -0
- package/dist/esm/activities/generateTranscription/index.js +26 -1
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.d.ts +65 -6
- package/dist/esm/activities/generateVideo/adapter.js +14 -0
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +40 -5
- package/dist/esm/activities/generateVideo/index.js +52 -2
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/snap.d.ts +14 -0
- package/dist/esm/activities/generateVideo/snap.js +54 -0
- package/dist/esm/activities/generateVideo/snap.js.map +1 -0
- package/dist/esm/activities/index.d.ts +3 -2
- package/dist/esm/activities/index.js +2 -0
- package/dist/esm/activities/index.js.map +1 -1
- package/dist/esm/activities/middleware/index.d.ts +2 -0
- package/dist/esm/activities/middleware/run.d.ts +20 -0
- package/dist/esm/activities/middleware/run.js +42 -0
- package/dist/esm/activities/middleware/run.js.map +1 -0
- package/dist/esm/activities/middleware/types.d.ts +118 -0
- package/dist/esm/client.d.ts +1 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/middlewares/otel.d.ts +8 -2
- package/dist/esm/middlewares/otel.js +145 -95
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.d.ts +24 -0
- package/dist/esm/middlewares/usage-attributes.js +43 -0
- package/dist/esm/middlewares/usage-attributes.js.map +1 -0
- package/dist/esm/types.d.ts +103 -14
- package/dist/esm/utilities/errors.d.ts +13 -0
- package/dist/esm/utilities/errors.js +22 -0
- package/dist/esm/utilities/errors.js.map +1 -0
- package/dist/esm/utilities/media-prompt.d.ts +35 -0
- package/dist/esm/utilities/media-prompt.js +43 -0
- package/dist/esm/utilities/media-prompt.js.map +1 -0
- package/dist/esm/utilities/numbers.d.ts +8 -0
- package/dist/esm/utilities/numbers.js +12 -0
- package/dist/esm/utilities/numbers.js.map +1 -0
- package/package.json +2 -2
- package/skills/ai-core/media-generation/SKILL.md +173 -3
- package/src/activities/chat/index.ts +32 -4
- package/src/activities/chat/middleware/types.ts +7 -0
- package/src/activities/chat/tools/lazy-tool-manager.ts +46 -4
- package/src/activities/generateAudio/index.ts +42 -1
- package/src/activities/generateImage/adapter.ts +16 -3
- package/src/activities/generateImage/index.ts +90 -5
- package/src/activities/generateSpeech/index.ts +42 -1
- package/src/activities/generateTranscription/index.ts +42 -1
- package/src/activities/generateVideo/adapter.ts +80 -4
- package/src/activities/generateVideo/index.ts +141 -6
- package/src/activities/generateVideo/snap.ts +100 -0
- package/src/activities/index.ts +4 -0
- package/src/activities/middleware/index.ts +20 -0
- package/src/activities/middleware/run.ts +88 -0
- package/src/activities/middleware/types.ts +173 -0
- package/src/client.ts +4 -0
- package/src/index.ts +23 -0
- package/src/middlewares/otel.ts +195 -120
- package/src/middlewares/usage-attributes.ts +65 -0
- package/src/types.ts +126 -13
- package/src/utilities/errors.ts +29 -0
- package/src/utilities/media-prompt.ts +86 -0
- package/src/utilities/numbers.ts +15 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { aiEventClient } from "@tanstack/ai-event-client";
|
|
2
2
|
import { streamGenerationResult } from "../stream-generation-result.js";
|
|
3
3
|
import { resolveDebugOption } from "../../logger/resolve.js";
|
|
4
|
+
import { createGenerationContext, runGenerationStart, runGenerationUsage, runGenerationFinish, runGenerationError } from "../middleware/run.js";
|
|
4
5
|
const kind = "tts";
|
|
5
6
|
function createId(prefix) {
|
|
6
7
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
@@ -14,12 +15,27 @@ function generateSpeech(options) {
|
|
|
14
15
|
return runGenerateSpeech(options);
|
|
15
16
|
}
|
|
16
17
|
async function runGenerateSpeech(options) {
|
|
17
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
adapter,
|
|
20
|
+
stream: _stream,
|
|
21
|
+
debug: _debug,
|
|
22
|
+
middleware,
|
|
23
|
+
...rest
|
|
24
|
+
} = options;
|
|
18
25
|
const model = adapter.model;
|
|
19
26
|
const requestId = createId("speech");
|
|
20
27
|
const startTime = Date.now();
|
|
21
28
|
const logger = resolveDebugOption(options.debug);
|
|
22
29
|
const providerName = adapter.provider ?? adapter.name ?? "unknown";
|
|
30
|
+
const mwCtx = createGenerationContext({
|
|
31
|
+
requestId,
|
|
32
|
+
activity: "tts",
|
|
33
|
+
provider: adapter.name,
|
|
34
|
+
model,
|
|
35
|
+
modelOptions: rest.modelOptions,
|
|
36
|
+
createId
|
|
37
|
+
});
|
|
38
|
+
await runGenerationStart(middleware, mwCtx);
|
|
23
39
|
aiEventClient.emit("speech:request:started", {
|
|
24
40
|
requestId,
|
|
25
41
|
provider: adapter.name,
|
|
@@ -63,6 +79,11 @@ async function runGenerateSpeech(options) {
|
|
|
63
79
|
bytes: result.audio.length,
|
|
64
80
|
contentType: result.contentType
|
|
65
81
|
});
|
|
82
|
+
if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage);
|
|
83
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
84
|
+
duration,
|
|
85
|
+
usage: result.usage
|
|
86
|
+
});
|
|
66
87
|
return result;
|
|
67
88
|
} catch (error) {
|
|
68
89
|
const duration = Date.now() - startTime;
|
|
@@ -76,6 +97,10 @@ async function runGenerateSpeech(options) {
|
|
|
76
97
|
modelOptions: rest.modelOptions,
|
|
77
98
|
timestamp: Date.now()
|
|
78
99
|
});
|
|
100
|
+
await runGenerationError(middleware, mwCtx, {
|
|
101
|
+
error,
|
|
102
|
+
duration
|
|
103
|
+
});
|
|
79
104
|
logger.errors("generateSpeech activity failed", {
|
|
80
105
|
error,
|
|
81
106
|
source: "generateSpeech"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../src/activities/generateSpeech/index.ts"],"sourcesContent":["/**\n * TTS Activity\n *\n * Generates speech audio from text using text-to-speech models.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { TTSAdapter } from './adapter'\nimport type { StreamChunk, TTSResult } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'tts' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract provider options from a TTSAdapter via ~types.\n */\nexport type TTSProviderOptions<TAdapter> =\n TAdapter extends TTSAdapter<any, any>\n ? TAdapter['~types']['providerOptions']\n : object\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the TTS activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The TTS adapter type\n * @template TStream - Whether to stream the output\n */\nexport interface TTSActivityOptions<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n> {\n /** The TTS adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** The text to convert to speech */\n text: string\n /** The voice to use for generation */\n voice?: string\n /** The output audio format */\n format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'\n /** The speed of the generated audio (0.25 to 4.0) */\n speed?: number\n /** Provider-specific options for TTS generation */\n modelOptions?: TTSProviderOptions<TAdapter>\n /**\n * Whether to stream the generation result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<TTSResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n}\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the TTS activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<TTSResult>\n */\nexport type TTSActivityResult<TStream extends boolean = false> =\n TStream extends true ? AsyncIterable<StreamChunk> : Promise<TTSResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * TTS activity - generates speech from text.\n *\n * Uses AI text-to-speech models to create audio from natural language text.\n *\n * @example Generate speech from text\n * ```ts\n * import { generateSpeech } from '@tanstack/ai'\n * import { openaiSpeech } from '@tanstack/ai-openai'\n *\n * const result = await generateSpeech({\n * adapter: openaiSpeech('tts-1-hd'),\n * text: 'Hello, welcome to TanStack AI!',\n * voice: 'nova'\n * })\n *\n * console.log(result.audio) // base64-encoded audio\n * ```\n *\n * @example With format and speed options\n * ```ts\n * const result = await generateSpeech({\n * adapter: openaiSpeech('tts-1'),\n * text: 'This is slower speech.',\n * voice: 'alloy',\n * format: 'wav',\n * speed: 0.8\n * })\n * ```\n */\nexport function generateSpeech<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(options: TTSActivityOptions<TAdapter, TStream>): TTSActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(() =>\n runGenerateSpeech(options),\n ) as TTSActivityResult<TStream>\n }\n return runGenerateSpeech(options) as TTSActivityResult<TStream>\n}\n\n/**\n * Run the core TTS generation logic (non-streaming).\n */\nasync function runGenerateSpeech<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n>(options: TTSActivityOptions<TAdapter, boolean>): Promise<TTSResult> {\n const { adapter, stream: _stream, debug: _debug, ...rest } = options\n const model = adapter.model\n const requestId = createId('speech')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n aiEventClient.emit('speech:request:started', {\n requestId,\n provider: adapter.name,\n model,\n text: rest.text,\n voice: rest.voice,\n format: rest.format,\n speed: rest.speed,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateSpeech provider=${providerName}`, {\n provider: providerName,\n model,\n })\n\n try {\n const result = await adapter.generateSpeech({ ...rest, model, logger })\n const duration = Date.now() - startTime\n\n aiEventClient.emit('speech:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n audio: result.audio,\n format: result.format,\n audioDuration: result.duration,\n contentType: result.contentType,\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n\n if (result.usage) {\n aiEventClient.emit('speech:usage', {\n requestId,\n model,\n usage: result.usage,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n }\n\n logger.output(`activity=generateSpeech bytes=${result.audio.length}`, {\n bytes: result.audio.length,\n contentType: result.contentType,\n })\n\n return result\n } catch (error) {\n const duration = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('speech:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n logger.errors('generateSpeech activity failed', {\n error,\n source: 'generateSpeech',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateSpeech() function without executing.\n */\nexport function createSpeechOptions<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(\n options: TTSActivityOptions<TAdapter, TStream>,\n): TTSActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type { TTSAdapter, TTSAdapterConfig, AnyTTSAdapter } from './adapter'\nexport { BaseTTSAdapter } from './adapter'\n"],"names":[],"mappings":";;;AAoBO,MAAM,OAAO;AAqEpB,SAAS,SAAS,QAAwB;AACxC,SAAO,GAAG,MAAM,IAAI,KAAK,IAAA,CAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAC1E;AAoCO,SAAS,eAGd,SAA4E;AAC5E,MAAI,QAAQ,QAAQ;AAClB,WAAO;AAAA,MAAuB,MAC5B,kBAAkB,OAAO;AAAA,IAAA;AAAA,EAE7B;AACA,SAAO,kBAAkB,OAAO;AAClC;AAKA,eAAe,kBAEb,SAAoE;AACpE,QAAM,EAAE,SAAS,QAAQ,SAAS,OAAO,QAAQ,GAAG,SAAS;AAC7D,QAAM,QAAQ,QAAQ;AACtB,QAAM,YAAY,SAAS,QAAQ;AACnC,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,SAAyB,mBAAmB,QAAQ,KAAK;AAC/D,QAAM,eACH,QAAiD,YACjD,QAA8B,QAC/B;AAEF,gBAAc,KAAK,0BAA0B;AAAA,IAC3C;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,MAAM,KAAK;AAAA,IACX,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb,OAAO,KAAK;AAAA,IACZ,cAAc,KAAK;AAAA,IACnB,WAAW;AAAA,EAAA,CACZ;AAED,SAAO,QAAQ,oCAAoC,YAAY,IAAI;AAAA,IACjE,UAAU;AAAA,IACV;AAAA,EAAA,CACD;AAED,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,eAAe,EAAE,GAAG,MAAM,OAAO,QAAQ;AACtE,UAAM,WAAW,KAAK,IAAA,IAAQ;AAE9B,kBAAc,KAAK,4BAA4B;AAAA,MAC7C;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA,OAAO,OAAO;AAAA,MACd,QAAQ,OAAO;AAAA,MACf,eAAe,OAAO;AAAA,MACtB,aAAa,OAAO;AAAA,MACpB;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AAED,QAAI,OAAO,OAAO;AAChB,oBAAc,KAAK,gBAAgB;AAAA,QACjC;AAAA,QACA;AAAA,QACA,OAAO,OAAO;AAAA,QACd,cAAc,KAAK;AAAA,QACnB,WAAW,KAAK,IAAA;AAAA,MAAI,CACrB;AAAA,IACH;AAEA,WAAO,OAAO,iCAAiC,OAAO,MAAM,MAAM,IAAI;AAAA,MACpE,OAAO,OAAO,MAAM;AAAA,MACpB,aAAa,OAAO;AAAA,IAAA,CACrB;AAED,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,WAAW,KAAK,IAAA,IAAQ;AAC9B,UAAM,MAAM;AACZ,kBAAc,KAAK,wBAAwB;AAAA,MACzC;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA,OAAO,EAAE,SAAS,IAAI,SAAS,MAAM,IAAI,KAAA;AAAA,MACzC;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AACD,WAAO,OAAO,kCAAkC;AAAA,MAC9C;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AACD,UAAM;AAAA,EACR;AACF;AASO,SAAS,oBAId,SACuC;AACvC,SAAO;AACT;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/activities/generateSpeech/index.ts"],"sourcesContent":["/**\n * TTS Activity\n *\n * Generates speech audio from text using text-to-speech models.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n createGenerationContext,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware'\nimport type { TTSAdapter } from './adapter'\nimport type { StreamChunk, TTSResult } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'tts' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract provider options from a TTSAdapter via ~types.\n */\nexport type TTSProviderOptions<TAdapter> =\n TAdapter extends TTSAdapter<any, any>\n ? TAdapter['~types']['providerOptions']\n : object\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the TTS activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The TTS adapter type\n * @template TStream - Whether to stream the output\n */\nexport interface TTSActivityOptions<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n> {\n /** The TTS adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** The text to convert to speech */\n text: string\n /** The voice to use for generation */\n voice?: string\n /** The output audio format */\n format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'\n /** The speed of the generated audio (0.25 to 4.0) */\n speed?: number\n /** Provider-specific options for TTS generation */\n modelOptions?: TTSProviderOptions<TAdapter>\n /**\n * Whether to stream the generation result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<TTSResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n}\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the TTS activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<TTSResult>\n */\nexport type TTSActivityResult<TStream extends boolean = false> =\n TStream extends true ? AsyncIterable<StreamChunk> : Promise<TTSResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * TTS activity - generates speech from text.\n *\n * Uses AI text-to-speech models to create audio from natural language text.\n *\n * @example Generate speech from text\n * ```ts\n * import { generateSpeech } from '@tanstack/ai'\n * import { openaiSpeech } from '@tanstack/ai-openai'\n *\n * const result = await generateSpeech({\n * adapter: openaiSpeech('tts-1-hd'),\n * text: 'Hello, welcome to TanStack AI!',\n * voice: 'nova'\n * })\n *\n * console.log(result.audio) // base64-encoded audio\n * ```\n *\n * @example With format and speed options\n * ```ts\n * const result = await generateSpeech({\n * adapter: openaiSpeech('tts-1'),\n * text: 'This is slower speech.',\n * voice: 'alloy',\n * format: 'wav',\n * speed: 0.8\n * })\n * ```\n */\nexport function generateSpeech<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(options: TTSActivityOptions<TAdapter, TStream>): TTSActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(() =>\n runGenerateSpeech(options),\n ) as TTSActivityResult<TStream>\n }\n return runGenerateSpeech(options) as TTSActivityResult<TStream>\n}\n\n/**\n * Run the core TTS generation logic (non-streaming).\n */\nasync function runGenerateSpeech<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n>(options: TTSActivityOptions<TAdapter, boolean>): Promise<TTSResult> {\n const {\n adapter,\n stream: _stream,\n debug: _debug,\n middleware,\n ...rest\n } = options\n const model = adapter.model\n const requestId = createId('speech')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'tts',\n provider: adapter.name,\n model,\n modelOptions: rest.modelOptions,\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('speech:request:started', {\n requestId,\n provider: adapter.name,\n model,\n text: rest.text,\n voice: rest.voice,\n format: rest.format,\n speed: rest.speed,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateSpeech provider=${providerName}`, {\n provider: providerName,\n model,\n })\n\n try {\n const result = await adapter.generateSpeech({ ...rest, model, logger })\n const duration = Date.now() - startTime\n\n aiEventClient.emit('speech:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n audio: result.audio,\n format: result.format,\n audioDuration: result.duration,\n contentType: result.contentType,\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n\n if (result.usage) {\n aiEventClient.emit('speech:usage', {\n requestId,\n model,\n usage: result.usage,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n }\n\n logger.output(`activity=generateSpeech bytes=${result.audio.length}`, {\n bytes: result.audio.length,\n contentType: result.contentType,\n })\n\n if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n const duration = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('speech:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n await runGenerationError(middleware, mwCtx, {\n error,\n duration,\n })\n logger.errors('generateSpeech activity failed', {\n error,\n source: 'generateSpeech',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateSpeech() function without executing.\n */\nexport function createSpeechOptions<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(\n options: TTSActivityOptions<TAdapter, TStream>,\n): TTSActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type { TTSAdapter, TTSAdapterConfig, AnyTTSAdapter } from './adapter'\nexport { BaseTTSAdapter } from './adapter'\n"],"names":[],"mappings":";;;;AA4BO,MAAM,OAAO;AA2EpB,SAAS,SAAS,QAAwB;AACxC,SAAO,GAAG,MAAM,IAAI,KAAK,IAAA,CAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAC1E;AAoCO,SAAS,eAGd,SAA4E;AAC5E,MAAI,QAAQ,QAAQ;AAClB,WAAO;AAAA,MAAuB,MAC5B,kBAAkB,OAAO;AAAA,IAAA;AAAA,EAE7B;AACA,SAAO,kBAAkB,OAAO;AAClC;AAKA,eAAe,kBAEb,SAAoE;AACpE,QAAM;AAAA,IACJ;AAAA,IACA,QAAQ;AAAA,IACR,OAAO;AAAA,IACP;AAAA,IACA,GAAG;AAAA,EAAA,IACD;AACJ,QAAM,QAAQ,QAAQ;AACtB,QAAM,YAAY,SAAS,QAAQ;AACnC,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,SAAyB,mBAAmB,QAAQ,KAAK;AAC/D,QAAM,eACH,QAAiD,YACjD,QAA8B,QAC/B;AAEF,QAAM,QAAQ,wBAAwB;AAAA,IACpC;AAAA,IACA,UAAU;AAAA,IACV,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,cAAc,KAAK;AAAA,IACnB;AAAA,EAAA,CACD;AAED,QAAM,mBAAmB,YAAY,KAAK;AAE1C,gBAAc,KAAK,0BAA0B;AAAA,IAC3C;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,MAAM,KAAK;AAAA,IACX,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb,OAAO,KAAK;AAAA,IACZ,cAAc,KAAK;AAAA,IACnB,WAAW;AAAA,EAAA,CACZ;AAED,SAAO,QAAQ,oCAAoC,YAAY,IAAI;AAAA,IACjE,UAAU;AAAA,IACV;AAAA,EAAA,CACD;AAED,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,eAAe,EAAE,GAAG,MAAM,OAAO,QAAQ;AACtE,UAAM,WAAW,KAAK,IAAA,IAAQ;AAE9B,kBAAc,KAAK,4BAA4B;AAAA,MAC7C;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA,OAAO,OAAO;AAAA,MACd,QAAQ,OAAO;AAAA,MACf,eAAe,OAAO;AAAA,MACtB,aAAa,OAAO;AAAA,MACpB;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AAED,QAAI,OAAO,OAAO;AAChB,oBAAc,KAAK,gBAAgB;AAAA,QACjC;AAAA,QACA;AAAA,QACA,OAAO,OAAO;AAAA,QACd,cAAc,KAAK;AAAA,QACnB,WAAW,KAAK,IAAA;AAAA,MAAI,CACrB;AAAA,IACH;AAEA,WAAO,OAAO,iCAAiC,OAAO,MAAM,MAAM,IAAI;AAAA,MACpE,OAAO,OAAO,MAAM;AAAA,MACpB,aAAa,OAAO;AAAA,IAAA,CACrB;AAED,QAAI,OAAO,MAAO,OAAM,mBAAmB,YAAY,OAAO,OAAO,KAAK;AAC1E,UAAM,oBAAoB,YAAY,OAAO;AAAA,MAC3C;AAAA,MACA,OAAO,OAAO;AAAA,IAAA,CACf;AAED,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,WAAW,KAAK,IAAA,IAAQ;AAC9B,UAAM,MAAM;AACZ,kBAAc,KAAK,wBAAwB;AAAA,MACzC;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA,OAAO,EAAE,SAAS,IAAI,SAAS,MAAM,IAAI,KAAA;AAAA,MACzC;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AACD,UAAM,mBAAmB,YAAY,OAAO;AAAA,MAC1C;AAAA,MACA;AAAA,IAAA,CACD;AACD,WAAO,OAAO,kCAAkC;AAAA,MAC9C;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AACD,UAAM;AAAA,EACR;AACF;AASO,SAAS,oBAId,SACuC;AACvC,SAAO;AACT;"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DebugOption } from '../../logger/types.js';
|
|
2
|
+
import { GenerationMiddleware } from '../middleware.js';
|
|
2
3
|
import { TranscriptionAdapter } from './adapter.js';
|
|
3
4
|
import { StreamChunk, TranscriptionResult } from '../../types.js';
|
|
4
5
|
/** The adapter kind this activity handles */
|
|
@@ -43,6 +44,12 @@ export interface TranscriptionActivityOptions<TAdapter extends TranscriptionAdap
|
|
|
43
44
|
* control and/or a custom `Logger`.
|
|
44
45
|
*/
|
|
45
46
|
debug?: DebugOption;
|
|
47
|
+
/**
|
|
48
|
+
* Observe-only middleware notified on start, usage, success, and error. Pass
|
|
49
|
+
* `otelMiddleware()` to emit OpenTelemetry spans, or implement the
|
|
50
|
+
* `GenerationMiddleware` contract for a custom backend.
|
|
51
|
+
*/
|
|
52
|
+
middleware?: Array<GenerationMiddleware>;
|
|
46
53
|
}
|
|
47
54
|
/**
|
|
48
55
|
* Result type for the transcription activity.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { aiEventClient } from "@tanstack/ai-event-client";
|
|
2
2
|
import { streamGenerationResult } from "../stream-generation-result.js";
|
|
3
3
|
import { resolveDebugOption } from "../../logger/resolve.js";
|
|
4
|
+
import { createGenerationContext, runGenerationStart, runGenerationUsage, runGenerationFinish, runGenerationError } from "../middleware/run.js";
|
|
4
5
|
const kind = "transcription";
|
|
5
6
|
function createId(prefix) {
|
|
6
7
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
@@ -16,12 +17,27 @@ function generateTranscription(options) {
|
|
|
16
17
|
);
|
|
17
18
|
}
|
|
18
19
|
async function runGenerateTranscription(options) {
|
|
19
|
-
const {
|
|
20
|
+
const {
|
|
21
|
+
adapter,
|
|
22
|
+
stream: _stream,
|
|
23
|
+
debug: _debug,
|
|
24
|
+
middleware,
|
|
25
|
+
...rest
|
|
26
|
+
} = options;
|
|
20
27
|
const model = adapter.model;
|
|
21
28
|
const requestId = createId("transcription");
|
|
22
29
|
const startTime = Date.now();
|
|
23
30
|
const logger = resolveDebugOption(options.debug);
|
|
24
31
|
const providerName = adapter.provider ?? adapter.name ?? "unknown";
|
|
32
|
+
const mwCtx = createGenerationContext({
|
|
33
|
+
requestId,
|
|
34
|
+
activity: "transcription",
|
|
35
|
+
provider: adapter.name,
|
|
36
|
+
model,
|
|
37
|
+
modelOptions: rest.modelOptions,
|
|
38
|
+
createId
|
|
39
|
+
});
|
|
40
|
+
await runGenerationStart(middleware, mwCtx);
|
|
25
41
|
aiEventClient.emit("transcription:request:started", {
|
|
26
42
|
requestId,
|
|
27
43
|
provider: adapter.name,
|
|
@@ -53,6 +69,11 @@ async function runGenerateTranscription(options) {
|
|
|
53
69
|
`activity=generateTranscription length=${result.text.length}`,
|
|
54
70
|
{ hasText: !!result.text }
|
|
55
71
|
);
|
|
72
|
+
if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage);
|
|
73
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
74
|
+
duration,
|
|
75
|
+
usage: result.usage
|
|
76
|
+
});
|
|
56
77
|
return result;
|
|
57
78
|
} catch (error) {
|
|
58
79
|
const duration = Date.now() - startTime;
|
|
@@ -66,6 +87,10 @@ async function runGenerateTranscription(options) {
|
|
|
66
87
|
modelOptions: rest.modelOptions,
|
|
67
88
|
timestamp: Date.now()
|
|
68
89
|
});
|
|
90
|
+
await runGenerationError(middleware, mwCtx, {
|
|
91
|
+
error,
|
|
92
|
+
duration
|
|
93
|
+
});
|
|
69
94
|
logger.errors("generateTranscription activity failed", {
|
|
70
95
|
error,
|
|
71
96
|
source: "generateTranscription"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../src/activities/generateTranscription/index.ts"],"sourcesContent":["/**\n * Transcription Activity\n *\n * Transcribes audio to text using speech-to-text models.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { TranscriptionAdapter } from './adapter'\nimport type { StreamChunk, TranscriptionResult } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'transcription' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract provider options from a TranscriptionAdapter via ~types.\n */\nexport type TranscriptionProviderOptions<TAdapter> =\n TAdapter extends TranscriptionAdapter<any, any>\n ? TAdapter['~types']['providerOptions']\n : object\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the transcription activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The transcription adapter type\n * @template TStream - Whether to stream the output\n */\nexport interface TranscriptionActivityOptions<\n TAdapter extends TranscriptionAdapter<\n string,\n TranscriptionProviderOptions<TAdapter>\n >,\n TStream extends boolean = false,\n> {\n /** The transcription adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** The audio data to transcribe - can be base64 string, File, Blob, or Buffer */\n audio: string | File | Blob | ArrayBuffer\n /** The language of the audio in ISO-639-1 format (e.g., 'en') */\n language?: string\n /** An optional prompt to guide the transcription */\n prompt?: string\n /** The format of the transcription output */\n responseFormat?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'\n /** Provider-specific options for transcription */\n modelOptions?: TranscriptionProviderOptions<TAdapter>\n /**\n * Whether to stream the transcription result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<TranscriptionResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n}\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the transcription activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<TranscriptionResult>\n */\nexport type TranscriptionActivityResult<TStream extends boolean = false> =\n TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<TranscriptionResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Transcription activity - converts audio to text.\n *\n * Uses AI speech-to-text models to transcribe audio content.\n *\n * @example Transcribe an audio file\n * ```ts\n * import { generateTranscription } from '@tanstack/ai'\n * import { openaiTranscription } from '@tanstack/ai-openai'\n *\n * const result = await generateTranscription({\n * adapter: openaiTranscription('whisper-1'),\n * audio: audioFile, // File, Blob, or base64 string\n * language: 'en'\n * })\n *\n * console.log(result.text)\n * ```\n *\n * @example With verbose output for timestamps\n * ```ts\n * const result = await generateTranscription({\n * adapter: openaiTranscription('whisper-1'),\n * audio: audioFile,\n * responseFormat: 'verbose_json'\n * })\n *\n * result.segments?.forEach(segment => {\n * console.log(`[${segment.start}s - ${segment.end}s]: ${segment.text}`)\n * })\n * ```\n *\n * @example Streaming transcription result\n * ```ts\n * for await (const chunk of generateTranscription({\n * adapter: openaiTranscription('whisper-1'),\n * audio: audioFile,\n * stream: true\n * })) {\n * console.log(chunk)\n * }\n * ```\n */\nexport function generateTranscription<\n TAdapter extends TranscriptionAdapter<\n string,\n TranscriptionProviderOptions<TAdapter>\n >,\n TStream extends boolean = false,\n>(\n options: TranscriptionActivityOptions<TAdapter, TStream>,\n): TranscriptionActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(() =>\n runGenerateTranscription(options),\n ) as TranscriptionActivityResult<TStream>\n }\n\n return runGenerateTranscription(\n options,\n ) as TranscriptionActivityResult<TStream>\n}\n\n/**\n * Run non-streaming transcription\n */\nasync function runGenerateTranscription<\n TAdapter extends TranscriptionAdapter<\n string,\n TranscriptionProviderOptions<TAdapter>\n >,\n>(\n options: TranscriptionActivityOptions<TAdapter, boolean>,\n): Promise<TranscriptionResult> {\n const { adapter, stream: _stream, debug: _debug, ...rest } = options\n const model = adapter.model\n const requestId = createId('transcription')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n aiEventClient.emit('transcription:request:started', {\n requestId,\n provider: adapter.name,\n model,\n language: rest.language,\n prompt: rest.prompt,\n responseFormat: rest.responseFormat,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateTranscription provider=${providerName}`, {\n provider: providerName,\n model,\n })\n\n try {\n const result = await adapter.transcribe({ ...rest, model, logger })\n const duration = Date.now() - startTime\n\n aiEventClient.emit('transcription:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n text: result.text,\n language: result.language,\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n\n logger.output(\n `activity=generateTranscription length=${result.text.length}`,\n { hasText: !!result.text },\n )\n\n return result\n } catch (error) {\n const duration = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('transcription:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n logger.errors('generateTranscription activity failed', {\n error,\n source: 'generateTranscription',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateTranscription() function without executing.\n */\nexport function createTranscriptionOptions<\n TAdapter extends TranscriptionAdapter<\n string,\n TranscriptionProviderOptions<TAdapter>\n >,\n TStream extends boolean = false,\n>(\n options: TranscriptionActivityOptions<TAdapter, TStream>,\n): TranscriptionActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n TranscriptionAdapter,\n TranscriptionAdapterConfig,\n AnyTranscriptionAdapter,\n} from './adapter'\nexport { BaseTranscriptionAdapter } from './adapter'\n"],"names":[],"mappings":";;;AAoBO,MAAM,OAAO;AA0EpB,SAAS,SAAS,QAAwB;AACxC,SAAO,GAAG,MAAM,IAAI,KAAK,IAAA,CAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAC1E;AAiDO,SAAS,sBAOd,SACsC;AACtC,MAAI,QAAQ,QAAQ;AAClB,WAAO;AAAA,MAAuB,MAC5B,yBAAyB,OAAO;AAAA,IAAA;AAAA,EAEpC;AAEA,SAAO;AAAA,IACL;AAAA,EAAA;AAEJ;AAKA,eAAe,yBAMb,SAC8B;AAC9B,QAAM,EAAE,SAAS,QAAQ,SAAS,OAAO,QAAQ,GAAG,SAAS;AAC7D,QAAM,QAAQ,QAAQ;AACtB,QAAM,YAAY,SAAS,eAAe;AAC1C,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,SAAyB,mBAAmB,QAAQ,KAAK;AAC/D,QAAM,eACH,QAAiD,YACjD,QAA8B,QAC/B;AAEF,gBAAc,KAAK,iCAAiC;AAAA,IAClD;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,UAAU,KAAK;AAAA,IACf,QAAQ,KAAK;AAAA,IACb,gBAAgB,KAAK;AAAA,IACrB,cAAc,KAAK;AAAA,IACnB,WAAW;AAAA,EAAA,CACZ;AAED,SAAO,QAAQ,2CAA2C,YAAY,IAAI;AAAA,IACxE,UAAU;AAAA,IACV;AAAA,EAAA,CACD;AAED,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,WAAW,EAAE,GAAG,MAAM,OAAO,QAAQ;AAClE,UAAM,WAAW,KAAK,IAAA,IAAQ;AAE9B,kBAAc,KAAK,mCAAmC;AAAA,MACpD;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA,MAAM,OAAO;AAAA,MACb,UAAU,OAAO;AAAA,MACjB;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AAED,WAAO;AAAA,MACL,yCAAyC,OAAO,KAAK,MAAM;AAAA,MAC3D,EAAE,SAAS,CAAC,CAAC,OAAO,KAAA;AAAA,IAAK;AAG3B,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,WAAW,KAAK,IAAA,IAAQ;AAC9B,UAAM,MAAM;AACZ,kBAAc,KAAK,+BAA+B;AAAA,MAChD;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA,OAAO,EAAE,SAAS,IAAI,SAAS,MAAM,IAAI,KAAA;AAAA,MACzC;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AACD,WAAO,OAAO,yCAAyC;AAAA,MACrD;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AACD,UAAM;AAAA,EACR;AACF;AASO,SAAS,2BAOd,SACiD;AACjD,SAAO;AACT;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/activities/generateTranscription/index.ts"],"sourcesContent":["/**\n * Transcription Activity\n *\n * Transcribes audio to text using speech-to-text models.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n createGenerationContext,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware'\nimport type { TranscriptionAdapter } from './adapter'\nimport type { StreamChunk, TranscriptionResult } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'transcription' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract provider options from a TranscriptionAdapter via ~types.\n */\nexport type TranscriptionProviderOptions<TAdapter> =\n TAdapter extends TranscriptionAdapter<any, any>\n ? TAdapter['~types']['providerOptions']\n : object\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the transcription activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The transcription adapter type\n * @template TStream - Whether to stream the output\n */\nexport interface TranscriptionActivityOptions<\n TAdapter extends TranscriptionAdapter<\n string,\n TranscriptionProviderOptions<TAdapter>\n >,\n TStream extends boolean = false,\n> {\n /** The transcription adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** The audio data to transcribe - can be base64 string, File, Blob, or Buffer */\n audio: string | File | Blob | ArrayBuffer\n /** The language of the audio in ISO-639-1 format (e.g., 'en') */\n language?: string\n /** An optional prompt to guide the transcription */\n prompt?: string\n /** The format of the transcription output */\n responseFormat?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'\n /** Provider-specific options for transcription */\n modelOptions?: TranscriptionProviderOptions<TAdapter>\n /**\n * Whether to stream the transcription result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<TranscriptionResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n}\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the transcription activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<TranscriptionResult>\n */\nexport type TranscriptionActivityResult<TStream extends boolean = false> =\n TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<TranscriptionResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Transcription activity - converts audio to text.\n *\n * Uses AI speech-to-text models to transcribe audio content.\n *\n * @example Transcribe an audio file\n * ```ts\n * import { generateTranscription } from '@tanstack/ai'\n * import { openaiTranscription } from '@tanstack/ai-openai'\n *\n * const result = await generateTranscription({\n * adapter: openaiTranscription('whisper-1'),\n * audio: audioFile, // File, Blob, or base64 string\n * language: 'en'\n * })\n *\n * console.log(result.text)\n * ```\n *\n * @example With verbose output for timestamps\n * ```ts\n * const result = await generateTranscription({\n * adapter: openaiTranscription('whisper-1'),\n * audio: audioFile,\n * responseFormat: 'verbose_json'\n * })\n *\n * result.segments?.forEach(segment => {\n * console.log(`[${segment.start}s - ${segment.end}s]: ${segment.text}`)\n * })\n * ```\n *\n * @example Streaming transcription result\n * ```ts\n * for await (const chunk of generateTranscription({\n * adapter: openaiTranscription('whisper-1'),\n * audio: audioFile,\n * stream: true\n * })) {\n * console.log(chunk)\n * }\n * ```\n */\nexport function generateTranscription<\n TAdapter extends TranscriptionAdapter<\n string,\n TranscriptionProviderOptions<TAdapter>\n >,\n TStream extends boolean = false,\n>(\n options: TranscriptionActivityOptions<TAdapter, TStream>,\n): TranscriptionActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(() =>\n runGenerateTranscription(options),\n ) as TranscriptionActivityResult<TStream>\n }\n\n return runGenerateTranscription(\n options,\n ) as TranscriptionActivityResult<TStream>\n}\n\n/**\n * Run non-streaming transcription\n */\nasync function runGenerateTranscription<\n TAdapter extends TranscriptionAdapter<\n string,\n TranscriptionProviderOptions<TAdapter>\n >,\n>(\n options: TranscriptionActivityOptions<TAdapter, boolean>,\n): Promise<TranscriptionResult> {\n const {\n adapter,\n stream: _stream,\n debug: _debug,\n middleware,\n ...rest\n } = options\n const model = adapter.model\n const requestId = createId('transcription')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'transcription',\n provider: adapter.name,\n model,\n modelOptions: rest.modelOptions,\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('transcription:request:started', {\n requestId,\n provider: adapter.name,\n model,\n language: rest.language,\n prompt: rest.prompt,\n responseFormat: rest.responseFormat,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateTranscription provider=${providerName}`, {\n provider: providerName,\n model,\n })\n\n try {\n const result = await adapter.transcribe({ ...rest, model, logger })\n const duration = Date.now() - startTime\n\n aiEventClient.emit('transcription:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n text: result.text,\n language: result.language,\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n\n logger.output(\n `activity=generateTranscription length=${result.text.length}`,\n { hasText: !!result.text },\n )\n\n if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n const duration = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('transcription:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n await runGenerationError(middleware, mwCtx, {\n error,\n duration,\n })\n logger.errors('generateTranscription activity failed', {\n error,\n source: 'generateTranscription',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateTranscription() function without executing.\n */\nexport function createTranscriptionOptions<\n TAdapter extends TranscriptionAdapter<\n string,\n TranscriptionProviderOptions<TAdapter>\n >,\n TStream extends boolean = false,\n>(\n options: TranscriptionActivityOptions<TAdapter, TStream>,\n): TranscriptionActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n TranscriptionAdapter,\n TranscriptionAdapterConfig,\n AnyTranscriptionAdapter,\n} from './adapter'\nexport { BaseTranscriptionAdapter } from './adapter'\n"],"names":[],"mappings":";;;;AA4BO,MAAM,OAAO;AAgFpB,SAAS,SAAS,QAAwB;AACxC,SAAO,GAAG,MAAM,IAAI,KAAK,IAAA,CAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAC1E;AAiDO,SAAS,sBAOd,SACsC;AACtC,MAAI,QAAQ,QAAQ;AAClB,WAAO;AAAA,MAAuB,MAC5B,yBAAyB,OAAO;AAAA,IAAA;AAAA,EAEpC;AAEA,SAAO;AAAA,IACL;AAAA,EAAA;AAEJ;AAKA,eAAe,yBAMb,SAC8B;AAC9B,QAAM;AAAA,IACJ;AAAA,IACA,QAAQ;AAAA,IACR,OAAO;AAAA,IACP;AAAA,IACA,GAAG;AAAA,EAAA,IACD;AACJ,QAAM,QAAQ,QAAQ;AACtB,QAAM,YAAY,SAAS,eAAe;AAC1C,QAAM,YAAY,KAAK,IAAA;AACvB,QAAM,SAAyB,mBAAmB,QAAQ,KAAK;AAC/D,QAAM,eACH,QAAiD,YACjD,QAA8B,QAC/B;AAEF,QAAM,QAAQ,wBAAwB;AAAA,IACpC;AAAA,IACA,UAAU;AAAA,IACV,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,cAAc,KAAK;AAAA,IACnB;AAAA,EAAA,CACD;AAED,QAAM,mBAAmB,YAAY,KAAK;AAE1C,gBAAc,KAAK,iCAAiC;AAAA,IAClD;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB;AAAA,IACA,UAAU,KAAK;AAAA,IACf,QAAQ,KAAK;AAAA,IACb,gBAAgB,KAAK;AAAA,IACrB,cAAc,KAAK;AAAA,IACnB,WAAW;AAAA,EAAA,CACZ;AAED,SAAO,QAAQ,2CAA2C,YAAY,IAAI;AAAA,IACxE,UAAU;AAAA,IACV;AAAA,EAAA,CACD;AAED,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,WAAW,EAAE,GAAG,MAAM,OAAO,QAAQ;AAClE,UAAM,WAAW,KAAK,IAAA,IAAQ;AAE9B,kBAAc,KAAK,mCAAmC;AAAA,MACpD;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA,MAAM,OAAO;AAAA,MACb,UAAU,OAAO;AAAA,MACjB;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AAED,WAAO;AAAA,MACL,yCAAyC,OAAO,KAAK,MAAM;AAAA,MAC3D,EAAE,SAAS,CAAC,CAAC,OAAO,KAAA;AAAA,IAAK;AAG3B,QAAI,OAAO,MAAO,OAAM,mBAAmB,YAAY,OAAO,OAAO,KAAK;AAC1E,UAAM,oBAAoB,YAAY,OAAO;AAAA,MAC3C;AAAA,MACA,OAAO,OAAO;AAAA,IAAA,CACf;AAED,WAAO;AAAA,EACT,SAAS,OAAO;AACd,UAAM,WAAW,KAAK,IAAA,IAAQ;AAC9B,UAAM,MAAM;AACZ,kBAAc,KAAK,+BAA+B;AAAA,MAChD;AAAA,MACA,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA,OAAO,EAAE,SAAS,IAAI,SAAS,MAAM,IAAI,KAAA;AAAA,MACzC;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,WAAW,KAAK,IAAA;AAAA,IAAI,CACrB;AACD,UAAM,mBAAmB,YAAY,OAAO;AAAA,MAC1C;AAAA,MACA;AAAA,IAAA,CACD;AACD,WAAO,OAAO,yCAAyC;AAAA,MACrD;AAAA,MACA,QAAQ;AAAA,IAAA,CACT;AACD,UAAM;AAAA,EACR;AACF;AASO,SAAS,2BAOd,SACiD;AACjD,SAAO;AACT;"}
|
|
@@ -1,4 +1,33 @@
|
|
|
1
|
-
import { VideoGenerationOptions, VideoJobResult, VideoStatusResult, VideoUrlResult } from '../../types.js';
|
|
1
|
+
import { ModelInputModalitiesByName, VideoGenerationOptions, VideoJobResult, VideoStatusResult, VideoUrlResult } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Structured description of the durations a video model accepts.
|
|
4
|
+
*
|
|
5
|
+
* Tagged union so the same shape can express discrete enums (OpenAI Sora,
|
|
6
|
+
* Veo), continuous ranges, mixed shapes, and models with no duration field.
|
|
7
|
+
* Consumed by `VideoAdapter.availableDurations()`.
|
|
8
|
+
*
|
|
9
|
+
* @experimental Video generation is an experimental feature and may change.
|
|
10
|
+
*/
|
|
11
|
+
export type DurationOptions<T extends string | number | undefined> = {
|
|
12
|
+
kind: 'discrete';
|
|
13
|
+
values: ReadonlyArray<NonNullable<T>>;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'range';
|
|
16
|
+
min: number;
|
|
17
|
+
max: number;
|
|
18
|
+
step?: number;
|
|
19
|
+
unit: 'seconds';
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'mixed';
|
|
22
|
+
values: ReadonlyArray<NonNullable<T>>;
|
|
23
|
+
range?: {
|
|
24
|
+
min: number;
|
|
25
|
+
max: number;
|
|
26
|
+
step?: number;
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'none';
|
|
30
|
+
};
|
|
2
31
|
/**
|
|
3
32
|
* Configuration for video adapter instances
|
|
4
33
|
*
|
|
@@ -24,8 +53,13 @@ export interface VideoAdapterConfig {
|
|
|
24
53
|
* - TProviderOptions: Provider-specific options (already resolved)
|
|
25
54
|
* - TModelProviderOptionsByName: Map from model name to its specific provider options
|
|
26
55
|
* - TModelSizeByName: Map from model name to its supported sizes
|
|
56
|
+
* - TModelInputModalitiesByName: Map from model name to the non-text prompt
|
|
57
|
+
* modalities it accepts (constrains the `prompt` part types at compile time)
|
|
58
|
+
* - TModelDurationByName: Map from model name to its supported duration
|
|
59
|
+
* union. Defaults to `Record<string, number>` so adapters that haven't
|
|
60
|
+
* declared a map keep today's `duration?: number` typing.
|
|
27
61
|
*/
|
|
28
|
-
export interface VideoAdapter<TModel extends string = string, TProviderOptions extends object = Record<string, unknown>, TModelProviderOptionsByName extends Record<string, any> = Record<string, any>, TModelSizeByName extends Record<string, string | undefined> = Record<string, string>> {
|
|
62
|
+
export interface VideoAdapter<TModel extends string = string, TProviderOptions extends object = Record<string, unknown>, TModelProviderOptionsByName extends Record<string, any> = Record<string, any>, TModelSizeByName extends Record<string, string | undefined> = Record<string, string>, TModelInputModalitiesByName extends ModelInputModalitiesByName = ModelInputModalitiesByName, TModelDurationByName extends Record<string, string | number | undefined> = Record<string, number>> {
|
|
29
63
|
/** Discriminator for adapter kind - used to determine API shape */
|
|
30
64
|
readonly kind: 'video';
|
|
31
65
|
/** Adapter name identifier */
|
|
@@ -39,12 +73,14 @@ export interface VideoAdapter<TModel extends string = string, TProviderOptions e
|
|
|
39
73
|
providerOptions: TProviderOptions;
|
|
40
74
|
modelProviderOptionsByName: TModelProviderOptionsByName;
|
|
41
75
|
modelSizeByName: TModelSizeByName;
|
|
76
|
+
modelInputModalitiesByName: TModelInputModalitiesByName;
|
|
77
|
+
modelDurationByName: TModelDurationByName;
|
|
42
78
|
};
|
|
43
79
|
/**
|
|
44
80
|
* Create a new video generation job.
|
|
45
81
|
* Returns a job ID that can be used to poll for status and retrieve the video.
|
|
46
82
|
*/
|
|
47
|
-
createVideoJob: (options: VideoGenerationOptions<TProviderOptions, TModelSizeByName[TModel]>) => Promise<VideoJobResult>;
|
|
83
|
+
createVideoJob: (options: VideoGenerationOptions<TProviderOptions, TModelSizeByName[TModel], TModelDurationByName[TModel]>) => Promise<VideoJobResult>;
|
|
48
84
|
/**
|
|
49
85
|
* Get the current status of a video generation job.
|
|
50
86
|
*/
|
|
@@ -54,12 +90,23 @@ export interface VideoAdapter<TModel extends string = string, TProviderOptions e
|
|
|
54
90
|
* Should only be called after status is 'completed'.
|
|
55
91
|
*/
|
|
56
92
|
getVideoUrl: (jobId: string) => Promise<VideoUrlResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Describe the durations this adapter's model accepts. Returns a tagged
|
|
95
|
+
* union so consumers can render UI / coerce input without provider-specific
|
|
96
|
+
* knowledge.
|
|
97
|
+
*/
|
|
98
|
+
availableDurations: () => DurationOptions<TModelDurationByName[TModel]>;
|
|
99
|
+
/**
|
|
100
|
+
* Coerce a raw seconds value to the closest valid duration for this model.
|
|
101
|
+
* Returns `undefined` for models with no duration field.
|
|
102
|
+
*/
|
|
103
|
+
snapDuration: (seconds: number) => TModelDurationByName[TModel] | undefined;
|
|
57
104
|
}
|
|
58
105
|
/**
|
|
59
106
|
* A VideoAdapter with any/unknown type parameters.
|
|
60
107
|
* Useful as a constraint in generic functions and interfaces.
|
|
61
108
|
*/
|
|
62
|
-
export type AnyVideoAdapter = VideoAdapter<any, any, any, any>;
|
|
109
|
+
export type AnyVideoAdapter = VideoAdapter<any, any, any, any, any, any>;
|
|
63
110
|
/**
|
|
64
111
|
* Abstract base class for video generation adapters.
|
|
65
112
|
* Extend this class to implement a video adapter for a specific provider.
|
|
@@ -68,7 +115,7 @@ export type AnyVideoAdapter = VideoAdapter<any, any, any, any>;
|
|
|
68
115
|
*
|
|
69
116
|
* Generic parameters match VideoAdapter - all pre-resolved by the provider function.
|
|
70
117
|
*/
|
|
71
|
-
export declare abstract class BaseVideoAdapter<TModel extends string = string, TProviderOptions extends object = Record<string, unknown>, TModelProviderOptionsByName extends Record<string, any> = Record<string, any>, TModelSizeByName extends Record<string, string | undefined> = Record<string, string>> implements VideoAdapter<TModel, TProviderOptions, TModelProviderOptionsByName, TModelSizeByName> {
|
|
118
|
+
export declare abstract class BaseVideoAdapter<TModel extends string = string, TProviderOptions extends object = Record<string, unknown>, TModelProviderOptionsByName extends Record<string, any> = Record<string, any>, TModelSizeByName extends Record<string, string | undefined> = Record<string, string>, TModelInputModalitiesByName extends ModelInputModalitiesByName = ModelInputModalitiesByName, TModelDurationByName extends Record<string, string | number | undefined> = Record<string, number>> implements VideoAdapter<TModel, TProviderOptions, TModelProviderOptionsByName, TModelSizeByName, TModelInputModalitiesByName, TModelDurationByName> {
|
|
72
119
|
readonly kind: "video";
|
|
73
120
|
abstract readonly name: string;
|
|
74
121
|
readonly model: TModel;
|
|
@@ -76,11 +123,23 @@ export declare abstract class BaseVideoAdapter<TModel extends string = string, T
|
|
|
76
123
|
providerOptions: TProviderOptions;
|
|
77
124
|
modelProviderOptionsByName: TModelProviderOptionsByName;
|
|
78
125
|
modelSizeByName: TModelSizeByName;
|
|
126
|
+
modelInputModalitiesByName: TModelInputModalitiesByName;
|
|
127
|
+
modelDurationByName: TModelDurationByName;
|
|
79
128
|
};
|
|
80
129
|
protected config: VideoAdapterConfig;
|
|
81
130
|
constructor(config: VideoAdapterConfig | undefined, model: TModel);
|
|
82
|
-
abstract createVideoJob(options: VideoGenerationOptions<TProviderOptions, TModelSizeByName[TModel]>): Promise<VideoJobResult>;
|
|
131
|
+
abstract createVideoJob(options: VideoGenerationOptions<TProviderOptions, TModelSizeByName[TModel], TModelDurationByName[TModel]>): Promise<VideoJobResult>;
|
|
83
132
|
abstract getVideoStatus(jobId: string): Promise<VideoStatusResult>;
|
|
84
133
|
abstract getVideoUrl(jobId: string): Promise<VideoUrlResult>;
|
|
134
|
+
/**
|
|
135
|
+
* Default implementation returns `{ kind: 'none' }`. Adapters that have
|
|
136
|
+
* declared their per-model duration map should override this.
|
|
137
|
+
*/
|
|
138
|
+
availableDurations(): DurationOptions<TModelDurationByName[TModel]>;
|
|
139
|
+
/**
|
|
140
|
+
* Default implementation returns `undefined`. Adapters that have declared
|
|
141
|
+
* their per-model duration map should override.
|
|
142
|
+
*/
|
|
143
|
+
snapDuration(_seconds: number): TModelDurationByName[TModel] | undefined;
|
|
85
144
|
protected generateId(): string;
|
|
86
145
|
}
|
|
@@ -6,6 +6,20 @@ class BaseVideoAdapter {
|
|
|
6
6
|
this.config = config;
|
|
7
7
|
this.model = model;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Default implementation returns `{ kind: 'none' }`. Adapters that have
|
|
11
|
+
* declared their per-model duration map should override this.
|
|
12
|
+
*/
|
|
13
|
+
availableDurations() {
|
|
14
|
+
return { kind: "none" };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Default implementation returns `undefined`. Adapters that have declared
|
|
18
|
+
* their per-model duration map should override.
|
|
19
|
+
*/
|
|
20
|
+
snapDuration(_seconds) {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
9
23
|
generateId() {
|
|
10
24
|
return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
11
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","sources":["../../../../src/activities/generateVideo/adapter.ts"],"sourcesContent":["import type {\n VideoGenerationOptions,\n VideoJobResult,\n VideoStatusResult,\n VideoUrlResult,\n} from '../../types'\n\n/**\n * Configuration for video adapter instances\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface VideoAdapterConfig {\n apiKey?: string\n baseUrl?: string\n timeout?: number\n maxRetries?: number\n headers?: Record<string, string>\n}\n\n/**\n * Video adapter interface with pre-resolved generics.\n *\n * An adapter is created by a provider function: `provider('model')` → `adapter`\n * All type resolution happens at the provider call site, not in this interface.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * Generic parameters:\n * - TModel: The specific model name (e.g., 'sora-2')\n * - TProviderOptions: Provider-specific options (already resolved)\n * - TModelProviderOptionsByName: Map from model name to its specific provider options\n * - TModelSizeByName: Map from model name to its supported sizes\n */\nexport interface VideoAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,\n TModelSizeByName extends Record<string, string | undefined> = Record<\n string,\n string\n >,\n> {\n /** Discriminator for adapter kind - used to determine API shape */\n readonly kind: 'video'\n /** Adapter name identifier */\n readonly name: string\n /** The model this adapter is configured for */\n readonly model: TModel\n\n /**\n * @internal Type-only properties for inference. Not assigned at runtime.\n */\n '~types': {\n providerOptions: TProviderOptions\n modelProviderOptionsByName: TModelProviderOptionsByName\n modelSizeByName: TModelSizeByName\n }\n\n /**\n * Create a new video generation job.\n * Returns a job ID that can be used to poll for status and retrieve the video.\n */\n createVideoJob: (\n options: VideoGenerationOptions
|
|
1
|
+
{"version":3,"file":"adapter.js","sources":["../../../../src/activities/generateVideo/adapter.ts"],"sourcesContent":["import type {\n ModelInputModalitiesByName,\n VideoGenerationOptions,\n VideoJobResult,\n VideoStatusResult,\n VideoUrlResult,\n} from '../../types'\n\n/**\n * Structured description of the durations a video model accepts.\n *\n * Tagged union so the same shape can express discrete enums (OpenAI Sora,\n * Veo), continuous ranges, mixed shapes, and models with no duration field.\n * Consumed by `VideoAdapter.availableDurations()`.\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport type DurationOptions<T extends string | number | undefined> =\n | { kind: 'discrete'; values: ReadonlyArray<NonNullable<T>> }\n | { kind: 'range'; min: number; max: number; step?: number; unit: 'seconds' }\n | {\n kind: 'mixed'\n values: ReadonlyArray<NonNullable<T>>\n range?: { min: number; max: number; step?: number }\n }\n | { kind: 'none' }\n\n/**\n * Configuration for video adapter instances\n *\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface VideoAdapterConfig {\n apiKey?: string\n baseUrl?: string\n timeout?: number\n maxRetries?: number\n headers?: Record<string, string>\n}\n\n/**\n * Video adapter interface with pre-resolved generics.\n *\n * An adapter is created by a provider function: `provider('model')` → `adapter`\n * All type resolution happens at the provider call site, not in this interface.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * Generic parameters:\n * - TModel: The specific model name (e.g., 'sora-2')\n * - TProviderOptions: Provider-specific options (already resolved)\n * - TModelProviderOptionsByName: Map from model name to its specific provider options\n * - TModelSizeByName: Map from model name to its supported sizes\n * - TModelInputModalitiesByName: Map from model name to the non-text prompt\n * modalities it accepts (constrains the `prompt` part types at compile time)\n * - TModelDurationByName: Map from model name to its supported duration\n * union. Defaults to `Record<string, number>` so adapters that haven't\n * declared a map keep today's `duration?: number` typing.\n */\nexport interface VideoAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,\n TModelSizeByName extends Record<string, string | undefined> = Record<\n string,\n string\n >,\n TModelInputModalitiesByName extends ModelInputModalitiesByName =\n ModelInputModalitiesByName,\n TModelDurationByName extends Record<string, string | number | undefined> =\n Record<string, number>,\n> {\n /** Discriminator for adapter kind - used to determine API shape */\n readonly kind: 'video'\n /** Adapter name identifier */\n readonly name: string\n /** The model this adapter is configured for */\n readonly model: TModel\n\n /**\n * @internal Type-only properties for inference. Not assigned at runtime.\n */\n '~types': {\n providerOptions: TProviderOptions\n modelProviderOptionsByName: TModelProviderOptionsByName\n modelSizeByName: TModelSizeByName\n modelInputModalitiesByName: TModelInputModalitiesByName\n modelDurationByName: TModelDurationByName\n }\n\n /**\n * Create a new video generation job.\n * Returns a job ID that can be used to poll for status and retrieve the video.\n */\n createVideoJob: (\n options: VideoGenerationOptions<\n TProviderOptions,\n TModelSizeByName[TModel],\n TModelDurationByName[TModel]\n >,\n ) => Promise<VideoJobResult>\n\n /**\n * Get the current status of a video generation job.\n */\n getVideoStatus: (jobId: string) => Promise<VideoStatusResult>\n\n /**\n * Get the URL to download/view the generated video.\n * Should only be called after status is 'completed'.\n */\n getVideoUrl: (jobId: string) => Promise<VideoUrlResult>\n\n /**\n * Describe the durations this adapter's model accepts. Returns a tagged\n * union so consumers can render UI / coerce input without provider-specific\n * knowledge.\n */\n availableDurations: () => DurationOptions<TModelDurationByName[TModel]>\n\n /**\n * Coerce a raw seconds value to the closest valid duration for this model.\n * Returns `undefined` for models with no duration field.\n */\n snapDuration: (seconds: number) => TModelDurationByName[TModel] | undefined\n}\n\n/**\n * A VideoAdapter with any/unknown type parameters.\n * Useful as a constraint in generic functions and interfaces.\n */\nexport type AnyVideoAdapter = VideoAdapter<any, any, any, any, any, any>\n\n/**\n * Abstract base class for video generation adapters.\n * Extend this class to implement a video adapter for a specific provider.\n *\n * @experimental Video generation is an experimental feature and may change.\n *\n * Generic parameters match VideoAdapter - all pre-resolved by the provider function.\n */\nexport abstract class BaseVideoAdapter<\n TModel extends string = string,\n TProviderOptions extends object = Record<string, unknown>,\n TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,\n TModelSizeByName extends Record<string, string | undefined> = Record<\n string,\n string\n >,\n TModelInputModalitiesByName extends ModelInputModalitiesByName =\n ModelInputModalitiesByName,\n TModelDurationByName extends Record<string, string | number | undefined> =\n Record<string, number>,\n> implements VideoAdapter<\n TModel,\n TProviderOptions,\n TModelProviderOptionsByName,\n TModelSizeByName,\n TModelInputModalitiesByName,\n TModelDurationByName\n> {\n readonly kind = 'video' as const\n abstract readonly name: string\n readonly model: TModel\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: TProviderOptions\n modelProviderOptionsByName: TModelProviderOptionsByName\n modelSizeByName: TModelSizeByName\n modelInputModalitiesByName: TModelInputModalitiesByName\n modelDurationByName: TModelDurationByName\n }\n\n protected config: VideoAdapterConfig\n\n constructor(config: VideoAdapterConfig = {}, model: TModel) {\n this.config = config\n this.model = model\n }\n\n abstract createVideoJob(\n options: VideoGenerationOptions<\n TProviderOptions,\n TModelSizeByName[TModel],\n TModelDurationByName[TModel]\n >,\n ): Promise<VideoJobResult>\n\n abstract getVideoStatus(jobId: string): Promise<VideoStatusResult>\n\n abstract getVideoUrl(jobId: string): Promise<VideoUrlResult>\n\n /**\n * Default implementation returns `{ kind: 'none' }`. Adapters that have\n * declared their per-model duration map should override this.\n */\n availableDurations(): DurationOptions<TModelDurationByName[TModel]> {\n return { kind: 'none' }\n }\n\n /**\n * Default implementation returns `undefined`. Adapters that have declared\n * their per-model duration map should override.\n */\n snapDuration(_seconds: number): TModelDurationByName[TModel] | undefined {\n return undefined\n }\n\n protected generateId(): string {\n return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n}\n"],"names":[],"mappings":"AA6IO,MAAe,iBAmBpB;AAAA,EACS,OAAO;AAAA,EAEP;AAAA,EAWC;AAAA,EAEV,YAAY,SAA6B,CAAA,GAAI,OAAe;AAC1D,SAAK,SAAS;AACd,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,qBAAoE;AAClE,WAAO,EAAE,MAAM,OAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa,UAA4D;AACvE,WAAO;AAAA,EACT;AAAA,EAEU,aAAqB;AAC7B,WAAO,GAAG,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,UAAU,CAAC,CAAC;AAAA,EAC9E;AACF;"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DebugOption } from '../../logger/types.js';
|
|
2
|
+
import { GenerationMiddleware } from '../middleware.js';
|
|
2
3
|
import { VideoAdapter } from './adapter.js';
|
|
3
|
-
import { StreamChunk, TokenUsage, VideoJobResult, VideoStatusResult, VideoUrlResult } from '../../types.js';
|
|
4
|
+
import { MediaPrompt, MediaPromptFor, StreamChunk, TokenUsage, VideoJobResult, VideoStatusResult, VideoUrlResult } from '../../types.js';
|
|
4
5
|
/** The adapter kind this activity handles */
|
|
5
6
|
export declare const kind: "video";
|
|
6
7
|
/**
|
|
@@ -11,6 +12,19 @@ export type VideoProviderOptions<TAdapter> = TAdapter extends VideoAdapter<any,
|
|
|
11
12
|
* Extract the size type for a VideoAdapter's model via ~types.
|
|
12
13
|
*/
|
|
13
14
|
export type VideoSizeForAdapter<TAdapter> = TAdapter extends VideoAdapter<infer TModel, any, any, infer TSizeMap> ? TModel extends keyof TSizeMap ? TSizeMap[TModel] : string : string;
|
|
15
|
+
/**
|
|
16
|
+
* Extract the prompt type a model accepts from a VideoAdapter via ~types.
|
|
17
|
+
* Mirrors `ImagePromptForModel`: models in the adapter's input-modality map
|
|
18
|
+
* get a `prompt` narrowed to text + their supported part types; adapters
|
|
19
|
+
* without a map fall back to the full MediaPrompt.
|
|
20
|
+
*/
|
|
21
|
+
export type VideoPromptForAdapter<TAdapter> = TAdapter extends VideoAdapter<infer TModel, any, any, any, infer ModsByName> ? string extends keyof ModsByName ? MediaPrompt : TModel extends keyof ModsByName ? MediaPromptFor<ModsByName[TModel][number]> : MediaPrompt : MediaPrompt;
|
|
22
|
+
/**
|
|
23
|
+
* Extract the duration type for a VideoAdapter's model via ~types.
|
|
24
|
+
* Mirrors `VideoSizeForAdapter`. Falls back to `number` for adapters that
|
|
25
|
+
* haven't declared per-model duration constraints.
|
|
26
|
+
*/
|
|
27
|
+
export type VideoDurationForAdapter<TAdapter> = TAdapter extends VideoAdapter<infer TModel, any, any, any, any, infer TDurationMap> ? TModel extends keyof TDurationMap ? TDurationMap[TModel] : number : number;
|
|
14
28
|
/**
|
|
15
29
|
* Base options shared by all video activity operations.
|
|
16
30
|
* The model is extracted from the adapter's model property.
|
|
@@ -33,12 +47,25 @@ interface VideoActivityBaseOptions<TAdapter extends VideoAdapter<string, any, an
|
|
|
33
47
|
export type VideoCreateOptions<TAdapter extends VideoAdapter<string, any, any, any>, TStream extends boolean = false> = VideoActivityBaseOptions<TAdapter> & {
|
|
34
48
|
/** Request type - create a new job (default if not specified) */
|
|
35
49
|
request?: 'create';
|
|
36
|
-
/**
|
|
37
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Description of the desired video. Either a plain string, or — for models
|
|
52
|
+
* that support image-conditioned generation — an ordered array of content
|
|
53
|
+
* parts interleaving text with image inputs. Image parts may carry
|
|
54
|
+
* `metadata.role` (`'start_frame' | 'end_frame' | 'reference' |
|
|
55
|
+
* 'character'`) to disambiguate intent; positional fallback otherwise. The
|
|
56
|
+
* accepted part types are narrowed per model via the adapter's
|
|
57
|
+
* input-modality map.
|
|
58
|
+
*/
|
|
59
|
+
prompt: VideoPromptForAdapter<TAdapter>;
|
|
38
60
|
/** Video size — format depends on the provider (e.g., "16:9", "1280x720") */
|
|
39
61
|
size?: VideoSizeForAdapter<TAdapter>;
|
|
40
|
-
/**
|
|
41
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Video duration in seconds. Adapters that declare a per-model duration
|
|
64
|
+
* map narrow this to the model's valid union (e.g. `4 | 6 | 8` for Veo 3).
|
|
65
|
+
* Pass `adapter.snapDuration(seconds)` to coerce raw seconds to a valid
|
|
66
|
+
* value.
|
|
67
|
+
*/
|
|
68
|
+
duration?: VideoDurationForAdapter<TAdapter>;
|
|
42
69
|
/**
|
|
43
70
|
* Whether to stream the video generation lifecycle.
|
|
44
71
|
* When true, returns an AsyncIterable<StreamChunk> that handles the full
|
|
@@ -60,6 +87,14 @@ export type VideoCreateOptions<TAdapter extends VideoAdapter<string, any, any, a
|
|
|
60
87
|
* control and/or a custom `Logger`.
|
|
61
88
|
*/
|
|
62
89
|
debug?: DebugOption;
|
|
90
|
+
/**
|
|
91
|
+
* Observe-only middleware notified on start, usage, success, and error. Pass
|
|
92
|
+
* `otelMiddleware()` to emit OpenTelemetry spans, or implement the
|
|
93
|
+
* `GenerationMiddleware` contract for a custom backend. In streaming mode the
|
|
94
|
+
* span covers the full create→poll→complete lifecycle; in non-streaming mode
|
|
95
|
+
* it covers job submission. An abandoned stream fires `onAbort`.
|
|
96
|
+
*/
|
|
97
|
+
middleware?: Array<GenerationMiddleware>;
|
|
63
98
|
} & ({} extends VideoProviderOptions<TAdapter> ? {
|
|
64
99
|
/** Provider-specific options for video generation */ modelOptions?: VideoProviderOptions<TAdapter>;
|
|
65
100
|
} : {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { aiEventClient } from "@tanstack/ai-event-client";
|
|
2
2
|
import { toRunErrorPayload } from "../error-payload.js";
|
|
3
3
|
import { resolveDebugOption } from "../../logger/resolve.js";
|
|
4
|
+
import { createGenerationContext, runGenerationStart, runGenerationUsage, runGenerationFinish, runGenerationError, runGenerationAbort } from "../middleware/run.js";
|
|
4
5
|
const kind = "video";
|
|
5
6
|
function createId(prefix) {
|
|
6
7
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
@@ -14,10 +15,21 @@ function generateVideo(options) {
|
|
|
14
15
|
return runCreateVideoJob(options);
|
|
15
16
|
}
|
|
16
17
|
async function runCreateVideoJob(options) {
|
|
17
|
-
const { adapter, prompt, size, duration, modelOptions } = options;
|
|
18
|
+
const { adapter, prompt, size, duration, modelOptions, middleware } = options;
|
|
18
19
|
const model = adapter.model;
|
|
20
|
+
const requestId = createId("video");
|
|
21
|
+
const startTime = Date.now();
|
|
19
22
|
const logger = resolveDebugOption(options.debug);
|
|
20
23
|
const providerName = adapter.provider ?? adapter.name ?? "unknown";
|
|
24
|
+
const mwCtx = createGenerationContext({
|
|
25
|
+
requestId,
|
|
26
|
+
activity: "video",
|
|
27
|
+
provider: adapter.name,
|
|
28
|
+
model,
|
|
29
|
+
modelOptions,
|
|
30
|
+
createId
|
|
31
|
+
});
|
|
32
|
+
await runGenerationStart(middleware, mwCtx);
|
|
21
33
|
logger.request(`activity=generateVideo provider=${providerName}`, {
|
|
22
34
|
provider: providerName,
|
|
23
35
|
model
|
|
@@ -35,8 +47,15 @@ async function runCreateVideoJob(options) {
|
|
|
35
47
|
jobId: result.jobId,
|
|
36
48
|
model: result.model
|
|
37
49
|
});
|
|
50
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
51
|
+
duration: Date.now() - startTime
|
|
52
|
+
});
|
|
38
53
|
return result;
|
|
39
54
|
} catch (error) {
|
|
55
|
+
await runGenerationError(middleware, mwCtx, {
|
|
56
|
+
error,
|
|
57
|
+
duration: Date.now() - startTime
|
|
58
|
+
});
|
|
40
59
|
logger.errors("generateVideo activity failed", {
|
|
41
60
|
error,
|
|
42
61
|
source: "generateVideo"
|
|
@@ -48,9 +67,11 @@ function sleep(ms) {
|
|
|
48
67
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
68
|
}
|
|
50
69
|
async function* runStreamingVideoGeneration(options) {
|
|
51
|
-
const { adapter, prompt, size, duration, modelOptions } = options;
|
|
70
|
+
const { adapter, prompt, size, duration, modelOptions, middleware } = options;
|
|
52
71
|
const model = adapter.model;
|
|
53
72
|
const runId = options.runId ?? createId("run");
|
|
73
|
+
const requestId = createId("video");
|
|
74
|
+
const obsStartTime = Date.now();
|
|
54
75
|
const pollingInterval = options.pollingInterval ?? 2e3;
|
|
55
76
|
const maxDuration = options.maxDuration ?? 6e5;
|
|
56
77
|
const logger = resolveDebugOption(options.debug);
|
|
@@ -62,6 +83,15 @@ async function* runStreamingVideoGeneration(options) {
|
|
|
62
83
|
threadId,
|
|
63
84
|
timestamp: Date.now()
|
|
64
85
|
};
|
|
86
|
+
const mwCtx = createGenerationContext({
|
|
87
|
+
requestId,
|
|
88
|
+
activity: "video",
|
|
89
|
+
provider: adapter.name,
|
|
90
|
+
model,
|
|
91
|
+
modelOptions,
|
|
92
|
+
createId
|
|
93
|
+
});
|
|
94
|
+
await runGenerationStart(middleware, mwCtx);
|
|
65
95
|
logger.request(
|
|
66
96
|
`activity=generateVideo provider=${providerName} stream=true`,
|
|
67
97
|
{
|
|
@@ -69,6 +99,7 @@ async function* runStreamingVideoGeneration(options) {
|
|
|
69
99
|
model
|
|
70
100
|
}
|
|
71
101
|
);
|
|
102
|
+
let settled = false;
|
|
72
103
|
try {
|
|
73
104
|
const jobResult = await adapter.createVideoJob({
|
|
74
105
|
model,
|
|
@@ -108,6 +139,13 @@ async function* runStreamingVideoGeneration(options) {
|
|
|
108
139
|
url: urlResult.url
|
|
109
140
|
}
|
|
110
141
|
);
|
|
142
|
+
if (urlResult.usage)
|
|
143
|
+
await runGenerationUsage(middleware, mwCtx, urlResult.usage);
|
|
144
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
145
|
+
duration: Date.now() - obsStartTime,
|
|
146
|
+
usage: urlResult.usage
|
|
147
|
+
});
|
|
148
|
+
settled = true;
|
|
111
149
|
yield {
|
|
112
150
|
type: "CUSTOM",
|
|
113
151
|
name: "generation:result",
|
|
@@ -136,6 +174,11 @@ async function* runStreamingVideoGeneration(options) {
|
|
|
136
174
|
throw new Error("Video generation timed out");
|
|
137
175
|
} catch (error) {
|
|
138
176
|
const payload = toRunErrorPayload(error, "Video generation failed");
|
|
177
|
+
settled = true;
|
|
178
|
+
await runGenerationError(middleware, mwCtx, {
|
|
179
|
+
error,
|
|
180
|
+
duration: Date.now() - obsStartTime
|
|
181
|
+
});
|
|
139
182
|
logger.errors("generateVideo activity failed", {
|
|
140
183
|
message: payload.message,
|
|
141
184
|
code: payload.code,
|
|
@@ -150,6 +193,13 @@ async function* runStreamingVideoGeneration(options) {
|
|
|
150
193
|
error: payload,
|
|
151
194
|
timestamp: Date.now()
|
|
152
195
|
};
|
|
196
|
+
} finally {
|
|
197
|
+
if (!settled) {
|
|
198
|
+
await runGenerationAbort(middleware, mwCtx, {
|
|
199
|
+
reason: "Video generation stream abandoned before completion",
|
|
200
|
+
duration: Date.now() - obsStartTime
|
|
201
|
+
});
|
|
202
|
+
}
|
|
153
203
|
}
|
|
154
204
|
}
|
|
155
205
|
async function getVideoJobStatus(options) {
|