katospiegel commited on
Commit
3521eb6
·
1 Parent(s): 1c97ed5

feat: Include Transformers implementation, paragraphs, and pdf output

Browse files
Files changed (8) hide show
  1. Dockerfile +6 -5
  2. app/app.py +284 -81
  3. app/app.sh +23 -13
  4. app/createpdf.py +289 -0
  5. app/gradio_app.py +14 -11
  6. app/odtp-output.md +0 -0
  7. app/paragraphsCreator.py +79 -0
  8. requirements.txt +12 -8
Dockerfile CHANGED
@@ -1,6 +1,10 @@
1
  FROM nvidia/cuda:12.1.0-devel-ubuntu22.04
2
 
3
- RUN apt-get update && apt-get install -y apt-utils
 
 
 
 
4
 
5
  RUN apt-get install -y python3.11 python3.11-venv python3-pip
6
 
@@ -66,7 +70,4 @@ RUN sed -i 's/\r$//' /odtp/odtp-component-client/odtp-app.sh
66
  RUN sed -i 's/\r$//' /odtp/odtp-component-client/startup.sh
67
  RUN sed -i 's/\r$//' /odtp/odtp-app/app.sh
68
 
69
- #ENTRYPOINT ["bash", "/odtp/odtp-component-client/startup.sh"]
70
- ENTRYPOINT [ "python3", "/odtp/odtp-app/gradio_app.py" ]
71
-
72
- # Create command to run the app that goes to an entrypoint basically the startup mode. Also I in order to work with an API I need some interface with an s3 to make it work?
 
1
  FROM nvidia/cuda:12.1.0-devel-ubuntu22.04
2
 
3
+ # Set environment variable to avoid interactive prompts
4
+ ENV DEBIAN_FRONTEND=noninteractive
5
+
6
+ # Weasyprint is necessary for pdf printing
7
+ RUN apt-get update && apt-get install -y apt-utils weasyprint
8
 
9
  RUN apt-get install -y python3.11 python3.11-venv python3-pip
10
 
 
70
  RUN sed -i 's/\r$//' /odtp/odtp-component-client/startup.sh
71
  RUN sed -i 's/\r$//' /odtp/odtp-app/app.sh
72
 
73
+ ENTRYPOINT ["bash", "/odtp/odtp-component-client/startup.sh"]
 
 
 
app/app.py CHANGED
@@ -1,19 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import argparse
3
- from typing import Any, Optional, TextIO, List
4
  from pyannote.audio import Pipeline, Audio
5
- import whisper
6
  from whisper.utils import WriteSRT, WriteVTT
7
- from whisper import Whisper
8
- import torch
9
- from math import ceil, floor
10
  import soundfile as sf
11
  import librosa
12
  import json
13
  from dataclasses import dataclass, asdict
14
- from typing import List
15
  from jsonschema import validate, ValidationError
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  @dataclass
19
  class Segment:
@@ -229,9 +396,12 @@ class SegmentsJSONWriter(AppendResultsMixin):
229
  else:
230
  f.write(',\n')
231
  for idx, segment in enumerate(segments):
232
- if idx > 0:
233
- f.write(',\n')
234
- json.dump(asdict(segment), f, ensure_ascii=False, indent=2)
 
 
 
235
  self.first_call = False
236
 
237
  def finalize(self):
@@ -244,57 +414,6 @@ class SegmentsJSONWriter(AppendResultsMixin):
244
  with open(self.output_path, 'a', encoding='utf-8') as f:
245
  f.write('\n]}\n')
246
 
247
- class WhisperFacade:
248
- wmodel: Whisper
249
-
250
- def __init__(self, model:str, *, quantize=False) -> None:
251
- """Load the Whisper model and optionally quantize."""
252
- print("Initialize whisper")
253
- whisper_model = whisper.load_model(model)
254
- if quantize:
255
- print("Quantize")
256
- DTYPE = torch.qint8
257
- qmodel: Whisper = torch.quantization.quantize_dynamic(
258
- whisper_model, {torch.nn.Linear}, dtype=DTYPE)
259
- del whisper_model
260
- self.wmodel = qmodel
261
- else:
262
- self.wmodel = whisper_model
263
-
264
- def _set_timing_for(self, segment: dict[str, float], # simplified typing
265
- offset: float) -> None:
266
- """For speech fragments in different parts of an audio file, patch the
267
- whisper segment and word timing using the offset (typically the diarization offset)
268
- in seconds. This makes the timing accurate for subtitles when multiple
269
- calls to whisper are used for various parts of the audio.
270
- """
271
- s = segment
272
- s['start'] += offset
273
- s['end'] += offset
274
- # Update word start/stop times, if present
275
- if 'words' in s:
276
- w: dict[str, float] # simplified typing
277
- for w in s['words']: # type: ignore
278
- w['start'] += offset
279
- w['end'] += offset
280
-
281
- def load_audio(self, file_path: str):
282
- self.audio = whisper.load_audio(file_path)
283
-
284
- def transcribe(self, *, start: float, end: float, options: dict[str, Any] ) -> dict[str, Any]:
285
- """Transcribe from start time to end time (both in seconds)."""
286
- SAMPLE_RATE = 16_000 # 16kHz audio
287
- start_index = floor(start * SAMPLE_RATE)
288
- end_index = ceil(end * SAMPLE_RATE)
289
- audio_segment = self.audio[start_index:end_index]
290
- result = whisper.transcribe(self.wmodel, audio_segment, **options)
291
- #
292
- segments = result['segments']
293
- s: dict[str, float] # simplified typing
294
- for s in segments: # type: ignore
295
- self._set_timing_for(segment=s, offset=start)
296
- return result
297
-
298
  def clip_audio(audio_file_path, sample_rate, start, end, output_path):
