@transloadit/node 6.2.1 → 6.3.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.
Files changed (58) hide show
  1. package/README.md +10 -0
  2. package/dist/alphalib/types/robots/_html-pdf.d.ts +3 -0
  3. package/dist/alphalib/types/robots/_html-pdf.d.ts.map +1 -0
  4. package/dist/alphalib/types/robots/_html-pdf.js +3 -0
  5. package/dist/alphalib/types/robots/_html-pdf.js.map +1 -0
  6. package/dist/alphalib/types/robots/_video-generate-description.d.ts +3 -0
  7. package/dist/alphalib/types/robots/_video-generate-description.d.ts.map +1 -0
  8. package/dist/alphalib/types/robots/_video-generate-description.js +3 -0
  9. package/dist/alphalib/types/robots/_video-generate-description.js.map +1 -0
  10. package/dist/alphalib/types/robots/cloudflare-store.d.ts +12 -0
  11. package/dist/alphalib/types/robots/cloudflare-store.d.ts.map +1 -1
  12. package/dist/alphalib/types/robots/cloudflare-store.js +20 -0
  13. package/dist/alphalib/types/robots/cloudflare-store.js.map +1 -1
  14. package/dist/alphalib/types/robots/document-convert.d.ts.map +1 -1
  15. package/dist/alphalib/types/robots/document-convert.js +3 -0
  16. package/dist/alphalib/types/robots/document-convert.js.map +1 -1
  17. package/dist/alphalib/types/robots/html-convert.d.ts.map +1 -1
  18. package/dist/alphalib/types/robots/html-convert.js +3 -0
  19. package/dist/alphalib/types/robots/html-convert.js.map +1 -1
  20. package/dist/alphalib/types/robots/s3-store.d.ts +8 -8
  21. package/dist/alphalib/types/robots/s3-store.d.ts.map +1 -1
  22. package/dist/alphalib/types/robots/s3-store.js +27 -1
  23. package/dist/alphalib/types/robots/s3-store.js.map +1 -1
  24. package/dist/alphalib/types/robots/text-speak.d.ts.map +1 -1
  25. package/dist/alphalib/types/robots/text-speak.js +0 -24
  26. package/dist/alphalib/types/robots/text-speak.js.map +1 -1
  27. package/dist/alphalib/types/robots/text-translate.d.ts.map +1 -1
  28. package/dist/alphalib/types/robots/text-translate.js +0 -13
  29. package/dist/alphalib/types/robots/text-translate.js.map +1 -1
  30. package/dist/alphalib/types/robots/video-encode.d.ts.map +1 -1
  31. package/dist/alphalib/types/robots/video-encode.js +2 -0
  32. package/dist/alphalib/types/robots/video-encode.js.map +1 -1
  33. package/dist/alphalib/types/robots/video-generate.d.ts.map +1 -1
  34. package/dist/alphalib/types/robots/video-generate.js +15 -25
  35. package/dist/alphalib/types/robots/video-generate.js.map +1 -1
  36. package/dist/alphalib/types/robots/video-subtitle.d.ts.map +1 -1
  37. package/dist/alphalib/types/robots/video-subtitle.js +2 -0
  38. package/dist/alphalib/types/robots/video-subtitle.js.map +1 -1
  39. package/dist/alphalib/types/robots/video-thumbs.d.ts.map +1 -1
  40. package/dist/alphalib/types/robots/video-thumbs.js +2 -0
  41. package/dist/alphalib/types/robots/video-thumbs.js.map +1 -1
  42. package/dist/cli/commands/assemblies.d.ts.map +1 -1
  43. package/dist/cli/commands/assemblies.js +24 -14
  44. package/dist/cli/commands/assemblies.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/alphalib/types/robots/_html-pdf.ts +3 -0
  47. package/src/alphalib/types/robots/_video-generate-description.ts +2 -0
  48. package/src/alphalib/types/robots/cloudflare-store.ts +20 -0
  49. package/src/alphalib/types/robots/document-convert.ts +3 -0
  50. package/src/alphalib/types/robots/html-convert.ts +3 -0
  51. package/src/alphalib/types/robots/s3-store.ts +27 -1
  52. package/src/alphalib/types/robots/text-speak.ts +0 -24
  53. package/src/alphalib/types/robots/text-translate.ts +0 -13
  54. package/src/alphalib/types/robots/video-encode.ts +2 -0
  55. package/src/alphalib/types/robots/video-generate.ts +15 -27
  56. package/src/alphalib/types/robots/video-subtitle.ts +2 -0
  57. package/src/alphalib/types/robots/video-thumbs.ts +2 -0
  58. package/src/cli/commands/assemblies.ts +23 -14
