NHLOCAL commited on
Commit
64c3c73
·
1 Parent(s): 9022007

פיצול משופר והצגה שלו למשתמש

Browse files
Files changed (2) hide show
  1. main.py +111 -47
  2. templates/index.html +34 -4
main.py CHANGED
@@ -4,7 +4,7 @@ from fastapi.templating import Jinja2Templates
4
  from google import genai
5
  from google.genai import types
6
  from pydub import AudioSegment
7
- from pydub.silence import split_on_silence
8
  import yaml
9
  import json
10
  import io
@@ -12,7 +12,7 @@ import os
12
  from datetime import timedelta
13
  import logging
14
  import asyncio
15
- from pydantic import BaseModel, Field # <-- CHANGE: Import BaseModel and Field from pydantic
16
 
17
  # --- Setup and Constants ---
18
  logging.basicConfig(level=logging.INFO)
@@ -20,14 +20,22 @@ app = FastAPI()
20
  templates = Jinja2Templates(directory="templates")
21
 
22
  # --- Audio Splitting Constants ---
23
- MAX_CHUNK_DURATION_MIN = 10
24
- MAX_CHUNK_DURATION_MS = MAX_CHUNK_DURATION_MIN * 60 * 1000
 
 
 
 
 
 
 
 
 
 
25
  SILENCE_THRESH_DB = -30
26
  MIN_SILENCE_LEN_MS = 500
27
- NO_SPLIT_DURATION_MIN = 14
28
 
29
- # --- CHANGE: Pydantic Schema Definition ---
30
- # This class replaces schema.json. It's type-safe and recommended by Google.
31
  class TranscriptionSegment(BaseModel):
32
  id: int = Field(description="מספר סידורי של הכתובית", ge=1)
33
  start_time: str = Field(description="שעת התחלה בפורמט HH:MM:SS,mmm")
@@ -36,7 +44,6 @@ class TranscriptionSegment(BaseModel):
36
 
37
  # --- Helper functions ---
38
 
39
- # <-- CHANGE: This function now only loads the system prompt.
40
  def load_system_prompt():
41
  """טוען system_prompt מקובץ חיצוני."""
42
  try:
@@ -50,35 +57,83 @@ def load_system_prompt():
50
  logging.error(f"Error loading configuration: {e}")
51
  raise HTTPException(status_code=500, detail=f"שגיאת שרת: בעיה בטעינת ההגדרות: {e}")
52
 
53
- def split_audio_smart(audio_segment, max_duration_ms, silence_thresh, min_silence_len):
54
- """מפצל אודיו למקטעים הקרובים ככל האפשר ל-max_duration_ms."""
55
- logging.info(f"Splitting audio smartly. Target duration: ~{max_duration_ms / 60000} mins. Silence threshold: {silence_thresh}dB.")
56
- raw_chunks = split_on_silence(
57
- audio_segment,
58
- min_silence_len=min_silence_len,
59
- silence_thresh=silence_thresh,
60
- keep_silence=500
61
- )
62
- if not raw_chunks:
63
- logging.warning("No silence detected for smart splitting. Using fixed-size chunks.")
64
- return [audio_segment[i:i + max_duration_ms] for i in range(0, len(audio_segment), max_duration_ms)]
 
 
 
 
 
 
 
65
  final_chunks = []
66
- current_recombined_chunk = AudioSegment.empty()
67
- for chunk in raw_chunks:
68
- if len(current_recombined_chunk) + len(chunk) > max_duration_ms and len(current_recombined_chunk) > 0:
69
- final_chunks.append(current_recombined_chunk)
70
- current_recombined_chunk = AudioSegment.empty()
71
- current_recombined_chunk += chunk
72
- while len(current_recombined_chunk) > max_duration_ms:
73
- final_chunks.append(current_recombined_chunk[:max_duration_ms])
74
- current_recombined_chunk = current_recombined_chunk[max_duration_ms:]
75
- if len(current_recombined_chunk) > 0:
76
- final_chunks.append(current_recombined_chunk)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  logging.info(f"File successfully split into {len(final_chunks)} chunks.")
78
  logging.info(f"Chunk durations (seconds): {[round(len(c) / 1000) for c in final_chunks]}")
