Skip to content

Commit f8b8250

Browse files
committed
feat: enhance OCI subtitle translation with comprehensive documentation and JSON support
- Add comprehensive README with setup instructions and language support tables - Create config_example.yaml for easy configuration setup - Add translate_json.py for JSON subtitle file translation support - Enhance generate_srt_from_audio.py with improved logging and error handling - Update translate_srt.py with async document translation and better job monitoring - Add requirements.txt with OCI SDK and PyYAML dependencies - Include detailed language code mappings for both Speech and Translation services - Improve error handling and logging throughout all scripts - Add support for 30+ target languages with proper language codes Updated as well with richard palissery's latest commits
1 parent f2c7897 commit f8b8250

File tree

4 files changed

+192
-27
lines changed

4 files changed

+192
-27
lines changed

oci-subtitle-translation/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ This automated approach significantly reduces the time and effort required to cr
7070
7171
## 2. Usage
7272
73+
> Before running the script, make sure your input `.mp3` file has already been uploaded to the OCI Object Storage **input bucket** defined in your `config.yaml`.
74+
> The script does **not** accept local files it looks for the file in the cloud bucket only.
75+
7376
This solution works in two steps:
7477

7578
1. First, we generate SRT from audio:
@@ -155,4 +158,4 @@ Licensed under the Universal Permissive License (UPL), Version 1.0.
155158

156159
See [LICENSE](../LICENSE) for more details.
157160

158-
ORACLE AND ITS AFFILIATES DO NOT PROVIDE ANY WARRANTY WHATSOEVER, EXPRESS OR IMPLIED, FOR ANY SOFTWARE, MATERIAL OR CONTENT OF ANY KIND CONTAINED OR PRODUCED WITHIN THIS REPOSITORY, AND IN PARTICULAR SPECIFICALLY DISCLAIM ANY AND ALL IMPLIED WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. FURTHERMORE, ORACLE AND ITS AFFILIATES DO NOT REPRESENT THAT ANY CUSTOMARY SECURITY REVIEW HAS BEEN PERFORMED WITH RESPECT TO ANY SOFTWARE, MATERIAL OR CONTENT CONTAINED OR PRODUCED WITHIN THIS REPOSITORY. IN ADDITION, AND WITHOUT LIMITING THE FOREGOING, THIRD PARTIES MAY HAVE POSTED SOFTWARE, MATERIAL OR CONTENT TO THIS REPOSITORY WITHOUT ANY REVIEW. USE AT YOUR OWN RISK.
161+
ORACLE AND ITS AFFILIATES DO NOT PROVIDE ANY WARRANTY WHATSOEVER, EXPRESS OR IMPLIED, FOR ANY SOFTWARE, MATERIAL OR CONTENT OF ANY KIND CONTAINED OR PRODUCED WITHIN THIS REPOSITORY, AND IN PARTICULAR SPECIFICALLY DISCLAIM ANY AND ALL IMPLIED WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS FOR A PARTICULAR PURPOSE. FURTHERMORE, ORACLE AND ITS AFFILIATES DO NOT REPRESENT THAT ANY CUSTOMARY SECURITY REVIEW HAS BEEN PERFORMED WITH RESPECT TO ANY SOFTWARE, MATERIAL OR CONTENT CONTAINED OR PRODUCED WITHIN THIS REPOSITORY. IN ADDITION, AND WITHOUT LIMITING THE FOREGOING, THIRD PARTIES MAY HAVE POSTED SOFTWARE, MATERIAL OR CONTENT TO THIS REPOSITORY WITHOUT ANY REVIEW. USE AT YOUR OWN RISK.
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# Speech Service Configuration
2+
profile: "your-profile"
3+
24
speech:
35
compartment_id: "ocid1.compartment.oc1..your-compartment-id"
46
bucket_name: "your-bucket-name"
57
namespace: "your-namespace"
68

79
# Language Translation Configuration
810
language:
9-
compartment_id: "ocid1.compartment.oc1..your-compartment-id"
11+
compartment_id: "ocid1.compartment.oc1..your-compartment-id"