@@ -0,0 +1,3 @@
1
+ /** Shared English contract for the HTML-to-PDF capture used by both conversion Robots. */
2
+ export const htmlPdfReadinessDescription =
3
+ 'For HTML-to-PDF output, printing starts once sampled visible frames, their documents, and their fonts have stayed ready for 500 ms. After navigation, PDF readiness has a 30-second cap; a page that does not become ready fails conversion. Hidden frames do not extend the ready interval. Content that first appears during the final sample window may be missed. Prepare dynamic content before capture.'
@@ -0,0 +1,2 @@
1
+ /** Scheduled end of the compatibility window for ignored video settings. */
2
+ export const videoGenerationParameterEnforcementDate = '2026-11-16'
@@ -24,6 +24,20 @@ export const meta: RobotMetaInput = {
24
24
  'Export uploaded files to `my_target_folder` in a Cloudflare R2 bucket:',
25
25
  extended_description: `
26
26
  The URL to the result file will be returned in the <dfn>Assembly Status JSON</dfn>.
27
+
28
+ ## Temporary credentials and permissions
29
+
30
+ You can use [temporary R2 credentials](https://developers.cloudflare.com/r2/api/s3/temporary-credentials/) by supplying the matching temporary \`key\`, \`secret\`, and \`session_token\`. Use the temporary secret returned or derived with the token, not the parent secret used to mint it. These values can also be stored together in Cloudflare <dfn>Template Credentials</dfn>. For per-Assembly credentials, send all three from your trusted backend; do not expose the parent credentials in browser instructions.
31
+
32
+ Credentials must remain valid through queuing, processing, upload retries, and verification. The Robot does not refresh them. Signed download URLs cannot outlive the credentials that sign them, even when \`sign_urls_for\` is longer.
33
+
34
+ When overriding a stored \`key\` or \`secret\` with a different value, supply the matching \`session_token\` inline too; the stored token will not be inherited. When updating Cloudflare Template Credentials, replacing either key or secret without a new token removes the old token. Omitting the token retains it only when the key and secret stay unchanged. You can also explicitly remove it with an empty string or \`null\`; empty tokens are treated as absent on creation too.
35
+
36
+ For locally signed credentials using explicit R2 \`actions\`, grant \`PutObject\` for small uploads and \`HeadObject\` for the Robot’s mandatory signed HEAD verification. Uploads larger than the default 5 MiB part size use multipart requests: also grant \`CreateMultipartUpload\`, \`UploadPart\`, \`CompleteMultipartUpload\`, and \`AbortMultipartUpload\` for failed-upload cleanup. Grant \`GetObject\` if consumers will use signed download URLs. These are R2 action names, not AWS IAM policy actions.
37
+
38
+ Scope \`paths.objectPaths\` to the exact destination object key and set \`path\` to that same key. A path ending in \`/\` appends a generated filename and will not match an exact-object grant. Use one output per exact-object key to avoid overwrites. No bucket listing or region-discovery permission is needed.
39
+
40
+ Strictly write-only credentials cannot pass this Robot’s HEAD verification. For uploads without read verification, use [/s3/store](/docs/robots/s3-store/) with the R2 configuration documented there. A presigned PUT URL is a different credential format and cannot be used as this Robot’s \`host\`.
27
41
  `,
28
42
  has_small_icon: true,
29
43
  purpose_sentence: 'exports encoding results to Cloudflare R2 buckets',
@@ -39,6 +53,12 @@ export const robotCloudflareStoreInstructionsSchema = robotBase
39
53
  .extend({
40
54
  robot: z.literal('/cloudflare/store'),
41
55
  path: storeFilePath,
56
+ session_token: z
57
+ .string()
58
+ .optional()
59
+ .describe(`
60
+ The session token belonging to the temporary Cloudflare R2 access key ID and secret access key supplied for this upload. It can also be supplied inside Template Credentials. The credentials must remain valid through upload and signed HEAD verification; the Robot does not refresh them.
61
+ `),
42
62
  headers: z
43
63
  .record(z.string())
44
64
  .default({ 'Content-Type': '${file.mime}' })
@@ -11,6 +11,7 @@ import {
11
11
  documentConvertLoadInternalErrorReason,
12
12
  documentConvertLoadUserErrorReason,
13
13
  } from '../responseErrors/DocumentConvertRobot.ts'
14
+ import { htmlPdfReadinessDescription } from './_html-pdf.ts'
14
15
  import {
15
16
  createProcessingExample,
16
17
  defineRobot,
@@ -119,6 +120,8 @@ export const robotDocumentConvertInstructionsSchema = robotBase
119
120
  .merge(robotUse)
120
121
  .extend({
121
122
  robot: z.literal('/document/convert').describe(`
123
+ ${htmlPdfReadinessDescription}
124
+
122
125
  > [!Note]
123
126
  > This Robot can convert files to PDF, but cannot convert PDFs to different formats. If you want to convert PDFs to say, JPEG or TIFF, use [🤖/image/resize](/docs/robots/image-resize/). If you want to turn them into text files or recognize (OCR) them to make them searchable, reach out, as we have a new Robot in the works for this.
124
127
 
@@ -6,6 +6,7 @@ import type {
6
6
 
7
7
  import { z } from 'zod'
8
8
 
9
+ import { htmlPdfReadinessDescription } from './_html-pdf.ts'
9
10
  import {
10
11
  defineRobotWithHiddenFields,
11
12
  robotBase,
@@ -50,6 +51,8 @@ export const robotHtmlConvertInstructionsSchema = robotBase
50
51
  A URL can be provided instead of an input HTML file, to capture a screenshot from the website referenced by the URL.
51
52
 
52
53
  Use [🤖/image/resize](/docs/robots/image-resize/) to resize or crop the screenshot as needed.
54
+
55
+ ${htmlPdfReadinessDescription}
53
56
  `),
54
57
  url: z
55
58
  .string()
@@ -36,6 +36,32 @@ export const meta: RobotMetaInput = {
36
36
  ...robotStoreMeta,
37
37
  example_code: createStorageStoreExample('/s3/store', 'YOUR_AWS_CREDENTIALS'),
38
38
  example_code_description: 'Export uploaded files to `my_target_folder` in an S3 bucket:',
39
+ extended_description: `
40
+ ## Export to R2 with temporary credentials
41
+
42
+ To upload one output to an exact R2 object key, send the matching temporary credentials from your trusted backend:
43
+
44
+ \`\`\`json
45
+ {
46
+ "robot": "/s3/store",
47
+ "use": ":original",
48
+ "host": "https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com",
49
+ "bucket": "my-bucket",
50
+ "bucket_region": "auto",
51
+ "acl": "bucket-default",
52
+ "key": "TEMPORARY_ACCESS_KEY_ID",
53
+ "secret": "TEMPORARY_SECRET_ACCESS_KEY",
54
+ "session_token": "TEMPORARY_SESSION_TOKEN",
55
+ "path": "exports/result.jpg"
56
+ }
57
+ \`\`\`
58
+
59
+ The HTTPS host enables TLS, \`bucket_region: "auto"\` avoids region discovery, and \`acl: "bucket-default"\` omits the generated ACL header. Do not add ACL or grant headers. Scope the [R2 temporary credentials](https://developers.cloudflare.com/r2/api/s3/temporary-credentials/) to the same bucket and exact object key, here \`exports/result.jpg\`. Do not end \`path\` with \`/\`: that appends a generated filename. Export one output per key to avoid overwrites.
60
+
61
+ For locally signed R2 credentials, \`actions: ["PutObject"]\` permits small single-request uploads with this configuration. Uploads larger than the default 5 MiB part size also require \`CreateMultipartUpload\`, \`UploadPart\`, \`CompleteMultipartUpload\`, and \`AbortMultipartUpload\` for failed-upload cleanup. These are R2 action names, not AWS IAM policy actions. No listing, region discovery, HEAD, or GET is required by this upload configuration. Unlike [/cloudflare/store](/docs/robots/cloudflare-store/), this Robot does not perform HEAD verification after upload.
62
+
63
+ Credentials must remain valid through queuing, processing, and upload retries; the Robot does not refresh them. A returned object URL does not grant read access. If you request signed download URLs with \`sign_urls_for\`, add \`GetObject\` permission; those URLs also expire when the temporary credentials expire. A presigned PUT URL cannot be used as \`host\`.
64
+ `,
39
65
  has_small_icon: true,
40
66
  purpose_sentence: 'exports encoding results to Amazon S3',
41
67
  purpose_word: 'Amazon S3',
@@ -155,7 +181,7 @@ This parameter provides signed URLs in the result JSON (in the \`signed_url\` an
155
181
  .string()
156
182
  .optional()
157
183
  .describe(`
158
- The session token belonging to the temporary AWS access key ID and secret access key supplied for this upload. The Robot does not assume a role or refresh these credentials; they must remain valid for the upload.
184
+ The session token belonging to the temporary access key ID and secret access key supplied for this upload to S3 or a compatible service such as R2. The Robot does not assume a role or refresh these credentials; they must remain valid for the upload.
159
185
  `),
160
186
  })
161
187
  .strict()
@@ -28,30 +28,6 @@ export const meta: RobotMetaInput = {
28
28
  extended_description: `
29
29
  > [!Warning]
30
30
  > Transloadit aims to be deterministic, but this <dfn>Robot</dfn> uses third-party AI services. The providers (AWS, GCP) will evolve their models over time, giving different responses for the same input media. Avoid relying on exact responses in your tests and application.
31
-
32
- ## Supported languages and voices
33
-
34
- {% for provider in text_speak_voices %}
35
-
36
- ### {{provider[0] | upcase }}
37
-
38
- <table class="table">
39
- <thead>
40
- <tr>
41
- <th>Language</th>
42
- <th>Voices</th>
43
- </tr>
44
- </thead>
45
- <tbody>
46
- {%- for language in provider[1] %}
47
- <tr>
48
- <td><strong>{{language[0]}}</strong></td>
49
- <td>{{ language[1] | join: ", " }}</td>
50
- </tr>
51
- {%- endfor %}
52
- </tbody>
53
- </table>
54
- {% endfor %}
55
31
  `,
56
32
  override_lvl1: 'Artificial Intelligence',
57
33
  purpose_sentence: 'synthesizes speech in documents',
@@ -26,19 +26,6 @@ export const meta: RobotMetaInput = {
26
26
  extended_description: `
27
27
  > [!Warning]
28
28
  > This <dfn>Robot</dfn> uses third-party AI services. They may tweak their models over time, giving different responses for the same input media. Avoid relying on exact responses in your tests and application.
29
-
30
- ## Supported languages
31
-
32
- {%- for provider in text_translate_languages %}
33
-
34
- ### {{provider[0] | upcase }}
35
-
36
- <!-- prettier-ignore -->
37
- {%- for language in provider[1] %}
38
- {{ language }}{% unless forloop.last %},{% endunless %}
39
- {%- endfor %}
40
-
41
- {%- endfor %}
42
29
  `,
43
30
  override_lvl1: 'Artificial Intelligence',
44
31
  purpose_sentence: 'translates text in documents',
@@ -45,6 +45,8 @@ Videos recorded with iPhone spatial audio can contain a stereo AAC track alongsi
45
45
 
46
46
  Files containing only unsupported audio still fail. Individual audio track selections and stream-copy requests keep the tracks you select; the output format must support copied tracks.
47
47
 
48
+ When adding watermarks or drawing with simple FFmpeg video filters such as \`drawtext\` to HLG or PQ HDR video, the video is tone-mapped to SDR BT.709 before drawing, so overlay colors retain their intended RGB values. SDR inputs keep their existing processing. A custom \`ffmpeg.pre:filter:v\` that supplies tone mapping controls its own color conversion. Custom multi-input \`filter_complex\` graphs must handle each input’s color space explicitly.
49
+
48
50
  ## Adding text overlays with FFmpeg
49
51
 
50
52
  You can add text overlays to videos using FFmpeg's \`drawtext\` filter through this <dfn>Robot</dfn>'s \`ffmpeg\` parameter. Here are two examples — one with the default font and one with a custom font family name:
@@ -11,6 +11,7 @@ import {
11
11
  robotParameterDocs,
12
12
  robotUse,
13
13
  } from './_instructions-primitives.ts'
14
+ import { videoGenerationParameterEnforcementDate } from './_video-generate-description.ts'
14
15
 
15
16
  export const meta: RobotMetaInput = {
16
17
  ...robotArtificialIntelligenceMeta,
@@ -19,8 +20,6 @@ export const meta: RobotMetaInput = {
19
20
  generated_video: {
20
21
  robot: '/video/generate',
21
22
  prompt: 'A slow cinematic drone shot of ocean cliffs at sunrise, realistic lighting.',
22
- duration: 5,
23
- aspect_ratio: '16:9',
24
23
  format: 'mp4',
25
24
  },
26
25
  },
@@ -46,15 +45,18 @@ export const robotVideoGenerateInstructionsSchema = robotBase
46
45
  .string()
47
46
  .optional()
48
47
  .describe(
49
- `The AI model to use for video generation. Defaults to ${videoGenerateDefaultModelIdentifier}.`,
48
+ `Video model; defaults to ${videoGenerateDefaultModelIdentifier}. Unsupported settings warn until ${videoGenerationParameterEnforcementDate}. MiniMax outputs are about six seconds.`,
50
49
  ),
51
50
  prompt: z.string().describe('The prompt describing the desired video content.'),
52
51
  format: z.enum(['mp4']).optional().describe('Format of the generated video.'),
53
52
  seed: z.number().optional().describe(robotParameterDocs.seed.description),
54
- aspect_ratio: z.string().optional().describe('Aspect ratio of the generated video.'),
55
- height: z.number().optional().describe('Height of the generated video.'),
56
- width: z.number().optional().describe('Width of the generated video.'),
57
- style: z.string().optional().describe('Style of the generated video.'),
53
+ aspect_ratio: z
54
+ .string()
55
+ .optional()
56
+ .describe('Aspect ratio for lightricks/ltx-video without an input image.'),
57
+ height: z.number().optional().describe('Pixel height for tencent/hunyuan-video.'),
58
+ width: z.number().optional().describe('Pixel width for tencent/hunyuan-video.'),
59
+ style: z.string().optional().describe('Unsupported by current models; omit it.'),
58
60
  num_outputs: z
59
61
  .number()
60
62
  .int()
@@ -62,26 +64,12 @@ export const robotVideoGenerateInstructionsSchema = robotBase
62
64
  .max(10)
63
65
  .optional()
64
66
  .describe('Number of video variants to generate.'),
65
- duration: z.number().optional().describe('Duration of the generated video in seconds.'),
66
- fps: z.number().optional().describe('Frames per second of the output video.'),
67
- motion_amount: z
68
- .number()
69
- .optional()
70
- .describe('Controls the intensity of motion in the generated video.'),
71
- camera_motion: z
72
- .string()
73
- .optional()
74
- .describe(
75
- 'Camera movement type (e.g., pan-left, pan-right, zoom-in, zoom-out, orbit, static, dolly, crane).',
76
- ),
77
- negative_prompt: z
78
- .string()
79
- .optional()
80
- .describe('Describes what should be avoided in the generated video.'),
81
- reference_strength: z
82
- .number()
83
- .optional()
84
- .describe('How closely the output should follow the reference input (0.0 - 1.0).'),
67
+ duration: z.number().optional().describe('Unsupported by current models; omit it.'),
68
+ fps: z.number().optional().describe('Frames per second for tencent/hunyuan-video.'),
69
+ motion_amount: z.number().optional().describe('Unsupported by current models; omit it.'),
70
+ camera_motion: z.string().optional().describe('Unsupported by current models; omit it.'),
71
+ negative_prompt: z.string().optional().describe('Content to avoid (lightricks/ltx-video).'),
72
+ reference_strength: z.number().optional().describe('Unsupported by current models; omit it.'),
85
73
  })
86
74
  .strict()
87
75
 
@@ -77,6 +77,8 @@ export const robotVideoSubtitleInstructionsSchema = robotBase
77
77
  This <dfn>Robot</dfn> supports SRT, VTT, ASS, and the timestamped JSON output of \`/speech/transcribe\` with \`"format": "json"\`. Pass the video as \`"video"\` and the transcript as \`"subtitles"\` in the \`use\` parameter. JSON words are grouped into captions and converted to ASS for burned word highlighting, or plain SRT cues for external subtitle streams. External JSON cues preserve casing and punctuation. Provider segments do not control caption breaks. SRT and VTT timing is preserved.
78
78
 
79
79
  Without a \`preset\` or custom \`ffmpeg\` options, the legacy \`"flash"\` preset produces FLV output. With this default, burned subtitles use MP3 audio; external subtitles copy the input audio. Set \`"preset": "empty"\` to keep the input container, such as MP4 for an MP4 input.
80
+
81
+ When burning subtitles into HLG or PQ HDR video, the video is tone-mapped to SDR BT.709 before captions are drawn. Caption colors retain their intended RGB values, and the output is tagged as SDR. External subtitles preserve the source video’s HDR colors. SDR inputs keep their existing processing. If \`ffmpeg.pre:filter:v\` supplies its own tone mapping, that filter controls color conversion.
80
82
  `),
81
83
  subtitles_type: zodInputPreservingTransform(z.enum(['burned', 'external', 'burn']), (val) =>
82
84
  val === 'burn' ? 'burned' : val,
@@ -70,6 +70,8 @@ export const robotVideoThumbsInstructionsSchema = robotBase
70
70
  robot: z.literal('/video/thumbs').describe(`
71
71
  Set \`smart: true\` to select strong preview images with AI instead of taking frames only at regular intervals. The Robot scores candidate frames for clarity, brightness, composition, faces, expressions, action, and visual interest, then returns the best \`count\` frames in chronological order. Smart results include \`file.meta.smart_score\` and \`file.meta.smart_reasons\`. If AI scoring is unavailable, the Assembly continues with the candidate frames in fallback order and includes a warning. No AI credentials are required.
72
72
 
73
+ HLG and PQ HDR video frames are tone-mapped to SDR sRGB before image encoding, including smart-thumbnail candidates. This preserves the intended appearance in browsers that display thumbnails as sRGB images. SDR inputs keep their existing processing.
74
+
73
75
  ## AI pricing
74
76
 
75
77
  Regular \`/video/thumbs\` processing charges still apply. AI frame analysis is billed separately at the underlying provider cost plus a ${smartThumbnailMarkupPercent}% Transloadit markup. Smart selection uses the dedicated \`${smartThumbnailModel}\` vision model with \`${smartThumbnailReasoningEffort}\` reasoning, independently of the \`/ai/chat\` default. The exact AI charge varies with the number of candidate frames and provider pricing.
@@ -484,12 +484,16 @@ function getJobInputPath(filepath: string): string {
484
484
  return normalizedFile
485
485
  }
486
486
 
487
- function createInputUploadStream(filepath: string): Readable {
488
- const instream = fs.createReadStream(filepath)
489
- // Attach a no-op error handler to prevent unhandled errors if stream is destroyed
490
- // before being consumed (e.g., due to output collision detection)
491
- instream.on('error', () => {})
492
- return instream
487
+ function hasPipedStdin(): boolean {
488
+ if (process.stdin.isTTY) return false
489
+
490
+ try {
491
+ const stats = fs.fstatSync(process.stdin.fd)
492
+ return stats.isFIFO() || stats.isFile()
493
+ } catch {
494
+ // A closed or unavailable descriptor is not an implicit upload.
495
+ return false
496
+ }
493
497
  }
494
498
 
495
499
  function createOutputPlan(pathname: string | undefined, mtime: Date): OutputPlan {
@@ -747,7 +751,7 @@ async function resolveResultDownloadTargets({
747
751
  return await buildDirectoryDownloadTargets({
748
752
  allFiles,
749
753
  baseDir: outputRoot,
750
- groupByStep: false,
754
+ groupByStep: inputs.length === 0,
751
755
  reservedPaths,
752
756
  })
753
757
  }
@@ -756,7 +760,7 @@ async function resolveResultDownloadTargets({
756
760
  return await buildDirectoryDownloadTargets({
757
761
  allFiles,
758
762
  baseDir: resolveDirectoryBaseDir(),
759
- groupByStep: entries.length > 1,
763
+ groupByStep: inPath == null || entries.length > 1,
760
764
  reservedPaths,
761
765
  })
762
766
  }
@@ -1592,7 +1596,7 @@ export async function create(
1592
1596
  const uploads =
1593
1597
  inPath === stdinWithPath.path
1594
1598
  ? {
1595
- in: createInputUploadStream(inPath),
1599
+ in: process.stdin,
1596
1600
  }
1597
1601
  : undefined
1598
1602
 
@@ -1660,7 +1664,7 @@ export async function create(
1660
1664
  scope: collection,
1661
1665
  })
1662
1666
  if (inPath === stdinWithPath.path) {
1663
- uploads[key] = createInputUploadStream(inPath)
1667
+ uploads[key] = process.stdin
1664
1668
  } else {
1665
1669
  files[key] = inPath
1666
1670
  }
@@ -1802,6 +1806,8 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
1802
1806
  details: `
1803
1807
  Create assemblies to process media files using Transloadit.
1804
1808
  You must specify either --steps or --template.
1809
+ Without uploads, directory outputs are saved as <output>/<step>/<result name>.
1810
+ Stdin is read with --input -, or implicitly with --steps when piped or redirected from a file.
1805
1811
  `,
1806
1812
  examples: [
1807
1813
  [
@@ -1812,6 +1818,10 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
1812
1818
  'Process with a template',
1813
1819
  'transloadit assemblies create --template TEMPLATE_ID -i input.jpg -o output/',
1814
1820
  ],
1821
+ [
1822
+ 'Download results from import-only or prompt-only steps',
1823
+ 'transloadit assemblies create --steps steps.json -o output/',
1824
+ ],
1815
1825
  [
1816
1826
  'Watch for changes',
1817
1827
  'transloadit assemblies create --steps steps.json -i input/ -o output/ --watch',
@@ -1864,9 +1874,8 @@ export class AssembliesCreateCommand extends AuthenticatedCommand {
1864
1874
 
1865
1875
  const inputList = this.inputs ?? []
1866
1876
 
1867
- // Default to stdin only for `--steps` mode (common "pipe a file into a one-off assembly" use case).
1868
- // For `--template` mode, templates may be inputless or use /http/import, so stdin should be explicit (`--input -`).
1869
- if (this.steps && inputList.length === 0 && !process.stdin.isTTY) {
1877
+ // Templates keep requiring explicit stdin; steps also accept pipes and redirected files.
1878
+ if (this.steps && inputList.length === 0 && hasPipedStdin()) {
1870
1879
  inputList.push('-')
1871
1880
  }
1872
1881
 
@@ -1975,7 +1984,7 @@ export class RunCommand extends AuthenticatedCommand {
1975
1984
  protected async run(): Promise<number | undefined> {
1976
1985
  const inputList = this.inputs ?? []
1977
1986
 
1978
- if (inputList.length === 0 && !process.stdin.isTTY) {
1987
+ if (inputList.length === 0 && hasPipedStdin()) {
1979
1988
  inputList.push('-')
1980
1989
  }
1981
1990