299
  # Ensure the output directory exists
300
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
@@ -309,45 +428,107 @@ def clip_audio(audio_file_path, sample_rate, start, end, output_path):
309
  # Write the audio segment to the output path
310
  sf.write(output_path, waveform[start_sample:end_sample], sr, format='WAV')
311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  def main(args):
313
  diarization, _, sample_rate = diarize_audio(args.hf_token, args.input_file)
314
- model = WhisperFacade(model=args.model, quantize=args.quantize)
315
- model.load_audio(args.input_file)
316
- #
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  writer = WriteSRTIncremental()
318
  writer_json = SegmentsJSONWriter()
319
- whisper_options = {"verbose": None, "word_timestamps": False,
320
- "task": args.task, "suppress_tokens": ""}
 
 
 
 
 
 
321
  if args.language:
 
 
322
  whisper_options["language"] = args.language
323
- writer_options = {"max_line_width":55, "max_line_count":2, "word_timestamps": False}
324
- if args.verbose=="True":
 
 
 
 
 
 
325
  print("Process diarized blocks")
 
326
 
327
- # Group consecutive segments of the same speaker
328
  grouped_segments = []
329
  current_speaker = None
330
- current_start = None
331
- current_end = None
332
 
333
  for turn, _, speaker in diarization.itertracks(yield_label=True):
334
  if args.verbose=="True":
335
  print(speaker)
336
- if turn.end - turn.start < 0.5: # Suppress short utterances (pyannote artifact)
337
- if args.verbose=="True":
338
- print(f"start={turn.start:.1f}s stop={turn.end:.1f}s IGNORED")
339
  continue
340
-
341
  if speaker == current_speaker:
342
  current_end = turn.end
343
  else:
344
  if current_speaker is not None:
345
  grouped_segments.append((current_start, current_end, current_speaker))
346
  current_speaker = speaker
347
- current_start = turn.start
348
- current_end = turn.end
349
 
350
- # Append the last segment
351
  if current_speaker is not None:
352
  grouped_segments.append((current_start, current_end, current_speaker))
353
 
@@ -355,14 +536,33 @@ def main(args):
355
  for start, end, speaker in grouped_segments:
356
  clip_path = f"/tmp/speaker_{speaker}_start_{start:.1f}_end_{end:.1f}.wav"
357
  clip_audio(args.input_file, sample_rate, start, end, clip_path)
358
- result = model.transcribe(start=start, end=end, options=whisper_options)
359
- language = result['language']
 
 
 
360
  if args.verbose=="True":
361
  print(f"start={start:.1f}s stop={end:.1f}s lang={language} {speaker}")
 
 
362
  writer(result, args.output_file, speaker, start, writer_options)
363
- writer_json(generate_segments(result['segments'], speaker, language), args.output_json_file)
 
364
  writer_json.finalize()
365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  if __name__ == '__main__':
367
  parser = argparse.ArgumentParser(description="Diarization and Whisper Transcription CLI")
368
  parser.add_argument('--model', type=str, required=True, help="Whisper model to use")
@@ -372,7 +572,10 @@ if __name__ == '__main__':
372
  parser.add_argument('--language', type=str, required=False, help="Language to use for transcription or translation")
373
  parser.add_argument('--input-file', type=str, required=True, help="Input audio file")
374
  parser.add_argument('--output-file', type=str, required=True, help="Output file for the results (SRT or VTT)")
375
- parser.add_argument('--output-json-file', type=str, required=True, help="Output file for the results (SRT or VTT)")
 
 
 
376
  parser.add_argument('--verbose', type=str, required=False, help="Printing status")
377
 
378
  args = parser.parse_args()
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any, Dict, Optional, TextIO, List
3
+ import whisper
4
+ from whisper import Whisper
5
+ import torch
6
+ from math import floor, ceil
7
+
8
+ from transformers import pipeline
9
+ import numpy as np
10
+ import librosa
11
+ import torch
12
+ from math import floor, ceil
13
+
14
  import os
15
  import argparse
 
16
  from pyannote.audio import Pipeline, Audio
 
17
  from whisper.utils import WriteSRT, WriteVTT
18
+
 
 
19
  import soundfile as sf
20
  import librosa
21
  import json
22
  from dataclasses import dataclass, asdict
 
23
  from jsonschema import validate, ValidationError
24
 
