메인 콘텐츠로 건너뛰기

Overview

This tutorial walks you through the complete process of uploading an audio file, getting it transcribed, and translated into multiple languages using the Tiro API.

Prerequisites

  • Valid Tiro API key
  • Audio file (MP3, WAV, M4A)
  • Max file size: 500MB
  • Max duration: 4 hours

Step 1: Create a Voice File Job

Start by creating a job with your desired transcription and translation settings:
curl -X POST https://api.tiro.ooo/v1/external/voice-file/jobs \
  -H "Authorization: Bearer $TIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transcriptLocaleHints": ["ko_KR"],
    "translationLocales": ["en_US"]
  }'
async function createVoiceFileJob() {
  const response = await fetch('https://api.tiro.ooo/v1/external/voice-file/jobs', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.TIRO_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      transcriptLocaleHints: ['ko_KR'],
      translationLocales: ['en_US']
    })
  });
  
  if (!response.ok) {
    throw new Error(`Failed to create job: ${response.status}`);
  }
  
  const job = await response.json();
  console.log('Job created:', job.id);
  console.log('Upload URL:', job.uploadUri);
  
  return job;
}
import requests
import os

def create_voice_file_job():
    response = requests.post(
        'https://api.tiro.ooo/v1/external/voice-file/jobs',
        headers={
            'Authorization': f'Bearer {os.environ["TIRO_API_KEY"]}',
            'Content-Type': 'application/json'
        },
        json={
            'transcriptLocaleHints': ['ko_KR'],
            'translationLocales': ['en_US']
        }
    )
    
    response.raise_for_status()
    job = response.json()
    
    print(f'Job created: {job["id"]}')
    print(f'Upload URL: {job["uploadUri"]}')
    
    return job
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

type CreateJobRequest struct {
    TranscriptLocaleHints []string `json:"transcriptLocaleHints"`
    TranslationLocales    []string `json:"translationLocales"`
}

type CreateJobResponse struct {
    ID        string `json:"id"`
    UploadUri string `json:"uploadUri"`
}

func createVoiceFileJob() (*CreateJobResponse, error) {
    reqBody := CreateJobRequest{
        TranscriptLocaleHints: []string{"ko_KR"},
        TranslationLocales:    []string{"en_US"},
    }
    
    jsonBody, _ := json.Marshal(reqBody)
    
    req, _ := http.NewRequest("POST", "https://api.tiro.ooo/v1/external/voice-file/jobs", bytes.NewBuffer(jsonBody))
    req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("TIRO_API_KEY")))
    req.Header.Set("Content-Type", "application/json")
    
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    var job CreateJobResponse
    json.NewDecoder(resp.Body).Decode(&job)
    
    fmt.Printf("Job created: %s\n", job.ID)
    fmt.Printf("Upload URL: %s\n", job.UploadUri)
    
    return &job, nil
}
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.*
import org.springframework.web.client.RestTemplate

data class CreateJobRequest(
    val transcriptLocaleHints: List<String>,
    val translationLocales: List<String>
)

data class CreateJobResponse(
    val id: String,
    val uploadUri: String
)

@Service
class VoiceFileService {
    
    @Value("\${tiro.api.key}")
    private lateinit var apiKey: String
    
    private val restTemplate = RestTemplate()
    
    fun createVoiceFileJob(): CreateJobResponse {
        val headers = HttpHeaders().apply {
            set("Authorization", "Bearer $apiKey")
            contentType = MediaType.APPLICATION_JSON
        }
        
        val request = CreateJobRequest(
            transcriptLocaleHints = listOf("ko_KR"),
            translationLocales = listOf("en_US")
        )
        
        val entity = HttpEntity(request, headers)
        
        val response = restTemplate.exchange(
            "https://api.tiro.ooo/v1/external/voice-file/jobs",
            HttpMethod.POST,
            entity,
            CreateJobResponse::class.java
        )
        
        val job = response.body!!
        println("Job created: ${job.id}")
        println("Upload URL: ${job.uploadUri}")
        
        return job
    }
}
Example Response:
{
  "id": "b2d1ab32-3fe2-4201-b0b4-391abdbaa023",
  "uploadUri": "https://storage.example.com/upload/signed-url"
}

