Start here
Join a film's acts into one film
/v1/studio/projects/{project}/films/joinon https://api.auray.ai/v1Join a film's acts into one video of the project, for a story longer than one film may run — free, as an export is.
Join a film's acts into one video of the project, for a story longer than one film may run — free, as an export is. films names 2 to 6 of the project's films in the order they play, all laid out at one shape, size and rate; each is exported as it stands unless its newest export was made at its revision and its video still stands, or is on its way, and then their videos are joined into one MP4, each whole and as it was made — its picture copied, never made again, and its sound joined to the next with no gap — with a poster drawn from it, filed in the project's Film folder. A joined film runs at most 50 minutes. Answered 201 at once with the joined film — a version-2 film document (FilmDocV2) that says join, whatever doc_version says — and its newest export on its way: read that export until it is done (item_id is the video) or failed. It waits for the acts still being exported, for up to 2 hours; when an act's export fails, or a film of it is deleted or held while it waits, the joined film's export fails (source_missing, source_withheld), and film_id with the same acts makes it again, exporting those acts first. name names it (left out: the first act's name); film_id lays out a joined film of the project again from the films named now. Refused when a film named is not one of the project's (reference_invalid names it); when the acts are laid out at different shapes, sizes or rates, together run longer than 50 minutes, or one holds no clip or cannot be exported as it is (film_invalid); while a clip of an act is withheld or held after a report (content_held); while an export of the joined film is being made (film_export_running); when the project holds as many items as its limits.items says — its owner's plan's figure (studio_limit_reached); while video is paused (video_paused); and while Cinema Studio is closed to new projects and exports (studio_paused). Nothing is begun unless all of it is. Needs the edit_items capability. Takes a key holding studio:write — Account offers it unticked — or the desktop's own session key. Spends nothing.
#Authentication
Authorization: Bearer auray_sk_… — studio:write — Account offers it unticked; the desktop's session key holds it.
#Request
#Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
project | string | yes |
#Body
application/json, required.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
films | string[] | yes | — | The acts, in the order they play: 2 to 6 of the project's films, each once, all laid out at one shape, size and rate. |
name | string | no | — | The joined film's name, and its video's. Left out: the first act's name, or the joined film's own when film_id names one. 0 to 80 characters. |
film_id | string | no | — | A joined film of the project to lay out again from the films named now, at its next rev. Left out: a new film. |
#Example request
curl -X POST https://api.auray.ai/v1/studio/projects/3f0c8a52-6a1d-4e8b-9c2d-7e5f4a3b2c1d/films/join \
-H "Authorization: Bearer $AURAY_API_KEY" \
-H "content-type: application/json" \
-d '{
"films": [
"9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"0b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e"
],
"name": "The long way home"
}'const res = await fetch("https://api.auray.ai/v1/studio/projects/3f0c8a52-6a1d-4e8b-9c2d-7e5f4a3b2c1d/films/join", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AURAY_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"films": [
"9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"0b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e"
],
"name": "The long way home"
}),
});
console.log(await res.json());import os, requests
r = requests.post(
"https://api.auray.ai/v1/studio/projects/3f0c8a52-6a1d-4e8b-9c2d-7e5f4a3b2c1d/films/join",
headers={"Authorization": f"Bearer {os.environ['AURAY_API_KEY']}"},
json={
"films": [
"9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"0b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e"
],
"name": "The long way home"
},
)
r.raise_for_status()
print(r.json())package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func main() {
body := []byte(`{
"films": [
"9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"0b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e"
],
"name": "The long way home"
}`)
req, err := http.NewRequest("POST", "https://api.auray.ai/v1/studio/projects/3f0c8a52-6a1d-4e8b-9c2d-7e5f4a3b2c1d/films/join", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("AURAY_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}<?php
$ch = curl_init("https://api.auray.ai/v1/studio/projects/3f0c8a52-6a1d-4e8b-9c2d-7e5f4a3b2c1d/films/join");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("AURAY_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"films" => [
"9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"0b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
],
"name" => "The long way home",
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response, PHP_EOL;require "net/http"
require "json"
uri = URI("https://api.auray.ai/v1/studio/projects/3f0c8a52-6a1d-4e8b-9c2d-7e5f4a3b2c1d/films/join")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("AURAY_API_KEY")}"
req["Content-Type"] = "application/json"
req.body = {
"films" => [
"9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"0b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e"
],
"name" => "The long way home"
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.bodyimport java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Main {
public static void main(String[] args) throws Exception {
String body = """
{
"films": [
"9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"0b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e"
],
"name": "The long way home"
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.auray.ai/v1/studio/projects/3f0c8a52-6a1d-4e8b-9c2d-7e5f4a3b2c1d/films/join"))
.header("Authorization", "Bearer " + System.getenv("AURAY_API_KEY"))
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}using System.Net.Http.Headers;
using System.Text;
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AURAY_API_KEY"));
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.auray.ai/v1/studio/projects/3f0c8a52-6a1d-4e8b-9c2d-7e5f4a3b2c1d/films/join");
var body = """
{
"films": [
"9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"0b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e"
],
"name": "The long way home"
}
""";
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");#Responses
#201 Created
Created.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
object | string | yes | — | One of "film". |
id | string | yes | — | |
project_id | string | yes | — | |
name | string | yes | — | 0 to 80 characters. |
doc | FilmDoc or FilmDocV2 | yes | — | Version 1 (FilmDoc) unless the request says doc_version=2; then either version, as the film was saved. |
doc (FilmDoc).v | integer | yes | — | One of 1. |
doc (FilmDoc).aspect | string | yes | — | Across or up. Every clip is fitted into the frame, never stretched or cut. One of "16:9", "9:16". |
doc (FilmDoc).clips | object[] | yes | — | |
doc (FilmDoc).clips[].item_id | string | yes | — | A video item of the project, in lower case. |
doc (FilmDoc).clips[].in_ms | integer | yes | — | 0 to …. |
doc (FilmDoc).clips[].out_ms | integer | yes | — | … to 86400000. |
doc (FilmDoc).clips[].transition | string | yes | — | How it joins the clip before it: at once, or by a cross-fade of picture and sound. The first clip cuts in. One of "cut", "fade". |
doc (FilmDoc).clips[].fade_ms | integer | yes | — | 0 for a cut; 200 to 3000 for a fade, and at most half the shorter of the two clips it joins. Two clips joined by a fade overlap by its length. 0 to 3000. |
doc (FilmDoc).sound | object | yes | — | |
doc (FilmDoc).sound.clips | string | yes | — | The clips' own sound, kept or every clip silent. One of "keep", "mute". |
doc (FilmDoc).sound.music | null or object | yes | — | One music bed under the clips, from in_ms into the sound; one that runs on past the film's end fades out over its last moments, and one that ends sooner simply ends. Null for none. |
doc (FilmDocV2).v | integer | yes | — | One of 2. |
doc (FilmDocV2).aspect | string | yes | — | Its shape as W:H — 16:9 and 9:16, and any shape a video model makes (4:5, 1:1, 21:9…). Every clip is fitted into the frame, never stretched or cut. |
doc (FilmDocV2).canvas | object | no | — | Its frame, when it names one: its shape at a shorter side of its own, each side even and 256 to 4096 pixels (1080 × 1350 for a 4:5 cut at 1080). Left out: its shape at 1080 on the shorter side. Up to 1080 on the shorter side for any cut; above it — 4K — only for a cut that holds a clip made at 4K (2160 or more on its shorter side), and never above 2160: a frame its clips do not hold is refused film_invalid when the film is started, saved or exported. |
doc (FilmDocV2).canvas.width | integer | yes | — | 256 to 4096. |
doc (FilmDocV2).canvas.height | integer | yes | — | 256 to 4096. |
doc (FilmDocV2).fps | integer | no | — | Frames a second, when it names a rate: 12 to 60, each frame a whole number of sound samples at 48 kHz (24, 25, 30 and 60 among them). Left out: 24. 12 to 60. |
doc (FilmDocV2).clips | object[] | yes | — | |
doc (FilmDocV2).clips[].item_id | string | yes | — | A video item of the project, in lower case. |
doc (FilmDocV2).clips[].in_ms | integer | yes | — | 0 to …. |
doc (FilmDocV2).clips[].out_ms | integer | yes | — | … to 86400000. |
doc (FilmDocV2).clips[].transition | string | yes | — | How it joins the clip before it: at once, or by a cross-fade of picture and sound. The first clip cuts in. One of "cut", "fade". |
doc (FilmDocV2).clips[].fade_ms | integer | yes | — | 0 for a cut; 200 to 3000 for a fade, and at most half the shorter of the two clips it joins. Two clips joined by a fade overlap by its length. 0 to 3000. |
doc (FilmDocV2).clips[].gain_db | integer | yes | 0 | The clip's own sound, in whole decibels. -30 to 6. |
doc (FilmDocV2).clips[].mute | boolean | yes | — | The clip's own sound silenced. |
doc (FilmDocV2).clips[].audio_fade_ms | integer | yes | — | Null: its sound fades with its picture. Otherwise the sound's own fade, in milliseconds, at most half the clip. 0 to 3000. May be null. |
doc (FilmDocV2).clips[].audio_lead_ms | integer | yes | — | How far its sound starts before (negative) or after its picture, in milliseconds. -2000 to 2000. |
doc (FilmDocV2).clips[].quiet | object[] | no | — | Where its own sound is lowered by quiet_db: stretches of the clip's own file — milliseconds of the file, not of the film, so a trim or a move never takes one off the voice it lowers — each at least 100 ms long and after the one before it, with a short ramp at each edge. With quiet_db, both or neither; an empty list says the clip was listened to and held no voice to lower. A clip muted whole because a voice covers most of it keeps its stretches beside mute. A save that names none of a film's titles, quiet and loop over a film that names one is refused film_invalid — see titles. |
doc (FilmDocV2).clips[].quiet[].from_ms | integer | yes | — | 0 to 86400000. |
doc (FilmDocV2).clips[].quiet[].to_ms | integer | yes | — | 0 to 86400000. |
doc (FilmDocV2).clips[].quiet_db | integer | no | -30 | How far quiet lowers the clip's own sound, in whole decibels. 0 lowers nothing and keeps the stretches. -30 to 0. |
doc (FilmDocV2).tracks | object[] | yes | — | At most one track of each kind. |
doc (FilmDocV2).tracks[].kind | string | yes | — | Narration, dialogue lines, the music, or sound effects. One of "narration", "dialogue", "music", "sfx". |
doc (FilmDocV2).tracks[].gain_db | integer | yes | 0 | The whole track's level, in whole decibels. -30 to 6. |
doc (FilmDocV2).tracks[].items | object[] | yes | — | |
doc (FilmDocV2).tracks[].items[].item_id | string | yes | — | A sound item of the project, in lower case. |
doc (FilmDocV2).tracks[].items[].at_ms | integer | yes | — | Where in the film it starts. 0 to 600000. |
doc (FilmDocV2).tracks[].items[].in_ms | integer | yes | — | Where in the sound the part kept starts. 0 to …. |
doc (FilmDocV2).tracks[].items[].out_ms | integer | yes | — | Where it ends. … to 86400000. |
doc (FilmDocV2).tracks[].items[].gain_db | integer | yes | 0 | Its level, in whole decibels. -30 to 6. |
doc (FilmDocV2).tracks[].items[].fade_in_ms | integer | yes | — | 0 to …. |
doc (FilmDocV2).tracks[].items[].fade_out_ms | integer | yes | — | 0 to …. |
doc (FilmDocV2).tracks[].items[].shot | object | no | — | The planned shot it was laid for: a plan's id and the shot's number. |
doc (FilmDocV2).tracks[].items[].loop | boolean | no | — | Repeated to fill: the part of the sound from in_ms to its end plays, then plays again, each repeat blended into the next over 750 ms (or a quarter of the part, when that is shorter), until the length asked (out_ms − in_ms) is filled — so out_ms may run past the sound's end. A part shorter than 100 ms or longer than 2 minutes plays once. False or left out: it plays once and stops at the sound's end. A save that names none of a film's titles, quiet and loop over a film that names one is refused film_invalid — see titles. |
doc (FilmDocV2).duck | object | yes | — | The music lowered under the voices — under the narration, the dialogue or both, by how much, and how fast it goes down and comes back; null for nothing lowered. May be null. |
doc (FilmDocV2).duck.under | string[] | yes | — | The voices it is lowered under: narration, dialogue or both. |
doc (FilmDocV2).duck.depth_db | integer | yes | -12 | How far the music is lowered, in whole decibels. -24 to 0. |
doc (FilmDocV2).duck.attack_ms | integer | yes | 200 | How fast it goes down when a voice starts. 1 to 2000. |
doc (FilmDocV2).duck.release_ms | integer | yes | 600 | How fast it comes back when the voice ends. 1 to 9000. |
doc (FilmDocV2).loudness | object | yes | — | |
doc (FilmDocV2).loudness.target_lufs | integer | yes | -16 | How loud the film is made, in LUFS: -16 for streaming, -14 louder, -23 for broadcast. One of -16, -14, -23. |
doc (FilmDocV2).titles | object[] | no | — | Words drawn into the film when it is made: its name, an end card, a lower third, a line between scenes. Left out, the film has none. Once a film has the key, a save that leaves it out is refused film_invalid: send an empty list to take every title away. And a save over a film that names titles, a clip's quiet or a sound's loop must name one of the three — the whole film is read, not each clip or sound — or it is refused film_invalid as from an app that knows none of them: send titles: [] when nothing else of the three is left. |
doc (FilmDocV2).titles[].text | string | yes | — | Its words: at most 2 lines of 24 characters for a title, 1 of 36 for a lower third, 3 of 32 for text. 1 to 80 characters. |
doc (FilmDocV2).titles[].subtext | string | yes | — | A smaller line under its words — at most 3 lines of 48 characters under a title, 1 under a lower third, 2 under text — or null. 1 to 160 characters. May be null. |
doc (FilmDocV2).titles[].at_ms | integer | yes | — | Where in the film it appears. 0 to 600000. |
doc (FilmDocV2).titles[].ms | integer | yes | — | How long it shows. 500 to 30000. |
doc (FilmDocV2).titles[].style | string | yes | — | title: large, bold and centred — a film's name, an end card. lower_third: a line low on the left, as a name and its role are set. text: a few centred lines — a date, a place, a sentence between scenes. One of "title", "lower_third", "text". |
doc (FilmDocV2).titles[].backdrop | string | yes | — | What it stands on: the picture as it is, the picture darkened, or black — a title card. One of "none", "dim", "black". |
doc (FilmDocV2).titles[].font | string | yes | — | Noto Sans or Noto Serif. One of "sans", "serif". |
doc (FilmDocV2).titles[].fade_in_ms | integer | yes | — | 0 to 2000. |
doc (FilmDocV2).titles[].fade_out_ms | integer | yes | — | Its two fades, which together are no longer than it shows. 0 to 2000. |
doc (FilmDocV2).titles[].shot | object | no | — | The planned shot it follows: laid out again, it moves with that shot's clip. |
doc (FilmDocV2).titles[].shot.plan_id | string | yes | — | |
doc (FilmDocV2).titles[].shot.shot | integer | yes | — | 1 to 60. |
doc (FilmDocV2).titles[].end | boolean | no | — | It follows the film's end: laid out again, it ends where the film does. Never with shot. One of true. |
doc (FilmDocV2).join | boolean | no | — | A joined film, written by joinStudioFilms: its clips are 2 to 6 of the project's films, each whole and as it was made — in_ms 0, out_ms its length, a cut, at 0 dB, unmuted, with no fade, lead or quiet of its own — and it has no tracks, duck or titles of its own, at most 50 minutes in all. It is exported by joining them, never by making a frame again. Left out, a film is a cut. Once a film says it, a save that names none of join, titles, a clip's quiet and an item's loop is refused film_invalid. One of true. |
rev | integer | yes | — | A change names the rev it was read at. 1 to …. |
duration_ms | integer | yes | — | Every clip's kept length, less each fade. 0 to …. |
sources | object[] | yes | — | Every clip — and the music, or a track's sound — that cannot be exported as it is now; empty when nothing stops it. |
sources[].at | string | yes | — | A clip, or the track — by its kind — whose sound it is; music for a version-1 film's music too. One of "clip", "narration", "dialogue", "music", "sfx". |
sources[].index | integer | yes | — | The clip's place in doc.clips, or the sound's place on its track (the one of kind at in doc.tracks); null for a version-1 film's music. 0 to …. May be null. |
sources[].item_id | string | yes | — | |
sources[].problem | string | yes | — | Gone from the project, in the Trash, still being copied in, withheld after a report, held while a report is looked at, a clip that is no video, music that is no sound, no stored file, or a file larger than a film reads. One of "missing", "trashed", "pending", "withheld", "held", "not_video", "not_audio", "no_file", "too_large". |
exportable | boolean | yes | — | It holds a clip and nothing in sources stops it. |
latest_export | null or object | yes | — | Its newest export, whatever its state; null before the first. |
created_at | string | yes | — | |
updated_at | string | yes | — | |
dub_of | string | no | — | The film of the project this one is a second language of; null for a film of its own, and again once that film is deleted. May be null. |
language | string | no | — | A second language's tag (id, en, pt-BR); null for a film of its own. May be null. |
request_id | string | no | — | Mirrored in the X-Request-Id header. |
Headers:
| Header | Description |
|---|---|
RateLimit-Limit | Burst capacity. |
RateLimit-Remaining | |
RateLimit-Reset | Seconds until refill. |
X-RateLimit-Limit | |
X-RateLimit-Remaining | |
X-RateLimit-Reset | Unix seconds. The X- spelling is what client libraries read. |
X-Request-Id |
#400 Bad Request
#401 Unauthorized
#403 Forbidden
#404 Not Found
#409 Conflict
#413 Payload Too Large
#429 Too Many Requests
#500 Internal Server Error
#503 Service Unavailable
#Status codes
| Status | Meaning |
|---|---|
201 | Created. |
400 | Bad Request |
401 | Unauthorized |
403 | Forbidden |
404 | Not Found |
409 | Conflict |
413 | Payload Too Large |
429 | Too Many Requests |
500 | Internal Server Error |
503 | Service Unavailable |
Every refusal names its code, and every code is explained at Errors.