25
+ import createpdf
26
+ import paragraphsCreator
27
+
28
+ from pydub import AudioSegment
29
+ from pytube import YouTube
30
+
31
+
32
+
33
+
34
+
35
+ class ASRFacade(ABC):
36
+ """Abstract base class to define an interface for transcription."""
37
+
38
+ @abstractmethod
39
+ def load_audio(self, file_path: str):
40
+ pass
41
+
42
+ @abstractmethod
43
+ def transcribe(
44
+ self,
45
+ start: float,
46
+ end: float,
47
+ options: Dict[str, Any],
48
+ ) -> Dict[str, Any]:
49
+ """Transcribe a portion of audio from start to end time in seconds."""
50
+ pass
51
+
52
+
53
+
54
+
55
+ class WhisperFacade(ASRFacade):
56
+ wmodel: Whisper
57
+ audio: Any # The loaded audio array from Whisper
58
+
59
+ def __init__(self, model: str, *, quantize=False) -> None:
60
+ print("Initialize Whisper")
61
+ whisper_model = whisper.load_model(model)
62
+ if quantize:
63
+ print("Quantize")
64
+ DTYPE = torch.qint8
65
+ qmodel: Whisper = torch.quantization.quantize_dynamic(
66
+ whisper_model, {torch.nn.Linear}, dtype=DTYPE
67
+ )
68
+ del whisper_model
69
+ self.wmodel = qmodel
70
+ else:
71
+ self.wmodel = whisper_model
72
+
73
+ def load_audio(self, file_path: str):
74
+ self.audio = whisper.load_audio(file_path)
75
+
76
+ def _set_timing_for(self, segment: dict[str, float], offset: float) -> None:
77
+ # Keep this logic the same
78
+ s = segment
79
+ s['start'] += offset
80
+ s['end'] += offset
81
+ if 'words' in s:
82
+ for w in s['words']:
83
+ w['start'] += offset
84
+ w['end'] += offset
85
+
86
+ def transcribe(
87
+ self,
88
+ start: float,
89
+ end: float,
90
+ options: dict[str, Any]
91
+ ) -> dict[str, Any]:
92
+ SAMPLE_RATE = 16_000
93
+ start_index = floor(start * SAMPLE_RATE)
94
+ end_index = ceil(end * SAMPLE_RATE)
95
+
96
+ audio_segment = self.audio[start_index:end_index]
97
+ result = whisper.transcribe(self.wmodel, audio_segment, **options)
98
+
99
+ for s in result['segments']:
100
+ self._set_timing_for(segment=s, offset=start)
101
+
102
+ return result
103
+
104
+
105
+ class TransformersFacade(ASRFacade):
106
+ """Use a Hugging Face ASR pipeline instead of Whisper."""
107
+
108
+ def __init__(self, model_name: str):
109
+ print(f"Initialize Transformers pipeline with model {model_name}")
110
+ self.asr_pipeline = pipeline("automatic-speech-recognition", model=model_name)
111
+ self.sr = 16000
112
+ self.audio_data = None # We'll store the loaded waveform here
113
+
114
+ def load_audio(self, file_path: str):
115
+ # We'll load audio into a single waveform at 16 kHz
116
+ print(f"Loading audio for Transformers from {file_path}")
117
+ waveform, sr = librosa.load(file_path, sr=self.sr, mono=True)
118
+ self.audio_data = waveform
119
+ print(f"Audio loaded: shape={waveform.shape}, sample_rate={sr}")
120
+
121
+ def transcribe(
122
+ self,
123
+ start: float,
124
+ end: float,
125
+ options: dict[str, Any]
126
+ ) -> dict[str, Any]:
127
+ """
128
+ We want to return a structure with 'segments' just like Whisper does.
129
+ We'll treat the entire chunk as a single forward pass to the pipeline.
130
+ If you prefer more advanced chunking or word-level timestamps, you can expand this.
131
+ """
132
+ start_sample = floor(start * self.sr)
133
+ end_sample = ceil(end * self.sr)
134
+ audio_segment = self.audio_data[start_sample:end_sample]
135
+
136
+ # The pipeline can handle direct numpy arrays.
137
+ # Some HF pipelines let you pass 'return_timestamps=True' in generate(), but that may vary by model.
138
+ # We'll do a straightforward approach here:
139
+ transcription = self.asr_pipeline(audio_segment)#, sampling_rate=self.sr)
140
+
141
+ # We want to unify the output structure with whisper-like dict.
142
+ # Example result for consistency:
143
+ result = {
144
+ "language": "ca", # or fetch from pipeline if available
145
+ "segments": [
146
+ {
147
+ "id": 0,
148
+ "start": start,
149
+ "end": end,
150
+ "text": transcription["text"],
151
+ # If you want words/tokens, you can parse them here if your model supports it
152
+ }
153
+ ]
154
+ }
155
+ return result
156
+
157
+ def create_asr_facade(model_name: str, quantize: bool = False) -> ASRFacade:
158
+ """Factory function to return either a Whisper or Transformers facade."""
159
+ # Example: if user requests 'base-ca', we switch to Transformers
160
+ if model_name in ['tiny.en',
161
+ 'tiny',
162
+ 'base.en',
163
+ 'base',
164
+ 'small.en',
165
+ 'small',
166
+ 'medium.en',
167
+ 'medium',
168
+ 'large-v1',
169
+ 'large-v2',
170
+ 'large-v3',
171
+ 'large',
172
+ 'large-v3-turbo',
173
+ 'turbo']:
174
+ return WhisperFacade(model=model_name, quantize=quantize)
175
+ else:
176
+ print(f"Trying to use Transformers model from {model_name}")
177
+ return TransformersFacade(model_name=model_name)
178
+
179
+
180
+
181
+
182
+
183
+ #############################################################################
184
 
185
  @dataclass