Step 2: Upload Your Audio File

Upload your audio file to the provided signed URL:
# Upload audio file to the signed URL (replace UPLOAD_URI with the uploadUri from Step 1)
curl -X PUT "$UPLOAD_URI" \
  -H "Content-Type: audio/mpeg" \
  --data-binary @/path/to/your/audio.mp3
async function uploadAudioFile(uploadUri, audioFile) {
  const response = await fetch(uploadUri, {
    method: 'PUT',
    body: audioFile,
    headers: {
      'Content-Type': audioFile.type || 'audio/mpeg'
    }
  });
  
  if (!response.ok) {
    throw new Error(`Upload failed: ${response.status}`);
  }
  
  console.log('File uploaded successfully');
}

// For Node.js with file system
const fs = require('fs');

async function uploadAudioFileNode(uploadUri, filePath) {
  const fileBuffer = fs.readFileSync(filePath);
  
  const response = await fetch(uploadUri, {
    method: 'PUT',
    body: fileBuffer,
    headers: {
      'Content-Type': 'audio/mpeg'
    }
  });
  
  if (!response.ok) {
    throw new Error(`Upload failed: ${response.status}`);
  }
  
  console.log('File uploaded successfully');
}
def upload_audio_file(upload_uri, file_path):
    with open(file_path, 'rb') as audio_file:
        response = requests.put(
            upload_uri,
            data=audio_file,
            headers={'Content-Type': 'audio/mpeg'}
        )
    
    response.raise_for_status()
    print('File uploaded successfully')
func uploadAudioFile(uploadUri, filePath string) error {
    file, err := os.Open(filePath)
    if err != nil {
        return err
    }
    defer file.Close()
    
    req, _ := http.NewRequest("PUT", uploadUri, file)
    req.Header.Set("Content-Type", "audio/mpeg")
    
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("upload failed: %d", resp.StatusCode)
    }
    
    fmt.Println("File uploaded successfully")
    return nil
}
fun uploadAudioFile(uploadUri: String, filePath: String) {
    val file = File(filePath)
    val headers = HttpHeaders().apply {
        contentType = MediaType.parseMediaType("audio/mpeg")
    }
    
    val resource = FileSystemResource(file)
    val entity = HttpEntity(resource, headers)
    
    val response = restTemplate.exchange(
        uploadUri,
        HttpMethod.PUT,
        entity,
        String::class.java
    )
    
    if (response.statusCode.is2xxSuccessful) {
        println("File uploaded successfully")
    } else {
        throw RuntimeException("Upload failed: ${response.statusCode}")
    }
}

Step 3: Notify Upload Completion

Tell the API that the upload is complete:
curl -X PUT "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/upload-complete" \
  -H "Authorization: Bearer $TIRO_API_KEY" \
  -H "Content-Type: application/json"
async function notifyUploadComplete(jobId) {
  const response = await fetch(
    `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}/upload-complete`,
    {
      method: 'PUT',
      headers: {
        'Authorization': `Bearer ${process.env.TIRO_API_KEY}`,
        'Content-Type': 'application/json'
      }
    }
  );
  
  if (!response.ok) {
    throw new Error(`Failed to notify upload: ${response.status}`);
  }
  
  console.log('Upload notification sent');
}
def notify_upload_complete(job_id):
    response = requests.put(
        f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}/upload-complete',
        headers={
            'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}',
            'Content-Type': 'application/json'
        }
    )
    
    response.raise_for_status()
    print('Upload notification sent')
func notifyUploadComplete(jobId string) error {
    url := fmt.Sprintf("https://api.tiro.ooo/v1/external/voice-file/jobs/%s/upload-complete", jobId)
    
    req, _ := http.NewRequest("PUT", url, nil)
    req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("TIRO_API_KEY")))
    req.Header.Set("Content-Type", "application/json")
    
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    fmt.Println("Upload notification sent")
    return nil
}
fun notifyUploadComplete(jobId: String) {
    val headers = HttpHeaders().apply {
        set("Authorization", "Bearer $apiKey")
        contentType = MediaType.APPLICATION_JSON
    }
    
    val entity = HttpEntity<String>(headers)
    
    restTemplate.exchange(
        "https://api.tiro.ooo/v1/external/voice-file/jobs/$jobId/upload-complete",
        HttpMethod.PUT,
        entity,
        String::class.java
    )
    
    println("Upload notification sent")
}