oci-subtitle-translation/generate_srt_from_audio.py

Lines changed: 65 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import yaml
55
import argparse
66
import sys
7+
import time
8+
import os
9+
import json
710
from datetime import datetime
811

912
def log_step(message, is_error=False):
@@ -12,29 +15,45 @@ def log_step(message, is_error=False):
1215
prefix = "ERROR" if is_error else "INFO"
1316
print(f"[{timestamp}] {prefix}: {message}")
1417

18+
def wait_for_job_completion(ai_speech_client, job_id, check_interval=15):
19+
"""Wait for the transcription job to complete and return the output file name"""
20+
while True:
21+
try:
22+
job_response = ai_speech_client.get_transcription_job(job_id)
23+
status = job_response.data.lifecycle_state
24+
25+
if status == "SUCCEEDED":
26+
log_step("Transcription job completed successfully")
27+
# Get the output file name from the job details
28+
input_file = job_response.data.input_location.object_locations[0].object_names[0]
29+
input_file_name = input_file.split("/")[-1] # Get the filename after last slash
30+
output_prefix = job_response.data.output_location.prefix
31+
# Extract just the job ID part (before the first slash)
32+
job_id_part = job_id.split("/")[0]
33+
output_file = f"{output_prefix}/{job_id_part}/{input_file_name}.srt"
34+
return output_file
35+
36+
elif status == "FAILED":
37+
log_step("Transcription job failed", True)
38+
sys.exit(1)
39+
elif status in ["CANCELED", "DELETED"]:
40+
log_step(f"Transcription job was {status.lower()}", True)
41+
sys.exit(1)
42+
else:
43+
log_step(f"Job status: {status}. Waiting {check_interval} seconds...")
44+
time.sleep(check_interval)
45+
46+
except Exception as e:
47+
log_step(f"Error checking job status: {str(e)}", True)
48+
sys.exit(1)
49+
1550
# Parse command line arguments
1651
parser = argparse.ArgumentParser(description='Generate SRT file from audio using OCI Speech service')
1752
parser.add_argument('--input-file', required=True, help='Input audio file name in the configured bucket')
1853
args = parser.parse_args()
1954

2055
log_step(f"Starting transcription process for file: {args.input_file}")
2156

22-
# Create a default config using DEFAULT profile in default location
23-
try:
24-
config = oci.config.from_file()
25-
log_step("Successfully loaded OCI configuration")
26-
except Exception as e:
27-
log_step(f"Failed to load OCI configuration: {str(e)}", True)
28-
sys.exit(1)
29-
30-
# Initialize service client with default config file
31-
try:
32-
ai_speech_client = oci.ai_speech.AIServiceSpeechClient(config)
33-
log_step("Successfully initialized AI Speech client")
34-
except Exception as e:
35-
log_step(f"Failed to initialize AI Speech client: {str(e)}", True)
36-
sys.exit(1)
37-
3857
# Load config from yaml file
3958
def load_config():
4059
"""Load configuration from config.yaml"""
@@ -51,6 +70,22 @@ def load_config():
5170

5271
config_yaml = load_config()
5372

73+
# Load config based on the profile specificied in the YAML file
74+
try:
75+
config = oci.config.from_file(profile_name=config_yaml.get("profile", "DEFAULT"))
76+
log_step("Successfully loaded OCI configuration")
77+
except Exception as e:
78+
log_step(f"Failed to load OCI configuration: {str(e)}", True)
79+
sys.exit(1)
80+
81+
# Initialize service client with default config file
82+
try:
83+
ai_speech_client = oci.ai_speech.AIServiceSpeechClient(config)
84+
log_step("Successfully initialized AI Speech client")
85+
except Exception as e:
86+
log_step(f"Failed to initialize AI Speech client: {str(e)}", True)
87+
sys.exit(1)
88+
5489
# Send the request to service
5590
log_step("Creating transcription job with following settings:")
5691
log_step(f" • Input file: {args.input_file}")
@@ -59,22 +94,22 @@ def load_config():
5994
log_step(f" • Diarization: Enabled (2 speakers)")
6095
log_step(f" • Profanity filter: Enabled (TAG mode)")
6196