186
  class Segment:
 
396
  else:
397
  f.write(',\n')
398
  for idx, segment in enumerate(segments):
399
+ if segment: # Check if the segment is not empty
400
+ if idx > 0:
401
+ f.write(',\n')
402
+
403
+ segment.text = segment.text.strip()
404
+ json.dump(asdict(segment), f, ensure_ascii=False, indent=2)
405
  self.first_call = False
406
 
407
  def finalize(self):
 
414
  with open(self.output_path, 'a', encoding='utf-8') as f:
415
  f.write('\n]}\n')
416
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  def clip_audio(audio_file_path, sample_rate, start, end, output_path):
418
  # Ensure the output directory exists
419
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
 
428
  # Write the audio segment to the output path
429
  sf.write(output_path, waveform[start_sample:end_sample], sr, format='WAV')
430
 
431
+ def convert_mpx_to_wav(file_path):
432
+ if file_path.lower().endswith('.mp3'):
433
+ # Load the MP3 file
434
+ audio = AudioSegment.from_mp3(file_path)
435
+
436
+ elif file_path.lower().endswith('.mp4'):
437
+ audio = AudioSegment.from_mp4(file_path)
438
+
439
+ else:
440
+ raise ValueError("Input file must be an MP3 or MP4 file")
441
+
442
+ # Define the output path
443
+ wav_file_path = os.path.splitext(file_path)[0] + '.wav'
444
+
445
+ # Export as WAV
446
+ audio.export(wav_file_path, format='wav')
447
+
448
+ return wav_file_path
449
+
450
+
451
+ def download_youtube_video(url, output_path='downloads'):
452
+ if url.startswith('http://') or url.startswith('https://'):
453
+ yt = YouTube(url)
454
+ video = yt.streams.filter(only_audio=True).first()
455
+ if not os.path.exists(output_path):
456
+ os.makedirs(output_path)
457
+ output_file = video.download(output_path)
458
+ base, ext = os.path.splitext(output_file)
459
+ new_file = base + '.mp4'
460
+ os.rename(output_file, new_file)
461
+ return new_file
462
+ else:
463
+ raise ValueError("The provided URL is not a valid HTTP link")
464
+
465
+
466
+
467
  def main(args):
468
  diarization, _, sample_rate = diarize_audio(args.hf_token, args.input_file)
469
+
470
+
471
+ if args.input_file.startswith('http://') or args.input_file.startswith('https://'):
472
+ file_path = download_youtube_video(args.input_file, output_path=os.path.dirname(args.output_file))
473
+ file_path = convert_mpx_to_wav(file_path)
474
+ elif args.input_file.lower().endswith('.mp3'):
475
+ file_path = convert_mpx_to_wav(args.input_file)
476
+ elif args.input_file.lower().endswith('.wav'):
477
+ file_path = args.input_file
478
+ elif args.input_file.lower().endswith('.mp4'):
479
+ file_path = convert_mpx_to_wav(args.input_file)
480
+ else:
481
+ raise ValueError("Input file must be an MP3, WAV or MP4 file")
482
+
483
+ # Create the correct ASR facade
484
+ asr_model = create_asr_facade(args.model, quantize=args.quantize)
485
+ asr_model.load_audio(args.input_file)
486
+
487
  writer = WriteSRTIncremental()
488
  writer_json = SegmentsJSONWriter()
489
+
490
+ # Whisper-like transcription options
491
+ whisper_options = {
492
+ "verbose": None,
493
+ "word_timestamps": False,
494
+ "task": args.task,
495
+ "suppress_tokens": ""
496
+ }
497
  if args.language:
498
+ # This is only relevant for Whisper. For Transformers,
499
+ # you might specify a different approach or ignore it.
500
  whisper_options["language"] = args.language
501
+
502
+ writer_options = {
503
+ "max_line_width": 55,
504
+ "max_line_count": 2,
505
+ "word_timestamps": False
506
+ }
507
+
508
+ if args.verbose == "True":
509
  print("Process diarized blocks")
510
+
511
 
 
512
  grouped_segments = []
513
  current_speaker = None
514
+ current_start = None
515
+ current_end = None
516
 
517
  for turn, _, speaker in diarization.itertracks(yield_label=True):
518
  if args.verbose=="True":
519
  print(speaker)
520
+ if turn.end - turn.start < 0.5:
521
+ # ignore short utterances
 
522
  continue
 
523
  if speaker == current_speaker:
524
  current_end = turn.end
525
  else:
526
  if current_speaker is not None:
527
  grouped_segments.append((current_start, current_end, current_speaker))
528
  current_speaker = speaker
529
+ current_start = turn.start
530
+ current_end = turn.end
531
 
 
532
  if current_speaker is not None:
533
  grouped_segments.append((current_start, current_end, current_speaker))
534
 
 
536
  for start, end, speaker in grouped_segments:
537
  clip_path = f"/tmp/speaker_{speaker}_start_{start:.1f}_end_{end:.1f}.wav"
538
  clip_audio(args.input_file, sample_rate, start, end, clip_path)
539
+
540
+ # Important: we call asr_model instead of model
541
+ result = asr_model.transcribe(start=start, end=end, options=whisper_options)
542
+ language = result.get('language', args.language or 'unknown')
543
+
544
  if args.verbose=="True":