Step 4: Poll for Job Completion

Monitor the job status until processing is complete:
# Check job status (poll until status is COMPLETED or FAILED)
curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID" \
  -H "Authorization: Bearer $TIRO_API_KEY"

# Example response:
# {
#   "id": "job_abc123",
#   "status": "COMPLETED",
#   "fileUploadedAt": "2024-01-01T12:00:20Z",
#   "processStartedAt": "2024-01-01T12:01:00Z",
#   "processCompletedAt": "2024-01-01T12:02:30Z"
# }
async function waitForJobCompletion(jobId) {
  let delay = 2000; // Start with 2 seconds
  const maxDelay = 30000; // Max 30 seconds
  const maxAttempts = 60; // 30 minutes max
  let attempts = 0;
  
  while (attempts < maxAttempts) {
    try {
      const response = await fetch(
        `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}`,
        {
          headers: {
            'Authorization': `Bearer ${process.env.TIRO_API_KEY}`
          }
        }
      );
      
      if (!response.ok) {
        throw new Error(`Failed to get job status: ${response.status}`);
      }
      
      const job = await response.json();
      console.log(`Job status: ${job.status}`);
      
      if (job.status === 'COMPLETED') {
        console.log('Job completed successfully!');
        return job;
      }
      
      if (job.status === 'FAILED') {
        throw new Error(`Job failed: ${job.errorMessage || 'Unknown error'}`);
      }
      
      // Still processing - wait with exponential backoff
      await new Promise(resolve => setTimeout(resolve, delay));
      delay = Math.min(delay * 1.2, maxDelay);
      attempts++;
      
    } catch (error) {
      console.error('Error checking job status:', error);
      attempts++;
      await new Promise(resolve => setTimeout(resolve, delay));
      delay = Math.min(delay * 1.5, maxDelay);
    }
  }
  
  throw new Error('Job did not complete within timeout');
}
import time
import asyncio

async def wait_for_job_completion(job_id):
    delay = 2  # Start with 2 seconds
    max_delay = 30  # Max 30 seconds
    max_attempts = 60  # 30 minutes max
    attempts = 0
    
    while attempts < max_attempts:
        try:
            response = requests.get(
                f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}',
                headers={
                    'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}'
                }
            )
            
            response.raise_for_status()
            job = response.json()
            print(f'Job status: {job["status"]}')
            
            if job['status'] == 'COMPLETED':
                print('Job completed successfully!')
                return job
                
            if job['status'] == 'FAILED':
                error_msg = job.get('errorMessage', 'Unknown error')
                raise Exception(f'Job failed: {error_msg}')
                
            # Still processing - wait with exponential backoff
            await asyncio.sleep(delay)
            delay = min(delay * 1.2, max_delay)
            attempts += 1
            
        except Exception as e:
            print(f'Error checking job status: {e}')
            attempts += 1
            await asyncio.sleep(delay)
            delay = min(delay * 1.5, max_delay)
    
    raise Exception('Job did not complete within timeout')
type JobStatus struct {
    ID        string `json:"id"`
    Status    string `json:"status"`
    ErrorMessage string `json:"errorMessage,omitempty"`
}