79
  return final_chunks
80
 
81
- # <-- CHANGE: Function now accepts a Pydantic model instead of a JSON schema object.
82
  def transcribe_chunk(chunk_audio, api_key, system_prompt, pydantic_schema, model_name):
83
  """שולח מקטע שמע אחד ל‑Gemini ומקבל JSON, בהתאם לסכמת Pydantic."""
84
  try:
@@ -95,8 +150,6 @@ def transcribe_chunk(chunk_audio, api_key, system_prompt, pydantic_schema, model
95
  config=types.GenerateContentConfig(
96
  system_instruction=system_prompt,
97
  response_mime_type="application/json",
98
- # <-- CHANGE: Pass the Pydantic model directly to the SDK.
99
- # The `list[]` indicates we expect a list of these objects.
100
  response_schema=list[pydantic_schema]
101
  )
102
  )
@@ -149,7 +202,6 @@ async def _transcribe_and_stream(api_key: str, file_content: bytes, model_name:
149
  return json.dumps(event_data) + "\n\n"
150
 
151
  try:
152
- # <-- CHANGE: Load only the system prompt and use the Pydantic class.
153
  system_prompt = load_system_prompt()
154
  pydantic_schema = TranscriptionSegment
155
 
@@ -157,20 +209,33 @@ async def _transcribe_and_stream(api_key: str, file_content: bytes, model_name:
157
 
158
  audio = AudioSegment.from_file(io.BytesIO(file_content))
159
  duration_min = len(audio) / (1000 * 60)
160
- chunks = []
161
-
162
- if duration_min <= NO_SPLIT_DURATION_MIN:
163
- yield send_event("progress", f"אורך הקובץ ({duration_min:.1f} דקות) קצר, מעבד כמקשה אחת...", 15)
164
- chunks = [audio]
165
- else:
166
- yield send_event("progress", f"אורך הקובץ ({duration_min:.1f} דקות) ארוך, מבצע חלוקה חכמה...", 15)
167
- chunks = await asyncio.to_thread(
168
- split_audio_smart, audio, MAX_CHUNK_DURATION_MS, SILENCE_THRESH_DB, MIN_SILENCE_LEN_MS
169
- )
170
 
171
  if not chunks:
172
  raise ValueError("לא נוצרו מקטעי שמע לעיבוד.")
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  yield send_event("progress", f"הקובץ חולק ל-{len(chunks)} מקטעים. מתחיל תמלול עם מודל {model_name}...", 20)
175
 
176
  all_segs, offset = [], 0
@@ -180,7 +245,6 @@ async def _transcribe_and_stream(api_key: str, file_content: bytes, model_name:
180
  progress_percent = 20 + int((i / total_chunks) * 75)
181
  yield send_event("progress", f"מתמלל מקטע {i+1} מתוך {total_chunks}...", progress_percent)
182
 
183
- # <-- CHANGE: Pass the Pydantic schema class to the transcription function.
184
  data, error_msg = await asyncio.to_thread(transcribe_chunk, ch, api_key, system_prompt, pydantic_schema, model_name)
185
 
186
  if error_msg:
@@ -228,4 +292,4 @@ async def handle_transcription_stream(
228
 
229
  file_content = await audio_file.read()
230
 
231
- return StreamingResponse(_transcribe_and_stream(api_key, file_content, model_name), media_type="text/event-stream")
 
4
  from google import genai
5
  from google.genai import types
6
  from pydub import AudioSegment
7
+ from pydub.silence import split_on_silence, detect_silence
8
  import yaml
9
  import json
10
  import io
 
12
  from datetime import timedelta
13
  import logging
14
  import asyncio
15
+ from pydantic import BaseModel, Field
16
 
17
  # --- Setup and Constants ---
18
  logging.basicConfig(level=logging.INFO)
 
20
  templates = Jinja2Templates(directory="templates")
21
 
22
  # --- Audio Splitting Constants ---
23
+ # אורך יעד למקטע שמע (לדוגמה, 10 דקות)
24
+ TARGET_CHUNK_DURATION_MIN = 10
25
+ TARGET_CHUNK_DURATION_MS = TARGET_CHUNK_DURATION_MIN * 60 * 1000
26
+
27
+ # זמן מינימלי למקטע לפני שמתחילים לחפש נקודת פיצול חלופית (לדוגמה, 7 דקות)
28
+ MIN_SPLIT_SEARCH_START_MIN = 7
29
+ MIN_SPLIT_SEARCH_START_MS = MIN_SPLIT_SEARCH_START_MIN * 60 * 1000
30
+
31
+ # אורך מקסימלי של מקטע בו נחפש שקט לפני חיתוך כפוי (לדוגמה, 14 דקות)
32
+ MAX_SPLIT_SEARCH_END_MIN = 14
33
+ MAX_SPLIT_SEARCH_END_MS = MAX_SPLIT_SEARCH_END_MIN * 60 * 1000
34
+
35
  SILENCE_THRESH_DB = -30
36
  MIN_SILENCE_LEN_MS = 500
 
37
 
38
+ # --- Pydantic Schema Definition ---
 
39
  class TranscriptionSegment(BaseModel):
40
  id: int = Field(description="מספר סידורי של הכתובית", ge=1)
41
  start_time: str = Field(description="שעת התחלה בפורמט HH:MM:SS,mmm")
 
44
 
45
  # --- Helper functions ---
46
 
 
47
  def load_system_prompt():
48
  """טוען system_prompt מקובץ חיצוני."""
49
  try:
 
57
  logging.error(f"Error loading configuration: {e}")
58
  raise HTTPException(status_code=500, detail=f"שגיאת שרת: בעיה בטעינת ההגדרות: {e}")
59
 
60
+ # NEW: Function to format milliseconds to HH:MM:SS
61
+ def format_ms_to_hms(ms):
62
+ td = timedelta(milliseconds=ms)
63
+ minutes, seconds = divmod(td.seconds, 60)
64
+ hours, minutes = divmod(minutes, 60)
65
+ hours += td.days * 24 # Handle durations > 24 hours
66
+ return f"{hours:02}:{minutes:02}:{seconds:02}"
67
+
68
+ def split_audio_smart(audio_segment, silence_thresh, min_silence_len):
69
+ """
70
+ מפצל אודיו למקטעים, תוך העדפת נקודות שקט ועם גבולות חיתוך מוגדרים.
71
+ - מנסה לשמור מקטעים סביב TARGET_CHUNK_DURATION_MS (10 דקות).
72
+ - מחפש נקודת שקט לפי הסדר:
73
+ 1. השקט הראשון שמתחיל בין MIN_SPLIT_SEARCH_START_MS (7 דקות) ל-TARGET_CHUNK_DURATION_MS (10 דקות).
74
+ 2. אם לא נמצא כזה, השקט הראשון שמתחיל בין TARGET_CHUNK_DURATION_MS (10 דקות) ל-MAX_SPLIT_SEARCH_END_MS (14 דקות).
75
+ - אם לא נמצא שקט מתאים באף אחד מהטווחים (7-14 דקות), יבוצע חיתוך כפוי ב-MIN_SPLIT_SEARCH_START_MS (7 דקות).
76
+ """
77
+ logging.info(f"Smart splitting: Target Chunk {TARGET_CHUNK_DURATION_MIN}m, Min Split Search Start {MIN_SPLIT_SEARCH_START_MIN}m, Max Split Search End {MAX_SPLIT_SEARCH_END_MIN}m")
78
+
79
  final_chunks = []
80
+ current_offset = 0
81
+ total_length = len(audio_segment)
82
+
83
+ while current_offset < total_length:
84
+ remaining_audio = audio_segment[current_offset:]
85
+
86
+ # אם האודיו שנותר קצר או שווה ל-MAX_SPLIT_SEARCH_END_MS (14 דקות), קח אותו כמקטע האחרון וסיים.
87
+ # זה מטפל גם בקבצים קצרים מ-14 דקות שלא יפוצלו מלכתחילה.
88
+ if len(remaining_audio) <= MAX_SPLIT_SEARCH_END_MS:
89
+ final_chunks.append(remaining_audio)
90
+ break
91
+
92
+ # הגדר את מקטע האודיו לבדיקת שקט. נחפש עד ל-MAX_SPLIT_SEARCH_END_MS מההתחלה הנוכחית.
93
+ segment_for_silence_detection = remaining_audio[:MAX_SPLIT_SEARCH_END_MS]
94
+
95
+ # זיהוי שקטים בתוך המקטע הנוכחי. המיקומים הם יחסיים לתחילת segment_for_silence_detection.
96
+ silences = detect_silence(
97
+ segment_for_silence_detection,
98
+ min_silence_len=min_silence_len,
99
+ silence_thresh=silence_thresh
100
+ )
101
+
102
+ # נקודת החיתוך שנבחרה, יחסית לתחילת המקטע הנוכחי.
103
+ split_point_relative_to_chunk_start = -1
104
+
105
+ # 1. חיפוש שקט בטווח המועדף (7 דקות עד לפני 10 דקות)
106
+ for s_start, s_end in silences:
107
+ if MIN_SPLIT_SEARCH_START_MS <= s_start < TARGET_CHUNK_DURATION_MS:
108
+ # נמצאה נקודת פיצול מועדפת. נשתמש בסוף קטע השקט כנקודת החיתוך.
109
+ split_point_relative_to_chunk_start = s_end
110
+ break
111
+
112
+ # 2. אם לא נמצא שקט מועדף, חפש בטווח המורחב (10 דקות עד לפני 14 דקות)
113
+ if split_point_relative_to_chunk_start == -1:
114
+ for s_start, s_end in silences:
115
+ if TARGET_CHUNK_DURATION_MS <= s_start < MAX_SPLIT_SEARCH_END_MS:
116
+ # נמצאה נקודת פיצול בטווח המורחב. נשתמש בסוף קטע השקט כנקודת החיתוך.
117
+ split_point_relative_to_chunk_start = s_end
118
+ break
119
+
120
+ # 3. אם לא נמצא שקט מתאים באף אחד מהטווחים (7-14 דקות)
121
+ if split_point_relative_to_chunk_start == -1:
122
+ # על פי בקשת המשתמש: "יתבצע חיתוך אחרי 7 דקות בלבד"
123
+ logging.warning(f"No suitable silence found between {MIN_SPLIT_SEARCH_START_MIN}m and {MAX_SPLIT_SEARCH_END_MIN}m. Performing hard cut at {MIN_SPLIT_SEARCH_START_MIN}m.")
124
+ split_point_relative_to_chunk_start = MIN_SPLIT_SEARCH_START_MS
125
+
126
+ # וודא שנקודת הפיצול לא חורגת מאורך האודיו הנותר (למען בטיחות).
127
+ split_point_relative_to_chunk_start = min(split_point_relative_to_chunk_start, len(remaining_audio))
128
+
129
+ # הוסף את המקטע שנקבע וקדם את ה-offset.
130
+ final_chunks.append(remaining_audio[:split_point_relative_to_chunk_start])
131
+ current_offset += split_point_relative_to_chunk_start
132
+
133
  logging.info(f"File successfully split into {len(final_chunks)} chunks.")
134
  logging.info(f"Chunk durations (seconds): {[round(len(c) / 1000) for c in final_chunks]}")
135
  return final_chunks
136
 
 
137
  def transcribe_chunk(chunk_audio, api_key, system_prompt, pydantic_schema, model_name):
138
  """שולח מקטע שמע אחד ל‑Gemini ומקבל JSON, בהתאם לסכמת Pydantic."""
139
  try:
 
150
  config=types.GenerateContentConfig(
151
  system_instruction=system_prompt,
152
  response_mime_type="application/json",
 
 
153
  response_schema=list[pydantic_schema]
154
  )
155
  )
 
202
  return json.dumps(event_data) + "\n\n"
203
 
204
  try:
 
205
  system_prompt = load_system_prompt()
206
  pydantic_schema = TranscriptionSegment
207
 
 
209
 
210
  audio = AudioSegment.from_file(io.BytesIO(file_content))
211
  duration_min = len(audio) / (1000 * 60)
212
+
213
+ yield send_event("progress", f"אורך הקובץ {duration_min:.1f} דקות. מבצע חלוקה חכמה...", 15)
214
+ chunks = await asyncio.to_thread(
215
+ split_audio_smart, audio, SILENCE_THRESH_DB, MIN_SILENCE_LEN_MS
216
+ )
 
 
 
 
 
217
 
218
  if not chunks:
219
  raise ValueError("לא נוצרו מקטעי שמע לעיבוד.")
220
 
221
+ # NEW: Calculate and send chunk timestamps
222
+ chunk_info_messages = []
223
+ current_cumulative_offset = 0
224
+ for i, ch in enumerate(chunks):
225
+ chunk_start_ms = current_cumulative_offset
226
+ chunk_end_ms = current_cumulative_offset + len(ch)
227
+ chunk_info_messages.append(
228
+ f"{i+1}. {format_ms_to_hms(chunk_start_ms)} - {format_ms_to_hms(chunk_end_ms)}"
229
+ )
230
+ current_cumulative_offset += len(ch)
231
+
232
+ yield send_event(
233
+ "chunk_timestamps", # New event type
234
+ message="השמע חולק למקטעים בנקודות הבאות:",
235
+ data="\n".join(chunk_info_messages)
236
+ )
237
+ # End NEW
238
+
239
  yield send_event("progress", f"הקובץ חולק ל-{len(chunks)} מקטעים. מתחיל תמלול עם מודל {model_name}...", 20)
240
 
241
  all_segs, offset = [], 0
 
245
  progress_percent = 20 + int((i / total_chunks) * 75)
246
  yield send_event("progress", f"מתמלל מקטע {i+1} מתוך {total_chunks}...", progress_percent)
247
 
 
248
  data, error_msg = await asyncio.to_thread(transcribe_chunk, ch, api_key, system_prompt, pydantic_schema, model_name)
249
 
250
  if error_msg:
 
292
 
293
  file_content = await audio_file.read()
294
 
295
+ return StreamingResponse(_transcribe_and_stream(api_key, file_content, model_name), media_type="text/event-stream")
templates/index.html CHANGED
@@ -57,7 +57,7 @@
57
  #status-container { margin-top: 1.5rem; display: none; }
58
  #status-message { text-align: center; padding: 1rem; font-weight: 600; border-radius: var(--border-radius-small) var(--border-radius-small) 0 0; }
59
  #status-message.loading { background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); }
60
- #status-message.error { background-color: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); } /* Corrected color */
61
  #progress-bar-container { width: 100%; background-color: var(--md-sys-color-surface-variant); border-radius: 0 0 var(--border-radius-small) var(--border-radius-small); overflow: hidden; height: 8px; }
62
  #progress-bar { width: 0%; height: 100%; background-color: var(--md-sys-color-primary); transition: width 0.3s ease-in-out; }
63
  #progress-bar.error { background-color: var(--md-sys-color-error); }
@@ -81,7 +81,7 @@
81
  </small>
82
  </div>
83
 
84
- <!-- NEW: Model Selection -->
85
  <div class="input-group">
86
  <label for="model-select">בחר מודל</label>
87
  <select id="model-select">
@@ -116,6 +116,25 @@
116
  </div>
117
  </div>
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  <section id="results-section" class="card">
120
  <h2>תוצאות התמלול (SRT)</h2>
121
  <textarea id="srt-output" readonly></textarea>
@@ -145,6 +164,10 @@
145
  const resultsSection = document.getElementById('results-section');
146
  const srtOutput = document.getElementById('srt-output');
147
  const downloadButton = document.getElementById('download-button');
 
 
 
 
148
 
149
  let audioFile = null;
150
 
@@ -165,10 +188,13 @@
165
  submitButton.disabled = false;
166
  statusContainer.style.display = 'none';
167
  resultsSection.style.display = 'none';
 
 
 
168
  updateStatus("", 0);
169
  }
170
 
171
- // --- NEW: API Key Persistence ---
172
  function loadApiKey() {
173
  const savedKey = localStorage.getItem('geminiApiKey');
174
  if (savedKey) {
@@ -194,7 +220,7 @@
194
  checkInputs();
195
  });
196
 
197
- // NEW: Model selection logic
198
  modelSelect.addEventListener('change', () => {
199
  modelCustomInput.style.display = (modelSelect.value === 'custom') ? 'block' : 'none';
200
  });
@@ -273,6 +299,10 @@
273
 
274
  if (event.type === 'progress') {
275
  updateStatus(event.message, event.percent);
 
 
 
 
276
  } else if (event.type === 'result') {
277
  updateStatus(event.message, event.percent);
278
  srtOutput.value = event.data;
 
57
  #status-container { margin-top: 1.5rem; display: none; }
58
  #status-message { text-align: center; padding: 1rem; font-weight: 600; border-radius: var(--border-radius-small) var(--border-radius-small) 0 0; }
59
  #status-message.loading { background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); }
60
+ #status-message.error { background-color: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); }
61
  #progress-bar-container { width: 100%; background-color: var(--md-sys-color-surface-variant); border-radius: 0 0 var(--border-radius-small) var(--border-radius-small); overflow: hidden; height: 8px; }
62
  #progress-bar { width: 0%; height: 100%; background-color: var(--md-sys-color-primary); transition: width 0.3s ease-in-out; }
63
  #progress-bar.error { background-color: var(--md-sys-color-error); }
 
81
  </small>
82
  </div>
83
 
84
+ <!-- Model Selection -->
85
  <div class="input-group">
86
  <label for="model-select">בחר מודל</label>
87
  <select id="model-select">
 
116
  </div>
117
  </div>
118
 
119
+ <!-- NEW: Section for displaying chunk cut times -->
120
+ <div id="chunk-info-section" class="card" style="margin-top: 1.5rem; display: none;">
121
+ <h2>חלוקת קובץ השמע למקטעים</h2>
122
+ <p id="chunk-info-message" style="margin-bottom: 1rem;"></p>
123
+ <pre id="chunk-timestamps-output" style="
124
+ background-color: var(--md-sys-color-surface);
125
+ padding: 1rem;
126
+ border-radius: var(--border-radius-small);
127
+ border: 1px solid var(--md-sys-color-outline);
128
+ font-family: monospace;
129
+ font-size: 0.9rem;
130
+ direction: ltr; /* Ensure LTR for timestamps */
131
+ text-align: left;
132
+ max-height: 200px;
133
+ overflow-y: auto;
134
+ "></pre>
135
+ </div>
136
+ <!-- END NEW -->
137
+
138
  <section id="results-section" class="card">
139
  <h2>תוצאות התמלול (SRT)</h2>
140
  <textarea id="srt-output" readonly></textarea>
 
164
  const resultsSection = document.getElementById('results-section');
165
  const srtOutput = document.getElementById('srt-output');
166
  const downloadButton = document.getElementById('download-button');
167
+ // NEW: Element selections for chunk info
168
+ const chunkInfoSection = document.getElementById('chunk-info-section');
169
+ const chunkInfoMessage = document.getElementById('chunk-info-message');
170
+ const chunkTimestampsOutput = document.getElementById('chunk-timestamps-output');
171
 
172
  let audioFile = null;
173
 
 
188
  submitButton.disabled = false;
189
  statusContainer.style.display = 'none';
190
  resultsSection.style.display = 'none';
191
+ chunkInfoSection.style.display = 'none'; // NEW: Hide chunk info section
192
+ chunkTimestampsOutput.textContent = ''; // NEW: Clear chunk info
193
+ chunkInfoMessage.textContent = ''; // NEW: Clear chunk info message
194
  updateStatus("", 0);
195
  }
196
 
197
+ // --- API Key Persistence ---
198
  function loadApiKey() {
199
  const savedKey = localStorage.getItem('geminiApiKey');
200
  if (savedKey) {
 
220
  checkInputs();
221
  });
222
 
223
+ // Model selection logic
224
  modelSelect.addEventListener('change', () => {
225
  modelCustomInput.style.display = (modelSelect.value === 'custom') ? 'block' : 'none';
226
  });
 
299
 
300
  if (event.type === 'progress') {
301
  updateStatus(event.message, event.percent);
302
+ } else if (event.type === 'chunk_timestamps') { // NEW: Handle chunk timestamps
303
+ chunkInfoSection.style.display = 'block';
304
+ chunkInfoMessage.textContent = event.message;
305
+ chunkTimestampsOutput.textContent = event.data;
306
  } else if (event.type === 'result') {
307
  updateStatus(event.message, event.percent);
308
  srtOutput.value = event.data;