545
  print(f"start={start:.1f}s stop={end:.1f}s lang={language} {speaker}")
546
+
547
+ # Use your existing logic to write SRT, JSON, etc.
548
  writer(result, args.output_file, speaker, start, writer_options)
549
+ writer_json(generate_segments(result['segments'], speaker, language), args.output_json_file)
550
+
551
  writer_json.finalize()
552
 
553
+ # If you want to validate JSON, paragraphs, PDF creation, etc.
554
+ paragraphsCreator.process_paragraphs(
555
+ args.output_json_file,
556
+ args.output_paragraphs_json_file,
557
+ 3
558
+ )
559
+ createpdf.convert_json_to_pdf(
560
+ args.output_paragraphs_json_file,
561
+ args.output_md_file,
562
+ args.output_pdf_file
563
+ )
564
+
565
+
566
  if __name__ == '__main__':
567
  parser = argparse.ArgumentParser(description="Diarization and Whisper Transcription CLI")
568
  parser.add_argument('--model', type=str, required=True, help="Whisper model to use")
 
572
  parser.add_argument('--language', type=str, required=False, help="Language to use for transcription or translation")
573
  parser.add_argument('--input-file', type=str, required=True, help="Input audio file")
574
  parser.add_argument('--output-file', type=str, required=True, help="Output file for the results (SRT or VTT)")
575
+ parser.add_argument('--output-json-file', type=str, required=True, help="Output json file.")
576
+ parser.add_argument('--output-paragraphs-json-file', type=str, required=True, help="Output paragraphs file")
577
+ parser.add_argument('--output-md-file', type=str, required=True, help="Output markdown file")
578
+ parser.add_argument('--output-pdf-file', type=str, required=True, help="Output pdf file")
579
  parser.add_argument('--verbose', type=str, required=False, help="Printing status")
580
 
581
  args = parser.parse_args()
app/app.sh CHANGED
@@ -33,18 +33,28 @@
33
  #########################################################
34
 
35
  if [ -n "$LANGUAGE" ]; then
36
- python3 /odtp/odtp-app/app.py --model $MODEL --quantize --hf-token $HF_TOKEN --task $TASK --language $LANGUAGE --input-file /odtp/odtp-input/$INPUT_FILE --output-file /odtp/odtp-output/$OUTPUT_FILE.translate.srt --output-json-file /odtp/odtp-output/$OUTPUT_FILE.translate.json
 
 
 
 
 
 
 
 
 
 
 
37
  else
38
- python3 /odtp/odtp-app/app.py --model $MODEL --quantize --hf-token $HF_TOKEN --task $TASK --input-file /odtp/odtp-input/$INPUT_FILE --output-file /odtp/odtp-output/$OUTPUT_FILE.srt --output-json-file /odtp/odtp-output/$OUTPUT_FILE.json
 
 
 
 
 
 
 
 
 
 
39
  fi
40
-
41
- #########################################################
42
- # 5. OUTPUT FOLDER MANAGEMENT
43
- # The selected output files generated should be placed in the output folder
44
- #########################################################
45
-
46
- # cp -r /odtp/odtp-workdir/output/* /odtp/odtp-output
47
-
48
- ############################################################################################
49
- # END OF MANUAL USER APP
50
- ############################################################################################
 
33
  #########################################################
34
 
35
  if [ -n "$LANGUAGE" ]; then
36
+ python3 /odtp/odtp-app/app.py \
37
+ --model $MODEL \
38
+ --quantize \
39
+ --hf-token $HF_TOKEN \
40
+ --task $TASK \
41
+ --language $LANGUAGE \
42
+ --input-file /odtp/odtp-input/$INPUT_FILE \
43
+ --output-file /odtp/odtp-output/$OUTPUT_FILE.srt \
44
+ --output-json-file /odtp/odtp-output/$OUTPUT_FILE.json \
45
+ --output-paragraphs-json-file /odtp/odtp-output/$OUTPUT_FILE-paragraphs.json \
46
+ --output-md-file /odtp/odtp-output/$OUTPUT_FILE.md \
47
+ --output-pdf-file /odtp/odtp-output/$OUTPUT_FILE.pdf
48
  else
49
+ python3 /odtp/odtp-app/app.py \
50
+ --model $MODEL \
51
+ --quantize \
52
+ --hf-token $HF_TOKEN \
53
+ --task $TASK \
54
+ --input-file /odtp/odtp-input/$INPUT_FILE \
55
+ --output-file /odtp/odtp-output/$OUTPUT_FILE.srt \
56
+ --output-json-file /odtp/odtp-output/$OUTPUT_FILE.json \
57
+ --output-paragraphs-json-file /odtp/odtp-output/$OUTPUT_FILE-paragraphs.json \
58
+ --output-md-file /odtp/odtp-output/$OUTPUT_FILE.md \
59
+ --output-pdf-file /odtp/odtp-output/$OUTPUT_FILE.pdf
60
  fi
 
 
 
 
 
 
 
 
 
 
 