func waitForJobCompletion(jobId string) (*JobStatus, error) {
    delay := 2 * time.Second
    maxDelay := 30 * time.Second
    maxAttempts := 60
    
    client := &http.Client{}
    url := fmt.Sprintf("https://api.tiro.ooo/v1/external/voice-file/jobs/%s", jobId)
    
    for attempts := 0; attempts < maxAttempts; attempts++ {
        req, _ := http.NewRequest("GET", url, nil)
        req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("TIRO_API_KEY")))
        
        resp, err := client.Do(req)
        if err != nil {
            time.Sleep(delay)
            delay = time.Duration(float64(delay) * 1.5)
            if delay > maxDelay {
                delay = maxDelay
            }
            continue
        }
        
        var job JobStatus
        json.NewDecoder(resp.Body).Decode(&job)
        resp.Body.Close()
        
        fmt.Printf("Job status: %s\n", job.Status)
        
        if job.Status == "COMPLETED" {
            fmt.Println("Job completed successfully!")
            return &job, nil
        }
        
        if job.Status == "FAILED" {
            return nil, fmt.Errorf("job failed: %s", job.ErrorMessage)
        }
        
        time.Sleep(delay)
        delay = time.Duration(float64(delay) * 1.2)
        if delay > maxDelay {
            delay = maxDelay
        }
    }
    
    return nil, fmt.Errorf("job did not complete within timeout")
}
import java.util.concurrent.TimeUnit

data class JobStatus(
    val id: String,
    val status: String,
    val errorMessage: String? = null
)

fun waitForJobCompletion(jobId: String): JobStatus {
    var delay = 2000L // 2 seconds
    val maxDelay = 30000L // 30 seconds
    val maxAttempts = 60
    
    repeat(maxAttempts) { attempt ->
        try {
            val headers = HttpHeaders().apply {
                set("Authorization", "Bearer $apiKey")
            }
            
            val entity = HttpEntity<String>(headers)
            
            val response = restTemplate.exchange(
                "https://api.tiro.ooo/v1/external/voice-file/jobs/$jobId",
                HttpMethod.GET,
                entity,
                JobStatus::class.java
            )
            
            val job = response.body!!
            println("Job status: ${job.status}")
            
            when (job.status) {
                "COMPLETED" -> {
                    println("Job completed successfully!")
                    return job
                }
                "FAILED" -> {
                    throw RuntimeException("Job failed: ${job.errorMessage}")
                }
            }
            
            TimeUnit.MILLISECONDS.sleep(delay)
            delay = minOf((delay * 1.2).toLong(), maxDelay)
            
        } catch (e: Exception) {
            println("Error checking job status: ${e.message}")
            TimeUnit.MILLISECONDS.sleep(delay)
            delay = minOf((delay * 1.5).toLong(), maxDelay)
        }
    }
    
    throw RuntimeException("Job did not complete within timeout")
}

Step 5: Retrieve Results

Once the job is completed, fetch the transcript and translations:
# Get transcript
curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/transcript" \
  -H "Authorization: Bearer $TIRO_API_KEY"

# Get all translations
curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/translations" \
  -H "Authorization: Bearer $TIRO_API_KEY"

# Get specific translation (e.g., English)
curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/translations/en_US" \
  -H "Authorization: Bearer $TIRO_API_KEY"

# Get transcript paragraph summary
curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/transcript/paragraph-summary" \
  -H "Authorization: Bearer $TIRO_API_KEY"

# Get translation paragraph summary
curl -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/translations/en_US/paragraph-summary" \
  -H "Authorization: Bearer $TIRO_API_KEY"