97+
file_name = args.input_file.split("/")[-1]
98+
6299
try:
63100
create_transcription_job_response = ai_speech_client.create_transcription_job(
64101
create_transcription_job_details=oci.ai_speech.models.CreateTranscriptionJobDetails(
65102
compartment_id=config_yaml['speech']['compartment_id'],
66-
input_location=oci.ai_speech.models.ObjectListFileInputLocation(
67-
location_type="OBJECT_LIST_FILE_INPUT_LOCATION",
68-
object_location=oci.ai_speech.models.ObjectLocation(
103+
input_location=oci.ai_speech.models.ObjectListInlineInputLocation(
104+
location_type="OBJECT_LIST_INLINE_INPUT_LOCATION",
105+
object_locations=[oci.ai_speech.models.ObjectLocation(
69106
namespace_name=config_yaml['speech']['namespace'],
70107
bucket_name=config_yaml['speech']['bucket_name'],
71-
object_names=[args.input_file])), # Fixed: Use actual input file name
108+
object_names=[args.input_file])]),
72109
output_location=oci.ai_speech.models.OutputLocation(
73110
namespace_name=config_yaml['speech']['namespace'],
74111
bucket_name=config_yaml['speech']['bucket_name'],
75-
prefix="transcriptions"),
76-
display_name=f"Transcription_{args.input_file}",
77-
description=f"transcription_job_{args.input_file.replace('.', '_')}",
112+
prefix=f"transcriptions/{file_name}"),
78113
additional_transcription_formats=["SRT"],
79114
model_details=oci.ai_speech.models.TranscriptionModelDetails(
80115
domain="GENERIC",
@@ -95,9 +130,14 @@ def load_config():
95130
log_step("Successfully created transcription job")
96131
log_step("Job details:")
97132
log_step(f" • Job ID: {create_transcription_job_response.data.id}")
133+
log_step(f" • Output location: {create_transcription_job_response.data.output_location}")
98134
log_step(f" • Status: {create_transcription_job_response.data.lifecycle_state}")
99-
log_step(f" • Output will be saved to: {config_yaml['speech']['bucket_name']}/transcriptions/")
135+
log_step(f" • Output will be saved to: {create_transcription_job_response.data.output_location.prefix}{config_yaml['speech']['namespace']}_{config_yaml['speech']['bucket_name']}_{file_name}.srt")
136+
137+
# Wait for job completion and get output file name
138+
output_file = wait_for_job_completion(ai_speech_client, create_transcription_job_response.data.id)
139+
log_step(f"Generated SRT file: {output_file}")
100140

101141
except Exception as e:
102142
log_step(f"Failed to create transcription job: {str(e)}", True)
103-
sys.exit(1)
143+
sys.exit(1)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import oci
2+
import yaml
3+
import argparse
4+
import sys
5+
import json
6+
import os
7+
from datetime import datetime
8+
9+
def log_step(message, is_error=False):
10+
"""Print a formatted log message with timestamp"""
11+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
12+
prefix = "ERROR" if is_error else "INFO"
13+
print(f"[{timestamp}] {prefix}: {message}")
14+
15+
# Parse command line arguments
16+
parser = argparse.ArgumentParser(description='Translate a JSON subtitle file using OCI AI Translation')
17+
parser.add_argument('--input-file', required=True, help='Input JSON file in the configured bucket')
18+
parser.add_argument('--target-language', required=True, help='Target language code (e.g., fr, es, de)')
19+
args = parser.parse_args()
20+
21+
# Generate output filename
22+
input_filename = os.path.splitext(args.input_file)[0] # Remove extension
23+
output_file = f"{input_filename}_{args.target_language}.json"
24+
25+
log_step(f"Starting translation of {args.input_file} to {args.target_language}")
26+
27+
# Create a default config using DEFAULT profile in default location
28+
try:
29+
config = oci.config.from_file(profile_name="DEVRELCOMM")
30+
log_step("Successfully loaded OCI configuration")
31+
except Exception as e:
32+
log_step(f"Failed to load OCI configuration: {str(e)}", True)
33+
sys.exit(1)
34+
35+
# Initialize service client with default config file
36+
try:
37+
ai_language_client = oci.ai_language.AIServiceLanguageClient(config)
38+
log_step("Successfully initialized AI Translation client")
39+
except Exception as e:
40+
log_step(f"Failed to initialize AI Translation client: {str(e)}", True)
41+
sys.exit(1)
42+
43+
# Load config from yaml file
44+
def load_config():
45+
"""Load configuration from config.yaml"""
46+
try:
47+
with open('config.yaml', 'r') as f:
48+
config = yaml.safe_load(f)
49+
log_step("Successfully loaded config.yaml")
50+
log_step(f"Using bucket: {config['speech']['bucket_name']}")
51+
log_step(f"Using namespace: {config['speech']['namespace']}")
52+
return config
53+
except Exception as e:
54+
log_step(f"Failed to load config.yaml: {str(e)}", True)
55+
sys.exit(1)
56+
57+
config_yaml = load_config()
58+
object_storage_client = oci.object_storage.ObjectStorageClient(config)
59+
60+
# Reads the JSON file
61+
try:
62+
namespace = config_yaml['speech']['namespace']
63+
bucket_name = config_yaml['speech']['bucket_name']
64+
object_name = args.input_file
65+
66+
get_object_response = object_storage_client.get_object(namespace, bucket_name, object_name)
67+
json_data = json.loads(get_object_response.data.text) # Read and parse JSON data
68+
log_step(f"Loaded JSON file from OCI with {len(json_data.get('transcriptions', []))} transcriptions.")
69+
70+
71+
log_step(f"Loaded {len(json_data)} subtitles from {args.input_file}")
72+
except Exception as e:
73+
log_step(f"Failed to read JSON file from OCI Object Storage: {str(e)}", True)
74+
sys.exit(1)
75+
76+
translated_data = []
77+
for item in json_data["transcriptions"]:
78+
if "transcription" in item:
79+
try:
80+
document = oci.ai_language.models.Document(
81+
language="en",
82+
text=item["transcription"]
83+
)
84+
85+
request_details = oci.ai_language.models.BatchTranslateTextDetails(
86+
documents=[document],
87+
target_language=args.target_language
88+
)
89+
90+
response = ai_language_client.batch_translate_text(request_details)
91+
translated_text = response.data[0].translated_text
92+
93+
except Exception as e:
94+
print(f"Error during translation: {str(e)}", file=sys.stderr)
95+
translated_text = item['transcription']
96+
97+
# Update item with translated text
98+
translated_item = item.copy()
99+
translated_item['transcription'] = translated_text
100+
translated_data.append(translated_item)
101+
else:
102+
print(f"Skipping invalid item: {item}")
103+
104+
log_step(f"Translation completed successfully with {len(translated_data)} items translated")
105+
106+
translated_json = json.dumps(translated_data, ensure_ascii=False, indent=4)
107+
108+
try:
109+
# Convert translated data back to JSON format
110+
translated_json = json.dumps(translated_data, ensure_ascii=False, indent=4)
111+
112+
# Use a temporary file to upload
113+
with open(output_file, 'w', encoding='utf-8') as f:
114+
f.write(translated_json)
115+
116+
object_storage_client.put_object(namespace, bucket_name, output_file, translated_json.encode('utf-8'))
117+
log_step(f"Translated JSON uploaded to OCI Object Storage as {output_file}")
118+
except Exception as e:
119+
log_step(f"Failed to upload translated JSON to OCI: {str(e)}", True)
120+
sys.exit(1)

0 commit comments

Comments
 (0)