app/createpdf.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import argparse
4
+ from md2pdf.core import md2pdf
5
+
6
+ def json_to_markdown(json_data, filename):
7
+ """
8
+ Convert JSON data into a Markdown string.
9
+
10
+ :param json_data: List of dictionaries each containing
11
+ "start", "end", "text", "speaker", "language"
12
+ :param filename: The name of the JSON file (used as the main title)
13
+ :return: A string of valid Markdown
14
+ """
15
+
16
+ # Title
17
+ title = filename.replace("-paragraphs.json", "")
18
+ md_output = f"# {title}\n\n"
19
+
20
+ # Transcription Header
21
+ md_output += "## Transcription\n\n"
22
+
23
+ # Build the content for each contribution
24
+ for entry in json_data:
25
+ speaker = entry.get("speaker", "Unknown Speaker")
26
+ language = entry.get("language", "Unknown Language")
27
+ start = entry.get("start", "")
28
+ end = entry.get("end", "")
29
+ text = entry.get("text", "")
30
+
31
+ # Speaker-language heading
32
+ md_output += f"### {speaker} - {language}\n\n"
33
+
34
+ # Table for start/end times
35
+ md_output += "| start | end |\n"
36
+ md_output += "|-------------|-------------|\n"
37
+ md_output += f"| {start} | {end} |\n\n"
38
+
39
+ # Add the text below the table
40
+ md_output += f"{text}\n\n"
41
+
42
+ return md_output
43
+
44
+
45
+ def save_markdown_to_file(markdown_text, output_md_path):
46
+ """
47
+ Save the Markdown text to a file.
48
+
49
+ :param markdown_text: The Markdown content as a string.
50
+ :param output_md_path: The path (including filename) where the .md should be saved.
51
+ """
52
+ with open(output_md_path, 'w', encoding='utf-8') as md_file:
53
+ md_file.write(markdown_text)
54
+
55
+
56
+ def markdown_to_pdf(markdown_text, output_pdf_path):
57
+ """
58
+ Convert a Markdown string to a PDF using md2pdf.
59
+
60
+ :param markdown_text: The Markdown text to convert.
61
+ :param output_pdf_path: The path (including filename) where the PDF should be saved.
62
+ """
63
+ # Custom CSS (adapt as you like)
64
+ custom_css = r"""
65
+ @font-face {
66
+ font-family: "Bitstream Vera Serif Bold";
67
+ src: url("https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf");
68
+ }
69
+
70
+ body {
71
+ margin: 0 auto;
72
+ background-color: white;
73
+ font-family: "Bitstream Vera Serif Bold";
74
+ color: #333333;
75
+ line-height: 1;
76
+ max-width: 800px;
77
+ padding: 30px;
78
+ font-size: 12px;
79
+ }
80
+
81
+ p {
82
+ line-height: 150%;
83
+ max-width: 960px;
84
+ font-weight: 400;
85
+ color: #333333;
86
+ }
87
+
88
+ h1,
89
+ h2,
90
+ h3,
91
+ h4 {
92
+ font-weight: 400;
93
+ }
94
+
95
+ h2,
96
+ h3,
97
+ h4,
98
+ h5,
99
+ p {
100
+ margin-bottom: 25px;
101
+ padding: 0;
102
+ }
103
+
104
+ h1 {
105
+ margin-bottom: 10px;
106
+ font-size: 300%;
107
+ padding: 0px;
108
+ }
109
+
110
+ h2 {
111
+ font-size: 150%;
112
+ }
113
+
114
+ h3 {
115
+ font-size: 120%;
116
+ }
117
+
118
+ h4 {
119
+ font-size: 100%;
120
+ }
121
+
122
+ h5 {
123
+ font-size: 80%;
124
+ font-weight: 100;
125
+ }
126
+
127
+ h6 {
128
+ font-size: 80%;
129
+ font-weight: 100;
130
+ color: red;
131
+ }
132
+
133
+ a {
134
+ color: grey;
135
+ margin: 0;
136
+ padding: 0;
137
+ vertical-align: baseline;
138
+ }
139
+
140
+ a:hover {
141
+ text-decoration: blink;
142
+ color: green;
143
+ }
144
+
145
+ a:visited {
146
+ color: black;
147
+ }
148
+
149
+ ul,
150
+ ol {
151
+ padding: 0;
152
+ margin: 0px 0px 0px 50px;
153
+ }
154
+
155
+ ul {
156
+ list-style-type: square;
157
+ list-style-position: inside;
158
+ }
159
+
160
+ li {
161
+ line-height: 150%;
162
+ }
163
+
164
+ li ul,
165
+ li ul {
166
+ margin-left: 24px;
167
+ }
168
+
169
+ pre {
170
+ padding: 0px 24px;
171
+ max-width: 800px;
172
+ white-space: pre-wrap;
173
+ }
174
+
175
+ code {
176
+ font-family: Consolas, Monaco, Andale Mono, monospace;
177
+ line-height: 1.5;
178
+ font-size: 13px;
179
+ }
180
+
181
+ aside {
182
+ display: block;
183
+ float: right;
184
+ width: 390px;
185
+ }
186
+
187
+ blockquote {
188
+ border-left: 0.5em solid #eee;
189
+ padding: 0 1em;
190
+ margin-left: 0;
191
+ max-width: 476px;
192
+ }
193
+
194
+ blockquote cite {
195
+ line-height: 20px;
196
+ color: #bfbfbf;
197
+ }
198
+
199
+ blockquote cite:before {
200
+ content: "\2014 \00A0";
201
+ }
202
+
203
+ blockquote p {
204
+ color: #666;
205
+ max-width: 460px;
206
+ }
207
+
208
+ hr {
209
+ text-align: left;
210
+ margin: 0 auto 0 0;
211
+ color: #999;
212
+ }
213
+
214
+ /* Table styling for the start/end times */
215
+ table {
216
+ border-collapse: collapse;
217
+ width: 100%;
218
+ margin-bottom: 15px;
219
+ }
220
+ table, th, td {
221
+ border: 1px solid #333;
222
+ padding: 6px;
223
+ }
224
+ th {
225
+ background-color: #eee;
226
+ text-align: left;
227
+ }
228
+ """
229
+
230
+ # Convert Markdown to PDF directly from a string
231
+ md2pdf(output_pdf_path,
232
+ md_content=markdown_text, # We pass the markdown text as "raw"
233
+ css_file_path=None, # If you have an external .css file, you can pass its path here
234
+ base_url=None # If you have images or relative links
235
+ )
236
+
237
+ # If we want the custom CSS from a file, we can write it to a temporary file:
238
+ # Optionally, you could do something like this:
239
+ #
240
+ # with open("custom_style.css", "w", encoding="utf-8") as css_file:
241
+ # css_file.write(custom_css)
242
+ #
243
+ # md2pdf(output_pdf_path,
244
+ # raw=markdown_text,
245
+ # css="custom_style.css", # pass the CSS file path
246
+ # extras=[],
247
+ # base_url=None
248
+ # )
249
+
250
+
251
+ def parse_args():
252
+ """
253
+ Parse command-line arguments using argparse.
254
+ """
255
+ parser = argparse.ArgumentParser(
256
+ description="Convert a JSON-based transcription to Markdown and then to PDF using md2pdf."
257
+ )
258
+ parser.add_argument("input_filename",
259
+ help="Path to the JSON file containing transcription data.")
260
+ parser.add_argument("--output_md",
261
+ default="transcription.md",
262
+ help="Output Markdown file name (default: transcription.md)")
263
+ parser.add_argument("--output_pdf",
264
+ default="transcription.pdf",
265
+ help="Output PDF file name (default: transcription.pdf)")
266
+ return parser.parse_args()
267
+
268
+ def convert_json_to_pdf(input_filename, output_md="transcription.md", output_pdf="transcription.pdf"):
269
+ # 1. Read JSON data
270
+ with open(input_filename, 'r', encoding='utf-8') as f:
271
+ data = json.load(f)
272
+
273
+ # 2. Convert JSON to Markdown
274
+ markdown_output = json_to_markdown(data, os.path.basename(input_filename))
275
+
276
+ # 3. Save Markdown to a .md file (optional)
277
+ save_markdown_to_file(markdown_output, output_md)
278
+ print(f"Markdown saved to: {output_md}")
279
+
280
+ # 4. Convert Markdown to PDF using md2pdf
281
+ markdown_to_pdf(markdown_output, output_pdf)
282
+ print(f"PDF generated and saved to: {output_pdf}")
283
+
284
+ def main():
285
+ args = parse_args()
286
+ convert_json_to_pdf(args.input_filename, args.output_md, args.output_pdf)
287
+
288
+ if __name__ == '__main__':
289
+ main()
app/gradio_app.py CHANGED
@@ -22,7 +22,7 @@ def cleanup_temp(temp_dir):
22
  """Remove temporary folder structure"""