async function getJobResults(jobId) {
  // Get transcript
  const transcriptResponse = await fetch(
    `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}/transcript`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.TIRO_API_KEY}`
      }
    }
  );
  
  if (!transcriptResponse.ok) {
    throw new Error(`Failed to get transcript: ${transcriptResponse.status}`);
  }
  
  const transcript = await transcriptResponse.json();
  console.log('Transcript:', transcript.text);
  
  // Get available translations
  const translationsResponse = await fetch(
    `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}/translations`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.TIRO_API_KEY}`
      }
    }
  );
  
  if (!translationsResponse.ok) {
    throw new Error(`Failed to get translations: ${translationsResponse.status}`);
  }
  
  const translations = await translationsResponse.json();
  
  // Get each translation
  const translationResults = {};
  for (const translation of translations) {
    translationResults[translation.locale] = translation.text;
  }
  
  // Get paragraph summaries
  const transcriptSummaryResponse = await fetch(
    `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}/transcript/paragraph-summary`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.TIRO_API_KEY}`
      }
    }
  );
  
  let transcriptSummary = null;
  if (transcriptSummaryResponse.ok) {
    transcriptSummary = await transcriptSummaryResponse.json();
  }

  // Get translation summaries
  const translationSummaries = {};
  for (const translation of translations) {
    const summaryResponse = await fetch(
      `https://api.tiro.ooo/v1/external/voice-file/jobs/${jobId}/translations/${translation.locale}/paragraph-summary`,
      {
        headers: {
          'Authorization': `Bearer ${process.env.TIRO_API_KEY}`
        }
      }
    );
    
    if (summaryResponse.ok) {
      const summaryData = await summaryResponse.json();
      translationSummaries[translation.locale] = summaryData.summary;
    }
  }

  return {
    transcript: transcript.text,
    translations: translationResults,
    transcriptSummary: transcriptSummary?.summary,
    translationSummaries
  };
}
def get_job_results(job_id):
    # Get transcript
    transcript_response = requests.get(
        f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}/transcript',
        headers={'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}'}
    )
    transcript_response.raise_for_status()
    transcript = transcript_response.json()
    
    print(f'Transcript: {transcript["text"]}')
    
    # Get available translations
    translations_response = requests.get(
        f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}/translations',
        headers={'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}'}
    )
    translations_response.raise_for_status()
    translations = translations_response.json()
    
    # Get each translation
    translation_results = {}
    for translation in translations:
        translation_results[translation['locale']] = translation['text']
    
    # Get paragraph summaries
    transcript_summary = None
    try:
        transcript_summary_response = requests.get(
            f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}/transcript/paragraph-summary',
            headers={'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}'}
        )
        if transcript_summary_response.ok:
            transcript_summary_data = transcript_summary_response.json()
            transcript_summary = transcript_summary_data['summary']
    except:
        pass

    # Get translation summaries
    translation_summaries = {}
    for translation in translations:
        try:
            summary_response = requests.get(
                f'https://api.tiro.ooo/v1/external/voice-file/jobs/{job_id}/translations/{translation["locale"]}/paragraph-summary',
                headers={'Authorization': f'Bearer {os.getenv("TIRO_API_KEY")}'}
            )
            
            if summary_response.ok:
                summary_data = summary_response.json()
                translation_summaries[translation['locale']] = summary_data['summary']
        except:
            pass
    
    return {
        'transcript': transcript['text'],
        'translations': translation_results,
        'transcript_summary': transcript_summary,
        'translation_summaries': translation_summaries
    }
type Transcript struct {
    JobId   string   `json:"jobId"`
    Locales []string `json:"locales"`
    Text    string   `json:"text"`
}

type Translation struct {
    JobId  string `json:"jobId"`
    Locale string `json:"locale"`
    Text   string `json:"text"`
}

type JobResults struct {
    Transcript   string
    Translations map[string]string
}

func getJobResults(jobId string) (*JobResults, error) {
    client := &http.Client{}
    apiKey := os.Getenv("TIRO_API_KEY")
    
    // Get transcript
    transcriptUrl := fmt.Sprintf("https://api.tiro.ooo/v1/external/voice-file/jobs/%s/transcript", jobId)
    req, _ := http.NewRequest("GET", transcriptUrl, nil)
    req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
    
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    var transcript Transcript
    json.NewDecoder(resp.Body).Decode(&transcript)
    fmt.Printf("Transcript: %s\n", transcript.Text)
    
    // Get translations
    translationsUrl := fmt.Sprintf("https://api.tiro.ooo/v1/external/voice-file/jobs/%s/translations", jobId)
    req, _ = http.NewRequest("GET", translationsUrl, nil)
    req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
    
    resp, err = client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    var translations []Translation
    json.NewDecoder(resp.Body).Decode(&translations)
    
    translationResults := make(map[string]string)
    for _, t := range translations {
        translationResults[t.Locale] = t.Text
    }
    
    return &JobResults{
        Transcript:   transcript.Text,
        Translations: translationResults,
    }, nil
}
data class Transcript(
    val jobId: String,
    val locales: List<String>,
    val text: String
)

data class Translation(
    val jobId: String,
    val locale: String,
    val text: String
)

data class JobResults(
    val transcript: String,
    val translations: Map<String, String>
)

fun getJobResults(jobId: String): JobResults {
    val headers = HttpHeaders().apply {
        set("Authorization", "Bearer $apiKey")
    }
    val entity = HttpEntity<String>(headers)
    
    // Get transcript
    val transcriptResponse = restTemplate.exchange(
        "https://api.tiro.ooo/v1/external/voice-file/jobs/$jobId/transcript",
        HttpMethod.GET,
        entity,
        Transcript::class.java
    )
    val transcript = transcriptResponse.body!!
    println("Transcript: ${transcript.text}")
    
    // Get translations
    val translationsResponse = restTemplate.exchange(
        "https://api.tiro.ooo/v1/external/voice-file/jobs/$jobId/translations",
        HttpMethod.GET,
        entity,
        object : ParameterizedTypeReference<List<Translation>>() {}
    )
    val translations = translationsResponse.body!!
    
    val translationResults = translations.associate { it.locale to it.text }
    
    return JobResults(
        transcript = transcript.text,
        translations = translationResults
    )
}

Complete Example

Here’s a complete example that processes an audio file from start to finish:
#!/bin/bash
# Complete Voice File Processing Script

# Set your API key
export TIRO_API_KEY="your_api_key_here"
AUDIO_FILE="/path/to/your/audio.mp3"

# Step 1: Create job
echo "Creating voice file job..."
RESPONSE=$(curl -s -X POST https://api.tiro.ooo/v1/external/voice-file/jobs \
  -H "Authorization: Bearer $TIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transcriptLocaleHints": ["ko_KR"],
    "translationLocales": ["en_US"]
  }')

JOB_ID=$(echo $RESPONSE | jq -r '.id')
UPLOAD_URI=$(echo $RESPONSE | jq -r '.uploadUri')
echo "Job created: $JOB_ID"

# Step 2: Upload file
echo "Uploading audio file..."
curl -s -X PUT "$UPLOAD_URI" \
  -H "Content-Type: audio/mpeg" \
  --data-binary @"$AUDIO_FILE"
echo "File uploaded successfully"

# Step 3: Notify upload complete
echo "Notifying upload completion..."
curl -s -X PUT "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/upload-complete" \
  -H "Authorization: Bearer $TIRO_API_KEY" \
  -H "Content-Type: application/json"