23
  shutil.rmtree(temp_dir)
24
 
25
- def process_audio(audio_file, model, task, language, hf_token):
26
  """Process audio file with Whisper and Pyannote"""
27
  # Create temp structure
28
  temp_dir = create_temp_structure()
@@ -97,7 +97,7 @@ with gr.Blocks() as demo:
97
  label="Upload Audio File (WAV format)"
98
  )
99
  model = gr.Dropdown(
100
- choices=["tiny", "base", "small", "medium", "large", "large-v2"],
101
  value="base",
102
  label="Whisper Model"
103
  )
@@ -107,13 +107,14 @@ with gr.Blocks() as demo:
107
  label="Task"
108
  )
109
  language = gr.Dropdown(
110
- choices=["auto", "en", "es", "fr", "de", "it", "pt", "nl", "ja", "zh", "ru"],
111
  value="auto",
112
  label="Source Language"
113
  )
114
  hf_token = gr.Textbox(
115
  label="Hugging Face Token",
116
- type="password"
 
117
  )
118
  submit_btn = gr.Button("Process Audio")
119
 
@@ -145,15 +146,17 @@ with gr.Blocks() as demo:
145
  outputs=[information, srt_output, json_output, srt_download, json_download]
146
  )
147
 
 
 
148
  if __name__ == "__main__":
149
- demo.launch(
 
 
 
 
150
  server_name="0.0.0.0", # More secure default for development
151
  server_port=7860, # Default Gradio port
152
- share=False, # Disable temporary public URL
153
  show_error=True, # Show detailed error messages
154
  debug=True # Enable debug mode for development
155
- )
156
-
157
-
158
-
159
- # TODO: Slow printing on the command.
 
22
  """Remove temporary folder structure"""
23
  shutil.rmtree(temp_dir)
24
 
25
+ def process_audio(audio_file, model, task, language, hf_token=None):
26
  """Process audio file with Whisper and Pyannote"""
27
  # Create temp structure