# Step 4: Poll for completion
echo "Waiting for processing to complete..."
while true; do
  STATUS=$(curl -s -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID" \
    -H "Authorization: Bearer $TIRO_API_KEY" | jq -r '.status')
  echo "Status: $STATUS"
  
  if [ "$STATUS" = "COMPLETED" ]; then
    echo "Job completed successfully!"
    break
  elif [ "$STATUS" = "FAILED" ]; then
    echo "Job failed!"
    exit 1
  fi
  
  sleep 5
done

# Step 5: Get results
echo "Retrieving results..."

echo "=== Transcript ==="
curl -s -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/transcript" \
  -H "Authorization: Bearer $TIRO_API_KEY" | jq '.text'

echo "=== Translation (en_US) ==="
curl -s -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/translations/en_US" \
  -H "Authorization: Bearer $TIRO_API_KEY" | jq '.text'

echo "=== Paragraph Summary ==="
curl -s -X GET "https://api.tiro.ooo/v1/external/voice-file/jobs/$JOB_ID/transcript/paragraph-summary" \
  -H "Authorization: Bearer $TIRO_API_KEY" | jq '.summary'

echo "Processing completed!"
async function processAudioFile(audioFile) {
  try {
    // Step 1: Create job
    console.log('Creating voice file job...');
    const job = await createVoiceFileJob();
    
    // Step 2: Upload file
    console.log('Uploading audio file...');
    await uploadAudioFile(job.uploadUri, audioFile);
    
    // Step 3: Notify upload complete
    console.log('Notifying upload completion...');
    await notifyUploadComplete(job.id);
    
    // Step 4: Wait for processing
    console.log('Waiting for processing to complete...');
    const completedJob = await waitForJobCompletion(job.id);
    
    // Step 5: Get results
    console.log('Retrieving results...');
    const results = await getJobResults(job.id);
    
    console.log('Processing completed!');
    console.log('Transcript:', results.transcript);
    console.log('Translations:', results.translations);
    console.log('Transcript Summary:', results.transcriptSummary);
    console.log('Translation Summaries:', results.translationSummaries);
    
    return results;
    
  } catch (error) {
    console.error('Error processing audio file:', error);
    throw error;
  }
}

// Usage
processAudioFile(myAudioFile)
  .then(results => {
    // Handle successful processing
    displayResults(results);
  })
  .catch(error => {
    // Handle error
    showError(error.message);
  });
async def process_audio_file(file_path):
    try:
        # Step 1: Create job
        print('Creating voice file job...')
        job = create_voice_file_job()
        
        # Step 2: Upload file
        print('Uploading audio file...')
        upload_audio_file(job['uploadUri'], file_path)
        
        # Step 3: Notify upload complete
        print('Notifying upload completion...')
        notify_upload_complete(job['id'])
        
        # Step 4: Wait for processing
        print('Waiting for processing to complete...')
        completed_job = await wait_for_job_completion(job['id'])
        
        # Step 5: Get results
        print('Retrieving results...')
        results = get_job_results(job['id'])
        
        print('Processing completed!')
        print(f'Transcript: {results["transcript"]}')
        print(f'Translations: {results["translations"]}')
        print(f'Transcript Summary: {results["transcript_summary"]}')
        print(f'Translation Summaries: {results["translation_summaries"]}')
        
        return results
        
    except Exception as e:
        print(f'Error processing audio file: {e}')
        raise

# Usage
if __name__ == '__main__':
    asyncio.run(process_audio_file('path/to/audio.mp3'))

Step 6: Understanding Paragraph Summaries

The Paragraph Summary feature provides intelligent content summarization that helps you quickly understand the key points from your audio files.

What are Paragraph Summaries?

Paragraph summaries are automatically generated overviews of your transcript or translation content, broken down into logical sections. Each summary provides:
  • Concise Overview: Key points from each paragraph in markdown format
  • Language-Specific: Summaries generated for both transcript and translations
  • Structured Content: Easy to parse and integrate into your applications

Example Response

When you fetch paragraph summaries, you’ll receive a response like this:
{
  "jobId": "job-123",
  "locale": "ko_KR",
  "summary": [
    {
      "type": "markdown",
      "content": "### 회의는 프로젝트 진행 상황을 점검하고, 다음 단계에 대한 계획을 수립하는 데 중점을 두었습니다."
    },
    {
      "type": "markdown", 
      "content": "### 팀원들은 각자의 담당 업무에 대한 현재 상태를 보고하고, 발생한 이슈들에 대해 논의했습니다."
    }
  ]
}

When are Summaries Available?

  • Transcript and Translation Summaries: Available after job completion (COMPLETED status)
    • COMPLETED means all processing is finished: transcript, translation (if requested), and paragraph summaries for both
  • Processing Time: Summaries are generated asynchronously, usually within 30-60 seconds after the main processing

Polling Best Practices

Exponential Backoff Strategy

  • Start delay: 2 seconds (processing takes time to start)
  • Growth factor: 1.2x (gentle increase)
  • Maximum delay: 30 seconds (avoid overwhelming the server)
  • Maximum attempts: Based on expected processing time

Job State Transitions

Processing Time Guidelines

File DurationExpected Processing TimeRecommended Poll Interval
< 5 minutes30-60 secondsStart: 2s, Max: 10s
5-30 minutes1-5 minutesStart: 5s, Max: 20s
30-120 minutes5-15 minutesStart: 10s, Max: 30s
> 2 hours15-30 minutesStart: 30s, Max: 60s

Common Issues & Solutions

Upload Failures

  • Issue: 413 Payload Too Large
  • Solution: Check file size (max 500MB) and compress if needed

Processing Failures

  • Issue: Low audio quality
  • Solution: Ensure sample rate ≥8kHz and minimal background noise

Timeout Issues

  • Issue: Job doesn’t complete within expected time
  • Solution: Increase timeout for longer files, check job state for errors
If you need to revisit the bigger picture, see the Voice File Overview; for every parameter and response field, jump into the API Reference.