28
  temp_dir = create_temp_structure()
 
97
  label="Upload Audio File (WAV format)"
98
  )
99
  model = gr.Dropdown(
100
+ choices=["tiny", "base", "small", "medium", "large", "large-v2", "large-v3", "large-v3-turbo", "softcatala/whisper-base-ca", "projecte-aina/whisper-large-v3-ca-3catparla"],
101
  value="base",
102
  label="Whisper Model"
103
  )
 
107
  label="Task"
108
  )
109
  language = gr.Dropdown(
110
+ choices=["auto", "en", "es", "ca", "fr", "de", "it", "pt", "nl", "ja", "zh", "ru"],
111
  value="auto",
112
  label="Source Language"
113
  )
114
  hf_token = gr.Textbox(
115
  label="Hugging Face Token",
116
+ type="password",
117
+ placeholder="Leave blank if not applicable"
118
  )
119
  submit_btn = gr.Button("Process Audio")
120
 
 
146
  outputs=[information, srt_output, json_output, srt_download, json_download]
147
  )
148
 
149
+ import argparse
150
+
151
  if __name__ == "__main__":
152
+ parser = argparse.ArgumentParser(description="Launch Gradio app with optional sharing.")
153
+ parser.add_argument('--share', action='store_true', help="Enable sharing the app with a public URL.")
154
+ args = parser.parse_args()
155
+
156
+ demo.queue().launch(
157
  server_name="0.0.0.0", # More secure default for development
158
  server_port=7860, # Default Gradio port
159
+ share=args.share, # Disable temporary public URL
160
  show_error=True, # Show detailed error messages
161
  debug=True # Enable debug mode for development
162
+ )
 
 
 
 
app/odtp-output.md ADDED
The diff for this file is too large to render. See raw diff
 
app/paragraphsCreator.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import sys
3
+
4
+ def seconds_to_hhmmss(seconds):
5
+ hours = seconds // 3600
6
+ minutes = (seconds % 3600) // 60
7
+ seconds = seconds % 60
8
+ return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}"
9
+
10
+
11
+ def create_paragraphs(annotations, min_gap=4):
12
+ paragraphs = []
13
+ current_paragraph = {}
14
+ prev_end_time = None
15
+ prev_speaker = None
16
+
17
+ for item in annotations:
18
+ start_time = item["start"]
19
+ end_time = item["end"]
20
+ transcript = item["text"]
21
+ speaker = item["speaker"]
22
+ language = item["language"]
23
+
24
+ # Start a new paragraph if timestamp gap exceeds min_gap or speaker changes
25
+ if (
26
+ prev_end_time is not None
27
+ and (
28
+ start_time - prev_end_time > min_gap
29
+ or speaker != prev_speaker
30
+ or (current_paragraph and language != current_paragraph["language"])
31
+ )
32
+ ):
33
+ paragraphs.append(current_paragraph)
34
+ current_paragraph = []
35
+
36
+ if current_paragraph:
37
+ current_paragraph["text"] += " " + transcript
38
+ current_paragraph["end"] = seconds_to_hhmmss(end_time)
39
+ else:
40
+ current_paragraph = {
41
+ "start": seconds_to_hhmmss(start_time),
42
+ "end": seconds_to_hhmmss(end_time),
43
+ "text": transcript,
44
+ "speaker": speaker,
45
+ "language": language
46
+ }
47
+
48
+ prev_end_time = end_time
49
+ prev_speaker = speaker
50
+
51
+ if current_paragraph:
52
+ paragraphs.append(current_paragraph)
53
+
54
+ return paragraphs
55
+
56
+ def process_paragraphs(input_file, output_file, min_gap):
57
+ with open(input_file, 'r') as f:
58
+ json_data = json.load(f)
59
+
60
+ annotations = json_data["segments"]
61
+
62
+ result = create_paragraphs(annotations, min_gap)
63
+
64
+ with open(output_file, 'w') as f:
65
+ json.dump(result, f, indent=4)
66
+
67
+ def main():
68
+ if len(sys.argv) != 4:
69
+ print("Usage: python paragraphsCreator.py <input_json_file> <output_json_file> <min_gap>")
70
+ sys.exit(1)
71
+
72
+ input_file = sys.argv[1]
73
+ output_file = sys.argv[2]
74
+ min_gap = int(sys.argv[3])
75
+
76
+ process_paragraphs(input_file, output_file, min_gap)
77
+
78
+ if __name__ == "__main__":
79
+ main()
requirements.txt CHANGED
@@ -1,9 +1,13 @@
1
  --extra-index-url https://download.pytorch.org/whl/cu121
2
- torch
3
- torchaudio
4
- openai-whisper
5
- pyannote.audio
6
- soundfile
7
- librosa
8
- jsonschema
9
- gradio
 
 
 
 
 
1
  --extra-index-url https://download.pytorch.org/whl/cu121
2
+ torch==2.5.1
3
+ torchaudio==2.5.1
4
+ openai-whisper==20240930
5
+ pyannote.audio==3.3.2
6
+ soundfile==0.12.1
7
+ librosa==0.10.2.post1
8
+ jsonschema==4.23.0
9
+ gradio==5.5.0
10
+ numpy==1.24.4
11
+ md2pdf==1.0.1
12
+ transformers==4.48.0
13
+ pytube