From d086d8b82a16a5518499409e08fe5add33ea2ac6 Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 18 Sep 2024 14:26:36 -0500 Subject: [PATCH 01/23] First Changes for Whisper --- ClassTranscribeDatabase/global.json | 2 +- ClassTranscribeServer/global.json | 2 +- PythonRpcServer/server.py | 9 + TaskEngine/Program.cs | 5 +- ...ptionTask.cs => AzureTranscriptionTask.cs} | 6 +- TaskEngine/Tasks/ConvertVideoToWavTask.cs | 9 +- TaskEngine/Tasks/LocalTranscriptionTask.cs | 185 ++++++++++++++++++ TaskEngine/Tasks/QueueAwakerTask.cs | 4 +- TaskEngine/Tasks/SceneDetectionTask.cs | 6 +- TaskEngine/TempCode.cs | 6 +- TaskEngine/global.json | 2 +- ct.proto | 9 + 12 files changed, 224 insertions(+), 21 deletions(-) rename TaskEngine/Tasks/{TranscriptionTask.cs => AzureTranscriptionTask.cs} (98%) create mode 100644 TaskEngine/Tasks/LocalTranscriptionTask.cs diff --git a/ClassTranscribeDatabase/global.json b/ClassTranscribeDatabase/global.json index 4100a4a8..215288b9 100644 --- a/ClassTranscribeDatabase/global.json +++ b/ClassTranscribeDatabase/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.201" + "version": "8.0" } } \ No newline at end of file diff --git a/ClassTranscribeServer/global.json b/ClassTranscribeServer/global.json index a679dd12..215288b9 100644 --- a/ClassTranscribeServer/global.json +++ b/ClassTranscribeServer/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.401" + "version": "8.0" } } \ No newline at end of file diff --git a/PythonRpcServer/server.py b/PythonRpcServer/server.py index 943c2c5c..bb880957 100644 --- a/PythonRpcServer/server.py +++ b/PythonRpcServer/server.py @@ -41,6 +41,15 @@ def LogWorker(logId, worker): class PythonServerServicer(ct_pb2_grpc.PythonServerServicer): + def CaptionRPC(self, request, context): + #See CaptionRequest + print( f"CaptionRPC({request.logId};{request.refId};{request.filePath};{request.phraseHints};{request.courseHints};{request.outputLanguages})") + kalturaprovider = KalturaProvider() + result = LogWorker(f"CaptionRPC({request.filePath})", lambda: kalturaprovider.getCaptions(request.refId)) + return ct_pb2.JsonString(json = result) + + + def GetScenesRPC(self, request, context): raise NotImplementedError('Implementation now in pyapi') # res = scenedetector.find_scenes(request.filePath) diff --git a/TaskEngine/Program.cs b/TaskEngine/Program.cs index a8e1c405..ebe513e5 100644 --- a/TaskEngine/Program.cs +++ b/TaskEngine/Program.cs @@ -81,7 +81,8 @@ public static void SetupServices() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() // .AddSingleton() .AddSingleton() @@ -175,7 +176,7 @@ static void createTaskQueues() { // Transcription Related _logger.LogInformation($"Creating TranscriptionTask consumers. Concurrency={concurrent_transcriptions} "); - _serviceProvider.GetService().Consume(concurrent_transcriptions); + _serviceProvider.GetService().Consume(concurrent_transcriptions); // no more! - _serviceProvider.GetService().Consume(concurrent_transcriptions); diff --git a/TaskEngine/Tasks/TranscriptionTask.cs b/TaskEngine/Tasks/AzureTranscriptionTask.cs similarity index 98% rename from TaskEngine/Tasks/TranscriptionTask.cs rename to TaskEngine/Tasks/AzureTranscriptionTask.cs index 217fa633..6bf250ad 100644 --- a/TaskEngine/Tasks/TranscriptionTask.cs +++ b/TaskEngine/Tasks/AzureTranscriptionTask.cs @@ -21,7 +21,7 @@ namespace TaskEngine.Tasks /// This task produces the transcriptions for a Video item. /// [SuppressMessage("Microsoft.Performance", "CA1812:MarkMembersAsStatic")] // This class is never directly instantiated - class TranscriptionTask : RabbitMQTask + class AzureTranscriptionTask : RabbitMQTask { private readonly MSTranscriptionService _msTranscriptionService; @@ -29,9 +29,9 @@ class TranscriptionTask : RabbitMQTask private readonly CaptionQueries _captionQueries; - public TranscriptionTask(RabbitMQConnection rabbitMQ, MSTranscriptionService msTranscriptionService, + public AzureTranscriptionTask(RabbitMQConnection rabbitMQ, MSTranscriptionService msTranscriptionService, // GenerateVTTFileTask generateVTTFileTask, - ILogger logger, CaptionQueries captionQueries) + ILogger logger, CaptionQueries captionQueries) : base(rabbitMQ, TaskType.TranscribeVideo, logger) { _msTranscriptionService = msTranscriptionService; diff --git a/TaskEngine/Tasks/ConvertVideoToWavTask.cs b/TaskEngine/Tasks/ConvertVideoToWavTask.cs index a8e2f363..5ec7475c 100644 --- a/TaskEngine/Tasks/ConvertVideoToWavTask.cs +++ b/TaskEngine/Tasks/ConvertVideoToWavTask.cs @@ -21,13 +21,13 @@ namespace TaskEngine.Tasks class ConvertVideoToWavTask : RabbitMQTask { private readonly RpcClient _rpcClient; - private readonly TranscriptionTask _transcriptionTask; + private readonly LocalTranscriptionTask _localTranscriptionTask; - public ConvertVideoToWavTask(RabbitMQConnection rabbitMQ, RpcClient rpcClient, TranscriptionTask transcriptionTask, ILogger logger) + public ConvertVideoToWavTask(RabbitMQConnection rabbitMQ, RpcClient rpcClient, LocalTranscriptionTask localTranscriptionTask, ILogger logger) : base(rabbitMQ, TaskType.ConvertMedia, logger) { _rpcClient = rpcClient; - _transcriptionTask = transcriptionTask; + _localTranscriptionTask = localTranscriptionTask; } protected override Task OnConsume(string videoId, TaskParameters taskParameters, ClientActiveTasks cleanup) @@ -72,11 +72,10 @@ private async Task OldOnConsumeNotUsed(string videoId) videoLatest.Audio = fileRecord; await _context.SaveChangesAsync(); - // If no transcriptions present, produce transcriptions. if (!videoLatest.Transcriptions.Any()) { - _transcriptionTask.Publish(videoLatest.Id); + _localTranscriptionTask.Publish(videoLatest.Id); } } } diff --git a/TaskEngine/Tasks/LocalTranscriptionTask.cs b/TaskEngine/Tasks/LocalTranscriptionTask.cs new file mode 100644 index 00000000..dc8b5629 --- /dev/null +++ b/TaskEngine/Tasks/LocalTranscriptionTask.cs @@ -0,0 +1,185 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Grpc.Core; +using Newtonsoft.Json.Linq; + + +using ClassTranscribeDatabase; +using ClassTranscribeDatabase.Models; +using ClassTranscribeDatabase.Services; + +using static ClassTranscribeDatabase.CommonUtils; + +#pragma warning disable CA2007 +// https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2007 +// We are okay awaiting on a task in the same thread + +namespace TaskEngine.Tasks +{ + /// + /// This task produces the transcriptions for a Video item. + /// + [SuppressMessage("Microsoft.Performance", "CA1812:MarkMembersAsStatic")] // This class is never directly instantiated + class LocalTranscriptionTask : RabbitMQTask + { + + private readonly CaptionQueries _captionQueries; + private readonly RpcClient _rpcClient; + + + public LocalTranscriptionTask(RabbitMQConnection rabbitMQ, + RpcClient rpcClient, + // GenerateVTTFileTask generateVTTFileTask, + ILogger logger, CaptionQueries captionQueries) + : base(rabbitMQ, TaskType.TranscribeVideo, logger) + { + _rpcClient = rpcClient; + _captionQueries = captionQueries; + } + + protected async override Task OnConsume(string videoId, TaskParameters taskParameters, ClientActiveTasks cleanup) + { + RegisterTask(cleanup, videoId); // may throw AlreadyInProgress exception + + const string SOURCEINTERNALREF= "ClassTranscribe/Local"; // Do not change me; this is a key inside the database + // to indicate the source of the captions was this code + + + using (var _context = CTDbContext.CreateDbContext()) + { + + // TODO: taskParameters.Force should wipe all captions and reset the Transcription Status + + Video video = await _context.Videos.Include(v => v.Video1).Where(v => v.Id == videoId).FirstAsync(); + // ! Note the 'Include' ; we don't build the whole tree of related Entities + + if (video.TranscriptionStatus == Video.TranscriptionStatusMessages.NOERROR) + { + GetLogger().LogInformation($"{videoId}:Skipping Transcribing of- already complete"); + return; + } + var medias = await _context.Medias.Include(m=>m.Playlist).Where(m=>m.VideoId == videoId && m.Playlist != null).ToListAsync(); + if(medias.Count == 0) { + GetLogger().LogInformation($"{videoId}:Skipping Transcribing - no media / playlist cares about this video"); + return; + } + + GetLogger().LogInformation($"{videoId}: Has new Phrase Hints: {video.HasPhraseHints()}"); + + string phraseHints = ""; + if (video.HasPhraseHints()) { + var data = await _context.TextData.FindAsync(video.PhraseHintsDataId); + phraseHints = data.Text; + } else + { // deprecated + phraseHints = video.PhraseHints ?? ""; + } + + GetLogger().LogInformation($"{videoId}:Using Phrase Hints length = {phraseHints.Length}"); + // GetKey can throw if the video.Id is currently being transcribed + // However registerTask should have already detected that + var key = TaskEngineGlobals.KeyProvider.GetKey(video.Id); + + video.TranscribingAttempts += 10; + await _context.SaveChangesAsync(); + GetLogger().LogInformation($"{videoId}: Updated TranscribingAttempts = {video.TranscribingAttempts}"); + try + { + + GetLogger().LogInformation($"{videoId}: Calling RecognitionWithVideoStreamAsync"); + + var request = new CTGrpc.CaptionRequest + { + LogId = videoId, + FilePath = video.Video1.VMPath, + PhraseHints = phraseHints, + CourseHints = "", + OutputLanguages = "en" + }; + var jsonString = ""; + try { + jsonString = (await _rpcClient.PythonServerClient.CaptionRPCAsync(request)).Json; + } + catch (RpcException e) + { + if (e.Status.StatusCode == StatusCode.InvalidArgument) + { + GetLogger().LogError($"CaptionRPC=({videoId}):{e.Message}"); + } + return; + } finally { + GetLogger().LogInformation($"{videoId} Caption - rpc complete"); + TaskEngineGlobals.KeyProvider.ReleaseKey(key, video.Id); + } + JArray jArray = JArray.Parse(jsonString); + + foreach (var captionsInLanguage in jArray) + { + var theLanguage = captionsInLanguage["Lang"].ToString(Newtonsoft.Json.Formatting.None); + var theCaptionsAsJson = captionsInLanguage["Captions"]; + + var theCaptions = new List(); + int cueCount = 0; + // Fix the next line of code + + foreach (var jsonCue in theCaptionsAsJson) { + var caption = new Caption() { + Index = cueCount ++, + Begin = TimeSpan.Parse(jsonCue["start"].ToString(Newtonsoft.Json.Formatting.None)), + End = TimeSpan.Parse(jsonCue["end"].ToString(Newtonsoft.Json.Formatting.None)) , + Text = jsonCue["text"] .ToString(Newtonsoft.Json.Formatting.None) + }; + + theCaptions.Add(caption); + } + if (theCaptions.Count > 0) + { + + var t = _context.Transcriptions.SingleOrDefault(t => t.VideoId == video.Id && t.SourceInternalRef == SOURCEINTERNALREF && t.Language == theLanguage && t.TranscriptionType == TranscriptionType.Caption); + GetLogger().LogInformation($"Find Existing Transcriptions null={t == null}"); + // Did we get the default or an existing Transcription entity? + if (t == null) + { + t = new Transcription() + { + TranscriptionType = TranscriptionType.Caption, + Captions = theCaptions, + Language = theLanguage, + VideoId = video.Id, + Label = $"{theLanguage} (ClassTranscribe)", + SourceInternalRef = SOURCEINTERNALREF, // + SourceLabel = "ClassTranscribe (Local" + (phraseHints.Length>0 ?" with phrase hints)" : ")") + }; + _context.Add(t); + } + else + { + t.Captions.AddRange(theCaptions); + } + } + } + + video.TranscriptionStatus = "NoError"; + // video.JsonMetadata["LastSuccessfulTime"] = result.LastSuccessTime.ToString(); + + // GetLogger().LogInformation($"{videoId}: Saving captions Code={result.ErrorCode}. LastSuccessTime={result.LastSuccessTime}"); + await _context.SaveChangesAsync(); + } + catch (Exception ex) + { + GetLogger().LogError(ex, $"{videoId}: Transcription Exception:${ex.StackTrace}"); + video.TranscribingAttempts += 1000; + await _context.SaveChangesAsync(); + throw; + } + + } + } + + } +} \ No newline at end of file diff --git a/TaskEngine/Tasks/QueueAwakerTask.cs b/TaskEngine/Tasks/QueueAwakerTask.cs index ed1d7225..3c4b0d2d 100644 --- a/TaskEngine/Tasks/QueueAwakerTask.cs +++ b/TaskEngine/Tasks/QueueAwakerTask.cs @@ -22,7 +22,7 @@ class QueueAwakerTask : RabbitMQTask private readonly DownloadPlaylistInfoTask _downloadPlaylistInfoTask; private readonly DownloadMediaTask _downloadMediaTask; // private readonly ConvertVideoToWavTask _convertVideoToWavTask; - private readonly TranscriptionTask _transcriptionTask; + private readonly LocalTranscriptionTask _transcriptionTask; // nope private readonly GenerateVTTFileTask _generateVTTFileTask; private readonly ProcessVideoTask _processVideoTask; private readonly SceneDetectionTask _sceneDetectionTask; @@ -39,7 +39,7 @@ public QueueAwakerTask() { } public QueueAwakerTask(RabbitMQConnection rabbitMQ, DownloadPlaylistInfoTask downloadPlaylistInfoTask, DownloadMediaTask downloadMediaTask, - TranscriptionTask transcriptionTask, ProcessVideoTask processVideoTask, + LocalTranscriptionTask transcriptionTask, ProcessVideoTask processVideoTask, // GenerateVTTFileTask generateVTTFileTask, SceneDetectionTask sceneDetectionTask, CreateBoxTokenTask createBoxTokenTask,// UpdateBoxTokenTask updateBoxTokenTask, diff --git a/TaskEngine/Tasks/SceneDetectionTask.cs b/TaskEngine/Tasks/SceneDetectionTask.cs index 1baf80c0..711bc92c 100644 --- a/TaskEngine/Tasks/SceneDetectionTask.cs +++ b/TaskEngine/Tasks/SceneDetectionTask.cs @@ -19,13 +19,13 @@ namespace TaskEngine.Tasks class SceneDetectionTask : RabbitMQTask { private readonly RpcClient _rpcClient; - private readonly TranscriptionTask _transcriptionTask; + private readonly LocalTranscriptionTask _transcriptionTask; - public SceneDetectionTask(RabbitMQConnection rabbitMQ,TranscriptionTask transcriptionTask, RpcClient rpcClient, ILogger logger) + public SceneDetectionTask(RabbitMQConnection rabbitMQ,LocalTranscriptionTask localTanscriptionTask, RpcClient rpcClient, ILogger logger) : base(rabbitMQ, TaskType.SceneDetection, logger) { _rpcClient = rpcClient; - _transcriptionTask = transcriptionTask; + _transcriptionTask = localTanscriptionTask; } /// Extracts scene information for a video. /// Beware: It is possible to start another scene task while the first one is still running diff --git a/TaskEngine/TempCode.cs b/TaskEngine/TempCode.cs index 896d2f72..34af0142 100644 --- a/TaskEngine/TempCode.cs +++ b/TaskEngine/TempCode.cs @@ -24,7 +24,7 @@ class TempCode private readonly PythonCrawlerTask _pythonCrawlerTask; private readonly ProcessVideoTask _processVideoTask; // private readonly GenerateVTTFileTask _generateVTTFileTask; - private readonly TranscriptionTask _transcriptionTask; + private readonly LocalTranscriptionTask _transcriptionTask; private readonly ConvertVideoToWavTask _convertVideoToWavTask; private readonly DownloadMediaTask _downloadMediaTask; private readonly DownloadPlaylistInfoTask _downloadPlaylistInfoTask; @@ -34,7 +34,7 @@ class TempCode public TempCode(CTDbContext c, CreateBoxTokenTask createBoxTokenTask, //UpdateBoxTokenTask updateBoxTokenTask, SceneDetectionTask ePubGeneratorTask, ProcessVideoTask processVideoTask, - TranscriptionTask transcriptionTask, ConvertVideoToWavTask convertVideoToWavTask, DownloadMediaTask downloadMediaTask, + LocalTranscriptionTask localTranscriptionTask, ConvertVideoToWavTask convertVideoToWavTask, DownloadMediaTask downloadMediaTask, DownloadPlaylistInfoTask downloadPlaylistInfoTask, QueueAwakerTask queueAwakerTask, CleanUpElasticIndexTask cleanUpElasticIndexTask, RpcClient rpcClient, PythonCrawlerTask pythonCrawlerTask) @@ -45,7 +45,7 @@ public TempCode(CTDbContext c, CreateBoxTokenTask createBoxTokenTask, //UpdateBo _sceneDetectionTask = ePubGeneratorTask; _processVideoTask = processVideoTask; // _generateVTTFileTask = generateVTTFileTask; - _transcriptionTask = transcriptionTask; + _transcriptionTask = localTranscriptionTask; _convertVideoToWavTask = convertVideoToWavTask; _downloadMediaTask = downloadMediaTask; _downloadPlaylistInfoTask = downloadPlaylistInfoTask; diff --git a/TaskEngine/global.json b/TaskEngine/global.json index a679dd12..215288b9 100644 --- a/TaskEngine/global.json +++ b/TaskEngine/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.401" + "version": "8.0" } } \ No newline at end of file diff --git a/ct.proto b/ct.proto index 512975ec..9ee01680 100644 --- a/ct.proto +++ b/ct.proto @@ -20,8 +20,17 @@ service PythonServer { rpc ComputeFileHash (FileHashRequest) returns (FileHashResponse) {} rpc GetMediaInfoRPC(File) returns (JsonString) {} + + rpc CaptionRPC(CaptionRequest) returns (JsonString) {} } +message CaptionRequest { + string logId = 1; + string filePath = 2; + string phraseHints = 3; + string courseHints = 4; + string outputLanguages = 5; +} // The request message containing the user's name. message JsonString { From 1615c68e2eeeffe76371bbacaee3d85a0b770f93 Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 18 Sep 2024 14:26:49 -0500 Subject: [PATCH 02/23] List SDKs --- TaskEngine.Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/TaskEngine.Dockerfile b/TaskEngine.Dockerfile index 6ff8d946..a99244c9 100644 --- a/TaskEngine.Dockerfile +++ b/TaskEngine.Dockerfile @@ -1,6 +1,8 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim as build # See https://mcr.microsoft.com/en-us/product/dotnet/sdk/tags #See more comments in API.Dockerfile +# RUN ls +RUN dotnet --list-sdks WORKDIR / RUN git clone https://github.com/eficode/wait-for.git @@ -8,6 +10,8 @@ RUN git clone https://github.com/eficode/wait-for.git WORKDIR /src COPY ./ClassTranscribeDatabase/ClassTranscribeDatabase.csproj ./ClassTranscribeDatabase/ClassTranscribeDatabase.csproj # --verbosity normal|diagnostic + + RUN dotnet restore --verbosity diagnostic ./ClassTranscribeDatabase/ClassTranscribeDatabase.csproj COPY ./TaskEngine/TaskEngine.csproj ./TaskEngine/TaskEngine.csproj From a00d7f78abf31b7573569fa06108c98019bb08a8 Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 18 Sep 2024 14:44:06 -0500 Subject: [PATCH 03/23] Build Whisper --- pythonrpcserver.Dockerfile | 89 +++++++++++++++----------------------- 1 file changed, 36 insertions(+), 53 deletions(-) diff --git a/pythonrpcserver.Dockerfile b/pythonrpcserver.Dockerfile index 22cc7069..8824aee8 100644 --- a/pythonrpcserver.Dockerfile +++ b/pythonrpcserver.Dockerfile @@ -1,53 +1,36 @@ - -# Total laptop build 626 seconds -#FROM python:3.7-slim-buster - -#FROM python:3.10.8-slim-buster - Failed to install scipy/numpy -#FROM python:3.9.15-slim-buster - failed to install scipy/numpy -FROM --platform=linux/amd64 python:3.8.15-slim-buster - -RUN apt-get update -RUN apt-get install -y curl gcc g++ make libglib2.0-0 libsm6 libxext6 libxrender-dev ffmpeg - -# Build stuff for tesseract -# Based on https://medium.com/quantrium-tech/installing-tesseract-4-on-ubuntu-18-04-b6fcd0cbd78f -#RUN apt-get install -y automake pkg-config libsdl-pango-dev libicu-dev libcairo2-dev bc libleptonica-dev -#RUN curl -L https://github.com/tesseract-ocr/tesseract/archive/refs/tags/4.1.1.tar.gz | tar xvz - -#WORKDIR /tesseract-4.1.1 -#RUN ./autogen.sh && ./configure && make -j && make install && ldconfig -# Slow! The above line takes 435 seconds on my laptop -#RUN make training && make training-install -# The above line takes 59 seconds on my laptop - -# RUN curl -L -o tessdata/eng.traineddata https://github.com/tesseract-ocr/tessdata/raw/main/eng.traineddata -# RUN curl -L -o tessdata/osd.traineddata https://github.com/tesseract-ocr/tessdata/raw/main/osd.traineddata - -# ENV TESSDATA_PREFIX=/tesseract-4.1.1/tessdata -#Disable multi-threading -ENV OMP_THREAD_LIMIT=1 - -WORKDIR /PythonRpcServer - - -COPY ./PythonRpcServer/requirements.txt requirements.txt -RUN pip install --no-cache-dir --upgrade pip -RUN pip install --no-cache-dir -r requirements.txt - -COPY ct.proto ct.proto -RUN python -m grpc_tools.protoc -I . --python_out=./ --grpc_python_out=./ ct.proto - -COPY ./PythonRpcServer . - -# Old:Downloaded tgz from https://github.com/nficano/pytube and renamed to include version -# New: Grab link directly from https://github.com/pytube/pytube/tags (-L => follow redirect) -# Uncomment to pull pytube tar.gz directly from github, if version unavailable on pypi (remember to comment out in PythonRpcServer/requirements.txt) -ARG PYTUBE_VERSION="" -RUN if [ "${PYTUBE_VERSION}" != "" ]; then curl -L https://github.com/pytube/pytube/archive/refs/tags/v${PYTUBE_VERSION}.tar.gz -o pytube.tar.gz && pip install --no-cache-dir --force-reinstall pytube.tar.gz && rm pytube.tar.gz; fi - -# RUN python -m nltk.downloader stopwords brown - - -# Nice:Very low priority but not lowest priority (18 out of 19) -#ionice: Best effort class but second lowest priory (6 out of 7) -CMD [ "nice","-n","18", "ionice","-c","2","-n","6", "python3", "-u", "/PythonRpcServer/server.py" ] +# # ------------------------------ +# # Stage 1: Build Whisper.cpp +# # ------------------------------ + FROM --platform=linux/amd64 python:3.8.15-slim-buster AS whisperbuild + RUN apt-get update && \ + apt-get install -y curl gcc g++ make libglib2.0-0 libsm6 libxext6 libxrender-dev ffmpeg git + + WORKDIR /whisper.cpp + RUN git clone https://github.com/ggerganov/whisper.cpp . && make + RUN bash ./models/download-ggml-model.sh base.en + +# ------------------------------ +# Stage 2: Setup Python RPC Server +# ------------------------------ + FROM --platform=linux/amd64 python:3.8.15-slim-buster AS rpcserver + RUN apt-get update && \ + apt-get install -y curl gcc g++ make libglib2.0-0 libsm6 libxext6 libxrender-dev ffmpeg + + ENV OMP_THREAD_LIMIT=1 + COPY --from=whisperbuild /whisper.cpp/main /usr/local/bin/whisper + COPY --from=whisperbuild /whisper.cpp/models/ggml-base.en.bin /usr/local/bin/models/ggml-base.en.bin + WORKDIR /PythonRpcServer + + COPY ./PythonRpcServer/requirements.txt requirements.txt + RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + + COPY ct.proto ct.proto + RUN python -m grpc_tools.protoc -I . --python_out=./ --grpc_python_out=./ ct.proto + + COPY ./PythonRpcServer . + + CMD [ "nice", "-n", "18", "ionice", "-c", "2", "-n", "6", "python3", "-u", "/PythonRpcServer/server.py" ] + + + \ No newline at end of file From ad02846fb7e8c07966b2669dab84bc878394467d Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 18 Sep 2024 15:20:10 -0500 Subject: [PATCH 04/23] Update LocalTranscriptionTask.cs --- TaskEngine/Tasks/LocalTranscriptionTask.cs | 96 +++++++++++----------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/TaskEngine/Tasks/LocalTranscriptionTask.cs b/TaskEngine/Tasks/LocalTranscriptionTask.cs index dc8b5629..0352ce3c 100644 --- a/TaskEngine/Tasks/LocalTranscriptionTask.cs +++ b/TaskEngine/Tasks/LocalTranscriptionTask.cs @@ -93,81 +93,83 @@ protected async override Task OnConsume(string videoId, TaskParameters taskParam GetLogger().LogInformation($"{videoId}: Calling RecognitionWithVideoStreamAsync"); - var request = new CTGrpc.CaptionRequest + var request = new CTGrpc.TranscriptionRequest { LogId = videoId, FilePath = video.Video1.VMPath, - PhraseHints = phraseHints, - CourseHints = "", - OutputLanguages = "en" + Model = "en", + Language = "en" + // PhraseHints = phraseHints, + // CourseHints = "", + // OutputLanguages = "en" }; var jsonString = ""; try { - jsonString = (await _rpcClient.PythonServerClient.CaptionRPCAsync(request)).Json; + jsonString = (await _rpcClient.PythonServerClient.TranscribeAudioRPCAsync(request)).Json; } catch (RpcException e) { if (e.Status.StatusCode == StatusCode.InvalidArgument) { - GetLogger().LogError($"CaptionRPC=({videoId}):{e.Message}"); + GetLogger().LogError($"TranscribeAudioRPCAsync=({videoId}):{e.Message}"); } return; } finally { - GetLogger().LogInformation($"{videoId} Caption - rpc complete"); + GetLogger().LogInformation($"{videoId} Transcribe - rpc complete"); TaskEngineGlobals.KeyProvider.ReleaseKey(key, video.Id); } - JArray jArray = JArray.Parse(jsonString); + + JObject jObject = JObject.Parse(jsonString); + // JArray jArray = JArray.Parse(jsonString); + var theLanguage = jObject["result"]["language"].ToString(Newtonsoft.Json.Formatting.None); + var theCaptionsAsJson = jObject["transcription"]; - foreach (var captionsInLanguage in jArray) + var theCaptions = new List(); + int cueCount = 0; + + foreach (var jsonCue in theCaptionsAsJson) { + var caption = new Caption() { + Index = cueCount ++, + Begin = TimeSpan.Parse(jsonCue["timestamps"]["from"].ToString(Newtonsoft.Json.Formatting.None)), + End = TimeSpan.Parse(jsonCue["timestamps"]["to"].ToString(Newtonsoft.Json.Formatting.None)) , + Text = jsonCue["text"] .ToString(Newtonsoft.Json.Formatting.None) + }; + + theCaptions.Add(caption); + } + if (theCaptions.Count > 0) { - var theLanguage = captionsInLanguage["Lang"].ToString(Newtonsoft.Json.Formatting.None); - var theCaptionsAsJson = captionsInLanguage["Captions"]; + GetLogger().LogInformation($"{videoId}: Created {theCaptions.Count} captions objects"); - var theCaptions = new List(); - int cueCount = 0; - // Fix the next line of code - - foreach (var jsonCue in theCaptionsAsJson) { - var caption = new Caption() { - Index = cueCount ++, - Begin = TimeSpan.Parse(jsonCue["start"].ToString(Newtonsoft.Json.Formatting.None)), - End = TimeSpan.Parse(jsonCue["end"].ToString(Newtonsoft.Json.Formatting.None)) , - Text = jsonCue["text"] .ToString(Newtonsoft.Json.Formatting.None) + var t = _context.Transcriptions.SingleOrDefault(t => t.VideoId == video.Id && t.SourceInternalRef == SOURCEINTERNALREF && t.Language == theLanguage && t.TranscriptionType == TranscriptionType.Caption); + GetLogger().LogInformation($"Find Existing Transcriptions null={t == null}"); + // Did we get the default or an existing Transcription entity? + if (t == null) + { + t = new Transcription() + { + TranscriptionType = TranscriptionType.Caption, + Captions = theCaptions, + Language = theLanguage, + VideoId = video.Id, + Label = $"{theLanguage} (ClassTranscribe)", + SourceInternalRef = SOURCEINTERNALREF, // + SourceLabel = "ClassTranscribe (Local" + (phraseHints.Length>0 ?" with phrase hints)" : ")") + // Todo store the entire Whisper result here }; - - theCaptions.Add(caption); + _context.Add(t); } - if (theCaptions.Count > 0) + else { - - var t = _context.Transcriptions.SingleOrDefault(t => t.VideoId == video.Id && t.SourceInternalRef == SOURCEINTERNALREF && t.Language == theLanguage && t.TranscriptionType == TranscriptionType.Caption); - GetLogger().LogInformation($"Find Existing Transcriptions null={t == null}"); - // Did we get the default or an existing Transcription entity? - if (t == null) - { - t = new Transcription() - { - TranscriptionType = TranscriptionType.Caption, - Captions = theCaptions, - Language = theLanguage, - VideoId = video.Id, - Label = $"{theLanguage} (ClassTranscribe)", - SourceInternalRef = SOURCEINTERNALREF, // - SourceLabel = "ClassTranscribe (Local" + (phraseHints.Length>0 ?" with phrase hints)" : ")") - }; - _context.Add(t); - } - else - { - t.Captions.AddRange(theCaptions); - } + t.Captions.AddRange(theCaptions); } } + video.TranscriptionStatus = "NoError"; // video.JsonMetadata["LastSuccessfulTime"] = result.LastSuccessTime.ToString(); - // GetLogger().LogInformation($"{videoId}: Saving captions Code={result.ErrorCode}. LastSuccessTime={result.LastSuccessTime}"); + GetLogger().LogInformation($"{videoId}: Saving captions"); await _context.SaveChangesAsync(); } catch (Exception ex) From 79953d4b3f3b852d7b074a2bc917201d4562cb7d Mon Sep 17 00:00:00 2001 From: SaltyFish0308 <126301143+SaltyFish0308@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:04:26 -0500 Subject: [PATCH 05/23] update ct.proto --- ct.proto | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/ct.proto b/ct.proto index 9ee01680..e16853c6 100644 --- a/ct.proto +++ b/ct.proto @@ -21,17 +21,17 @@ service PythonServer { rpc ComputeFileHash (FileHashRequest) returns (FileHashResponse) {} rpc GetMediaInfoRPC(File) returns (JsonString) {} - rpc CaptionRPC(CaptionRequest) returns (JsonString) {} + rpc TranscribeAudioRPC (TranscriptionRequest) returns (JsonString) {} } -message CaptionRequest { - string logId = 1; - string filePath = 2; - string phraseHints = 3; - string courseHints = 4; - string outputLanguages = 5; +message TranscriptionRequest { + string filePath = 1; // Path to the audio/video file to be transcribed + string model = 2; // Whisper model to use (e.g., 'base-en', 'tiny-en') + string language = 3; // Language in audio. + string logId = 4; } + // The request message containing the user's name. message JsonString { string json = 1; @@ -40,7 +40,6 @@ message JsonString { // The response message containing the greetings. message PlaylistRequest { string Url = 1; - int32 stream = 2; JsonString metadata = 3; } From d545d52a27d59202e116840fb62a851de716efed Mon Sep 17 00:00:00 2001 From: Tyler Liu Date: Wed, 25 Sep 2024 14:48:21 -0500 Subject: [PATCH 06/23] whisper transcribing function added --- PythonRpcServer/randomvoice_16kHz.json | 1 + PythonRpcServer/randomvoice_16kHz.wav | Bin 0 -> 115406 bytes PythonRpcServer/server.py | 3 ++ PythonRpcServer/transcribe.py | 57 +++++++++++++++++++++++++ randomvoice_16kHz.json | 1 + whisper.cpp | 1 + 6 files changed, 63 insertions(+) create mode 100644 PythonRpcServer/randomvoice_16kHz.json create mode 100644 PythonRpcServer/randomvoice_16kHz.wav create mode 100644 PythonRpcServer/transcribe.py create mode 100644 randomvoice_16kHz.json create mode 160000 whisper.cpp diff --git a/PythonRpcServer/randomvoice_16kHz.json b/PythonRpcServer/randomvoice_16kHz.json new file mode 100644 index 00000000..c3053a9b --- /dev/null +++ b/PythonRpcServer/randomvoice_16kHz.json @@ -0,0 +1 @@ +{"text": " Hello? Hello? Hello?", "segments": [{"id": 0, "seek": 0, "start": 0.0, "end": 3.0, "text": " Hello? Hello? Hello?", "tokens": [50363, 18435, 30, 18435, 30, 18435, 30, 50513], "temperature": 0.0, "avg_logprob": -0.636968559688992, "compression_ratio": 1.1764705882352942, "no_speech_prob": 0.22877301275730133}], "language": "en"} \ No newline at end of file diff --git a/PythonRpcServer/randomvoice_16kHz.wav b/PythonRpcServer/randomvoice_16kHz.wav new file mode 100644 index 0000000000000000000000000000000000000000..fd2a335fa5c2b51af12305a5f51ef972327b5542 GIT binary patch literal 115406 zcmW)o1DG927lyl9cM#jQZQI@$8{4*R+qP}n+Sqn7bJ42*&A<1V>?E1Y-0rSA=hLZr zRV!7xa7HMN$~LUnp?kkHaTG-nyhfGcli`Y@Dshw=RqHoM#&4_Es#Lpb{YnjD@_CKc z13DDSn~PVTJc`n;b-&i5_%5ZCaY_fJh7wPiEicPV$|&Wcl1WS#U&Si*teQ%Dtqsv% z=|_wW<|5yzuqoj=A|fKrh0E|8;giFwg;x(N?OSgCWBxSKnDNb!QOkU0cJbN1?P1~J z&%&05%?ZmC_QcoBx6X`Xo;4a8RrJx?C$*EhQM48LMSanOU!U^x<0$#0Drb1p-8)W6 z=Z+n1&9UNIxvimAHmkg~I+QcCA=o`QFxVjI1&#%B1a}611Z#%uP+#l4b<|#LH?f!6 zr|l>9Lwm3N$-Zmfv2WW4?U>F8r>MKcec<--Oxa#ekW*xoY^bzWaw|o+BdL{Jil@{Q z)kK*1t_)N1D-p`SN9cPjAuq(bH2IK^vqXgZ}X3l#7twZH1Zfr z^b-10ZM0TQ>#JSU>T31XS7NW&E;@=`N;M^|l2rL1Z^_$ouN);)$)BF(E%R!4dA#rL z8`pKydF8xx-cGl*+uN<;)^g{$i`|;;2S;-w+=lKtcRw%PtKyCEBIQ~cLs_60B9$m5 z9w?oaAM&U?DYMH5o=;Ae@s$)xE9C^&GLD}L@e<|$sjU`@}iyy7oU`5Vz9U^ zZi$lWOSQ0;N2{To)qZN9v|d`2x>+r+_E0ydOVy+5ziMeUmHI~P6lH~9d8o`&3UjCS z$)>V`Tq5tucTy;OWJCGKo8YzavU|_mg>GT@yJI_^^Pi)-$=sXHKIekd)QNNs*|F`n zRzka;UDmE*zp;-w6Wm7LbFZe1r8H9(DmRs#%4fwCCtHzQUlqAX**-!etz1+?F?pU{{+su9C z+;KKJ6`V{?ap#wPiPu%ToqfofZ!NR-S!1l()+KAN)zF$BDjxa}>=~>PY#7`hPy_4z z?fo9@4V5O>r{7YJExqUuI*m+w#ed2 zQgJ}^P*ZCiwbj}SEt7s&&t(iTju~alU*;5FoUr9#<-?=GpGTC7%n{inazNy~$R&|$ zBfmyoiOd;!IpSwT<;b#;sUlBDJczLQ*c@>>!itC&*)=kD_K)qXSq|;-Q@1& zDpPumyzbt7?~PYku4g6ekbap?$*AO1iYn!m_R49V$r)vUNUwHhEv-@?tI_I5b(fk= zO`!gx7F7*3x0;_Ob0L?H_%CenMA_FM3G7t?$#X=@a!H`hC5M@!T-Y+r~L# zw9(y|XcRUSW3C>dzo2S<*A8m!w9o1YwYEA$eW8xj?rSmhYI;UJTwkfh(<-Tj#4IHw zf5^1TZ~05+RE8*LmAlGocEkzR{$kekeYsgSm!GKt*WC5)Ot-C@-A&JXp6w>}=6FeE zgf!Tf%h)gDWLMcmcHqe@l-uP~`Ja3#Z^-X5hti8(v`(~AEp@QAUpuK~*E{KsUf!r- zbTRT9cl8?jcCEG+TT`^J>KFB`YHB65Q(8Pdp&rzpYE|{gdUk!amQ&lU7E?QjfU-?7 zm5wr{Oe$Y`L2sjy0<6Jq$|R+U5|qi5-pVE=N+~Nmu~yBhRn=Pa zEbeQwxW-v(JhhzItL#t)@oyb@iOSJWPL*}!L|K8m{#Yr%%B)9+s-)&%Kh>i4Y*d%9 zrZ@2nGiwX9FWOaYgjQL*ueMgxs#(>~Vv!g_H*=Nwe8+T3aU~Jox0te2nNC%krOZ~Q z@%cREfYMWmRDQ|_RJlg7undzEytrOLZ;H3Vo9dP1YJa(rUJ@_AH^@_DExA+v=1QMS zl{z7WaO;-`r%L=LXz4Ze!P? znmbNxcaWQ(_julW;iZ+w=rp61YsxX@j}lL;7BSWLB8ysAZJ_Q`zp+N>hkF13(4R4t>P6XQe) z@k=?VbfOctl`E-RRlFPYl(|lScI`xGtkc-}Ve5|O2q(8Q%*oCg$?3iJ;>ldHyv#4l z$rN&n7tia@PO0LSbWQgWm3yNz#_8j9bDDq${x~h!Ki|2^gk1XvZ?){EWEJ;CO?8cW zNu9-$Z_BeDrrxEC*B70|VsTLKt{CdPhy5HPOmwrL`Sel=ej1 ztyx-q{g|HHsAs%1N}DB2zcJECW{lDoYERYX>U}Xuv=`+>GtkB(Wu`KP+Bs3`@}u|2 zyF_KZ>b7#iOPFAP9)7u%~RO70@+xP65c6D1?mKA0BtqS&f z`>Vau9%&D=huCB6ZurpGMCL*>-B2tUFVxQP17K%Y) z9iKN5MZsb9#XJ!u+Np^^Go3X}|5u-*-_y_QYxN`g2tAX2S?jN5(o$$Dd#EE7y{y`Z z=Q&E%)YD?P_*e844Mjc?Ne!5#j8JkYujGC?Ru%-IT=mX)^Vqx9yrVp=)%2kfV1(*C z$Jd~=GxiSqfE~yATbV5 z*nYw5slAOI#w{tmB&O)~d zd;PUnLFSZgrv3=zXbW6_6wxl9Zcm4zwRQx#81>GcB{9dLVb zPscgsoS05O-qm5NjOB*HtZ*y8HPu>aWwm~U4C{Bu4K=pTTc50;mDk>EM>wsV#Z>6B z?khLDH^f^He*FY`-S3`scY#~4xUsyi?r%4NR~aOFL-qvG?Nd4kgB^VW4Dd`%#~se0 zWzgc&W3Fh4^!j>ly{rCQZ)5BRRo60GoBx_U&CKRWc1v!ftzKB4t|!-v=$ZA_d}SSd zDAybllzoeC8bhlAf-u#TswMuRQfOkna!G#ky73-jP!Z43zl+O6@+&JQj%*+!MCoLIdsc|$|@y?7%Do5`TxI< z6S$+zL`m^d8LBi=1}ifZKfPrVo%%Z{VS?zwUB4*4i_%nxnCf1&0KH+5wiiB-M$e$f zKNWN&QuHiM)Kd1=9}G2H3SVtbBtJ`@pZ75o@T z9O&U6;qT+m=+Egt5Pdy*TXg^EsnME0r9X>5kw1mMlE0S!bM*V@5`NRaB)VYq@u=BR zZ=;e%uZwQ)|Kwj8SRdRJDg&AeSZ}O{Ryw=4y~`nH1c=5eb-W#`rTY&p<*Qx9bw0-tI>xh+xI#JWP>?8x- zH*?Rr9#yWWH^MvMh0AX86gVmaEaD}3k7M&?Z8o_-UY zv{!4TeW!!9QICi}ppPZ2v>o0bH;3EDnP^Y79)=c#YK5|drUz37CkM|5dxUaY)va;X zVD?r@D-k>GM<_?=WN=gPTJUwy3Z@F_q57dOp*dDzyC0RXtMi4*--rFFIrW_?&I_JX zdwaay*Pd*<_E_rURZvn>_lz6i)u(4czo+d-VhQc=ng3p92*&0mrORTd7aEV--aTu~S*199Ft2S(Fmg z@M_9!Ia1HdZ>y+u^U4pfp`?rrz9JJqf0-f*TnC!JT|=<=+idsK*X-oK#FV#-*! z=ys|=Yjv^uQ*Ej>)BdFrx8kQ?)IB|s@q+c$PH&-``U-7=HcdOud%2^np_|6keycxJ zSIw!lg`2j~W@wYN%~~wIo?co{q(9TTQBST=1-4KDnuxk0mqTx-X6QlTp3cVas`?Ff67Rn6*S6|*+*G`&cPkQ*r8#er=cv?0Bak2sTj}ej$NE5l9uaUN;OOZ zy6wZWm`YvfC{BtndgBiDx0+Bps^!!->fgEMeui##GdG(T&28o|bA-9w+)3R#26H&C z5767_|L7z2A=JMrthF?He7%{zLk}~4=o56Qebzo`jkHT}iF9g|(9|hnzmi^=D$~f; zUJ7rcI|hDG7j6*GE#w;Ptn*G>_g~Poa9@CUE;#eJ6Wi%q1)UX6Hun^m?!MO!mVFVN z`AHs>t>s91(s?R;LH6cIo@W`izPrIK;??rTdI?ztyW}gG2P|40on){wm38u6$qRDm zEIx{Vc*@V!>{<(Lu-08$tm*t0TmPiRhC{pq9~IRegPxM9D@9k7m00Y{Bw+H>n$VL` z<&Wt5S(g>{XsxW)UyY=%1wqmu+0ixRc96qAUMBAh^`M-a*uCnMa-P#+Kl55`PqAy- ziv6GU-O6aEvkTgp`SpUe-Wp}Ku}Y&IZMPO$yQ~Z_%Ie^Y79c^#sq1#;)zls6-h@?r za3!eslbg*`slhX-CBM9ivZXvCHF!=pWjp=ghw@h0tUOUlbF~kZ8!)sNXciyn0wq8_ zelY`HutQ6&AJ;1wn~k_AT?(AAgYkhCyHBg8-Bc%mn2M?4s=__Utxi*qgM!m&ZGlln`|&)pl#PK~FH2UT~4d3;mXf{{zA8Tegm^_SQx&Wp?9sj$Rz@lK>s z^^e2y>JU~jj?1V*%-9^ z%X{d}MTI)#RzufVPc428Q;4+vRwC}>ckWy!J3n_d$~t4cw3dT(Dp)TMokD9v39S8Adi0%Bc6%o#+^&mv46KyND*)oF1)quV()0T*UQ7^ab47@&qQ6?? z|5%{3o?id0-P6u!*|kmR0%hS2_r)m@8%_|V&ea;|#f+UsN>hNP3!6#IMCJ$Msd2}s zW<1u%fC)-#3)M|1V*e>~saeH&VoSOHVrW6fy>VX1J?BoQON^w?2E9ApHmcKgZvb4U zvo{^>O1e!z+;g3-aEc17kAg6Y`rLyN&LQWX6Q8>khy75OJ@OdU@Rv*sC;vuw`63qb zr20_Ly6M|=4UMOTx!#OIs7F$>ErNy;jO}x@p>J$H|$_o zD>NL-x6xP1SHoxe)|ds%RYq0A(+}#c^q(-xg4Dlttdg$cjxrFP?*u&1gZ1r03ur2v z!1<4Q<-H&7a(2Uf=LC#y9kpy)=yI@KFlq39phVz|zp#I6bgAe((G8=kfC{Xrmr-w` zl<0WTO`?lMUya%j^&#qO)b}VEl`Oh=bl2#|(Qb5ce>eYVe{KI%|5tzEz?{J0K<8l2 zP&_M(eZ>B5uWL+adKbD~+y3>cS;vW98wr>1pf=Xce!z$>O{xwY#ss+pjqx9p{KS}v_P zcVZcO#aaik~LTGc(MN&0|T0yO+R-`tMN9jDNuI5f(P)D*e?~CQ=qa$H5wM889n(8(e zhPECK|3Y!uSpl@yWI~9nC?GpQiv1Nsxxn*CBYhytCvdn3>gQ)x{dGEHLsaZG=vn>k zY`k*9yNlad?U;5MepkotN##0aKd@^#Wza~UqFQ&cUs_{PCS%d-ufZN_u>u!6x1Aa= zpex{_j9v~ep%=%Ca-XAxXjJ?L-YPGPoFmhqgEm9YjxEN5SMP`bR~et~xB$&!BKPy6 zI*+Qem+LBb z6~1Y{{l0C!$G#)J8NU9$=Du%cc|3+c)Y#od9o&St#zZ|#Ujx&Np>0s7aIJIT=RzF= zDxWD@iML99Wu6RrDZPAd8)u~**Ir>Iu?~mEhh~Phg?5Dwhi->rTY0UR)`QTA&_jBB znvfT~9BdgZO6UI+NDyohydLZniVjV)K3Xa5ilB+(c9fl(N|Dbgu7bk=(-0{cnX5Jsl!34@`Spt2o8g9orIN4t6?tNKSX{cO;?UaQXJ)`O`LG3K=MY67@ct1gHP3RhL zP}YW`?rf#sokh2L029g}ilDl;6IEFGFWF0HQGi-2wLo}Bl?Gz+|DB@B>I1bjl`9sA zZ4dhQU~LNhJ%)Byy+Q~3qviwuUDERCrSuj0X1%7KQXi~;)gz1pV4l?a7j&XZ+7EW+ zXW^h9Z=t%ZLorLhPaL3pLoG`zFL|@PT;2fpHwgGU%5X1~kg;_CmN-Af>>~I@zpcvr z$#Xjuy8L$MHH_k(v%uNI*ET@iF79M=)=<-1xfj7SO}vX<3p|Le${XdQauRjzKD&G? zsKb@vbkTE4v{D=nJySdod{Ljt5QaG^tMnBTJurF{o$;F>+GTDbf@1qb_!=O3j0}S8ShSa5;?Yg z7Ja7_s(b)UUBeCY2IBs#hOMrawrtJX*@pUdS=kE$Yt0j%1U_+;C8CsiQ*8o1yr(Tf zq5iEu)iWBO^(lH%J(oUTUxt2D9bCCa&tw!ZY8&Z{5E$Gr(i)%iO?n>~>JzQL7R`FP zEJjiN#w+pB8Oy^^OL^biOkRI)E#Ar+bjs%50~EEMj%&BEuUc8GQla6&+`%J(WPua@ zySOHmV5;f-9sQ5|B?H3)ivoiK>jM?oPf0`V*;|`KYeLsTX{@5wV#~CPQtJ-mtK4!- z^u4%lU-vzVTrPgbH?IsUcRcEmD{Cmr6|7()orNQrL^8Ki+Y-iHaQJ%q^#u?k+q+rZ*eI!MsJOAHZXdjKvO4}$X72h#?tekjJ9+gQ8YQ@;S`+0rRW5PK{qH^RikC^w(jpiy|zs+X8-M;U*IQxC;eW!fWeR+L8 z-%GQpdDS>?bTh7k{RiPJn8sEVxJ3F{EvsGzHc*7;msNkIDg4<6x}yu?K1aWst;AH; z;PNc+KCoV@x#Qe^sKH;IpH3{4^=YiMne2yzPDRk&V5btbyfN$S7RpXD`+}X;iNGD3 zZs)QK*c0hVyX?OmE6VcO8SSR_R8)fcw#U=jVEy8C0xmn>dSX4YR$KGX6Pn_=9>=d~ z4<|S2997WneO^vFNmj2q6=ohzXL?rfQE!*G&N~H5Zvy80&->wJl9lCml(1vo36R=u zaQ8Ma+DEStI^SWOrS&`!n_84uxdoclWkxwz&Z3UBgW)Apjwp3;XUeP9P-ccF$zR1JH7|^^mY&l1ZgexhnB{!7xy&qXrZfAS!_8FYK6HtZ#t~za(SsiH z1fI1B-6NpQ)_Q1hwH|m?EyM@B;n%1Mndqt|SyyeT0k6eNp@0^o@Tresd7fy9Vmt-E zbRDiiFZC5`udBF*g0vnz$$_0`0U;>zzSqmE>m~9wyJg)kj_Wjbn^O&5qG>;|JKO2) z*WA&PR&r~amBemtm*dYKqbCOJ98OJV6gu#EeC^uKY-g8q%?Y8)Yi=gIypHg@X6|Ts z-&@?*o^B_)%@}u&tFmuvdCS4NU%WQ*ro1E1%j0r4b@T(z!c_!KJ{TX{U6-{ZR4yI>OiciXGyO_)On;FRI}b=1~uc zsbYkf1Cr~8+P+B~5ed|M>Hu{+9BGyIUi<5`oz-{Yz-{L##nNlRmCNXL^=`OukMwu+ z$<83d_*y^S_cfI4siKyUROKvSqGc#^Db+q`k)~#9Yt$}$MLls)iLD%v8DwE^xSP+- z;3mNlE#a1O2fK^FGhf{qu0x$p;&l8!V$iLdp>?6Ap?^YMK@N9AidDs`$A@c8v6p}d zn%Xt&YIX*m+Yof&2&W#k`?XsIow1B8ic>D=F4cHXqtUK~SObGRMt@z#{mVw~V!HBB zNrQ^97G@WTroL45NFH1R%Z%dPc4yUn7rjLR{(UlP^KDS~R=nnEq9`84Y&^Hh%6IoxjK@GToE*b~5Q=zjx#^qUxGcZw0r)M+X8@tURzOtxhSHixA9S&;{ zHrw~s9BM8%4pXT{!FQ+A0|%(R)OYG=tq_V&RpYbK$ee-F+srIyy2d0UxzS8tg(~$> zU9RR-Pk~Kyh*k9846K*&bh*v=W^GZSY&4Z+vOPPfkhg~}`prq{EVHwNVxYI1cNu2flNx=btc$`w?Ly>N>GN)nu%ez--aaLE2+h3;VG#TQqo z!Ao%?AK*0)1vhSy%c*tas575r1c{ugt0Fer?Qp z<|Z?lF9YoVk}qFa;joxt*}^{i?)x73l7?js`{TRgJLhVZs0_C-4>oj(&behodfzC0`Q!e?8M zHJHUSi30hz@-ljN;FiPiiLZjX{^0Ipq&LLJDG7N0$m#UkIC7TP8s(@b2=p4>PIsAr zKCw?sQ#<`1m`68;{G8v1z zJ`^V1+RNvKdp6$oa_=4$=muP^yZhKV0tQ$B8y8Mm^aTwM^$0BJlXcq?WMm4UHT1M| zqy9d2E`j~p^VN={yQRtSoFu_>+{;W#WE;7VcJ!YMu)Dpas~)S}s7-ZnNxqX=ienTf z&k|;KN2ku?(|otlZmO8ic%CbHekZk|5@P{bqyols+=pn` zY8#a5a^TY%;yP+^8gh31NEseh%H!QSB>$z`MZiVDr$s&4&)%yQVd1XUS-+*H03kj%&Kf<9u0|QWtRv)pO#QUB4LyFcUegE}3BZ+a&F*0N zkG|Qy(!S)r{^l7Yz0r-e-c0MjwLZq59ZdIcK`t+bn66ZzLdF15Md361yt8gmcQ#nV zZOWv6iz=QFtdJvH(Hf7`|xPhzhn zy3#C=c?Xg(7iCdcW{UskB2SVcTmW8+MURN5WO2KprZH0-nnTqYkX?nKSdrgEP~ z(^;;;YopOb&XbyaOMamu=y4Fb?o;@{S~Tk*-p&eVfgQA-f|;A6^B1N{r?Dnk>#R6b z{mjlR=QIho`tbbJUInz?h4hW)-eTNV+iMT18KB%IKNwQ7<3=VI7jZOpp{NE)8m%HJ z+8Lf+kIc(?H!j_#lc!P(ZBntDlux7;^C%Ojk2AoweZ1lD;q@?6nZ&aQx8%Mr|S10n;llrXU9)p`bCZVX< zZ*bo8*~@H$r&$8+LgPvndiilUm+>8@%8vZY|GbV~YWn0yaOP{b1XaEl?&n*juaKfE z%Fsb{x06(v)+iD8L2sA9#L2{C-sv!uzo{gArIJgO78~%Xx`K`~;7v~lVa^pH)a{O< z0T?K8R0&@(rMd^iKSGp2q57#L6wOgh`tsc~i$dUv!Qv=cj)Sm*Ox!WQVuEioDm%GP z)k&MyBhxko)q4_bF{g|xm*bPq1Ob%yVuB!IdspB-LqY7vQAmHg4ZTq~Rj<8fAntNF z|FK9(ZNX2Tjq)GxN}+(Mpv#P~vbg9nF_mZX87peOjHx82Giu5?p7b=3N_WyjZ{2Nl zgswPg^GM$GLdiSoC?LaY_-51jepm4AZ{sFR<7aljTUqKRa-TUH_}V6cZ((86;9#Rz>xOcd{yd)jSCG^~P0-IV zcQ-jDiL1JU_y0tWSH9prtoVOQ=P>!IC^fNm8a4Yp%6D9`k$hN3T#(x!_P@T=5){SR z;)HlB;(}9Sp#k?~U1o%5-4}7tv{tA=@M>)M?tftQ_dJs?tZ=DhMorAZ=W~?Wq({ck z^RuGwwgTL_2HC8wN3NvIr@Az6vs+GbGe zM)G(uamyRa_uc|n(Hk#0_v4(*$lt9Ki|~34D#S*Tbum#0`{H}PBt!8~?Fqt}ic+&2 zcJy1x!@8`(>Kfmk7_C}L9Di4dThb9pw6OtH7iwn7i z{P1799RYDVO3h%Ftdv%SD`ckW_HvxC&qvj0(eNRLM zZ)#LGN*Pl~icX-fR5WZbct!G;ZRwXO)RU;z&B#x+W1SwBLY5%~vBYgbo-Zc4=SSy> za~ucru+xb?pg9M?wkho+)*7p~Rntmf&7yC{3?&WC3PptqSd*>kV1jj4MN$W2LhV9t zgX4n3f&+rngL8v_f-OVKL!r<}be@HFdiq2RuwWwm!qwhQ@2i(smg3cg{8m2{ms@yW z=~47&D_?PXUV(CZu}(URw5nfisl}m3PBi?6&&*(|<~`$$kDafhu!` z6}tdlzYZP62Ty&=n(5Atzlj4;1_rTO_LBdQF*u3mdWSvQU3sq*rXD;X!@N(4k1ml3 zr+)xi+fQ`Xchrb7tnU*1O5$4ihje;RlE8zk$Y7Bg$8ziEt zus>8~B(QMX@<%CPJM)U>;> z@e!=a1nPBM*luDE6BE~QfLyS~68QlYWebEkw8Q98rNwd450rRN9SBYgQjaT$ z=Vbri(?1qa12>>i7I(9`lUUQ0K|5c_tc}3eOz53(r@>;gpb~8-_h34CK@ESCBOlz+ z-X}PIUs(uM?HKN+>WzXq{2`&g$4X(%2^9$CN9R}&%8JsL48*X|DMw28t2^A=MD^{D zvgDE)ZRB2a@;Dv};znB|SNX)5DBN$}7}QxTWoJ?k(d9@&P#Rx0G;H*2?&e9Z-v=sWsrt z} z@oh#)4^(~+hMNRd)tM^a79BPZuFF*FL=uoo3uPTyv67?)O0!-+k&get8XE@keJS!$ zNmHo_smpihhu`VUe=&kbRUFN(t4Oj)pd8jSx?_SrCdTS%FU{~FEfLh$MW== zlIpj}wwxtB^sk*1{?y+}j6?q>bR%>GWPCofA@n1}lmmIb8l ztO()nUM5ZJvJ?L!NjC>fJrzx1E?JdLaP&{~m}kmw*3xKp;yR{#K9i`|mrztc_8lEh<|o zzGovCQf=Pv7W#Qfx?2t0zGmd>TPw|#zNo-C_&fnMstxPCA-Ya9IJgF`Z#28$Hnk@o zDqVNJXA2aeYjO-p$I~(^^==cYxuDj4QNy%kT5|2BTA#Xn75q6)>}KCD62Hi<}*Z5LP^@sXSblz~CTpMU|6k2E{_(y6KF%RB49&}w6ZMq}L-GAf(udA2W)9@ae zuxF>B*QJ&pNu$;T?|rAH9^<{uMAbXXY7VgnZdoI&f9adOtsYi0CW4Z%E1sj?+I9^m z9Tn~?xN?)5j|9yzCKWb-Gzanj^sK~-RN0r*+WK-OyY&e(bDOx@8T`5og!2I`UX5JU zd?l?|BjQm3C!(nT&6OumM~OE|8|5-jViZ-V7GJxcr;`hnyac;DA&j9g2xl!A{5AJv zJ2@Y?DmlhG;t_eDCOAI7$dd_Js)rhzf$4(}+=1CBPi4s>tmUWP$J2CVVc7N-p5$0G zg%kKxRan!9QSd&9I;hIi`8YwvJjs4-z&#y7cbg8fi=!^V{VPbF+Ci7CNRRt0iogz* zqf}f*lYU4-|2OOVG}>NrnbsSDUV0G5It;C;KkibrGsQjRK0{@wNIu~RU85U0``xI{ zTi};P(c)g%3U1S5)V2%!8jA_BTOhPIs4yS6=k47tbf}K*KTI*i1(CJkcX@bX3HUy@ z-6iGNEB^LfyU(XS>X7#Q|kE8ISnv^=jlOa*@z#0>?iL&utI4xl1~G zo_d%k`HnsBF)7dqe8!}vRj&2;hg_*GKG z!0Bj4CtJu&+a7e)6(HHK?B46({?nkKa6FchOvkKY(&w)aaT6D%GaS+Ls=_43vFn;r zbAQMzuox*9%Z(s~a&(;h+?l6v(tp9OcgS{i1GSYQm!i=}wz9fb!J>ljptW?2u1xId zsILR5F=^o%=fQU;nd%r!Wo$~l8i!Y7D%(Lcx4?gy-=boA^<#V~{;u=k9Q;!@TRjhW4Ohv?kp$o7~H9>y~wvEZ#M8^4-xPF7oW# zfzG?Q?{JqZ|6c{p!PRZ%wOW3WE$DckNbuwm4{@YksO?Ebj0P>lg>n9?k3x4WMMiRn zJ{66jGm3UqQbbwxhuV0Q#YpWKe(zBnlulYtrU3TwagYq=VSXR3JyE}_XHebNvX`2% ze|Euusx$3Sn_m-YAE`0_QN7#1@{=&#q{AV9l8i~f9ji>Qk111=kX%lEDZ(sVZ0{!5 z*}&OA`XH(O+`5cQ8n8Mt<+$A$>IyH6{*|9D-Huwno_#Qf`VsARgYV5EVOWgGMvWZ7 zDW>FZf{#{fo5(qhAvbZ3PI#6?+kjyAL)@SF zyw|GUR-D)?UI8W@B1mVyg9Y3t)3gkHIEG1@WVp(A+#0;|?_OtVp?=4~&C5m_Dx09N zixW&Xt>*iFqEBanLl=bWPNpWVgbfyf4d$Q&mZt*`hS@v=Nez-!Wh<(78t&&H`tNo) z$2d@FZ=9ZdGCs&^B?;$$!Ll3anA^}l!&u|6S*T-MK}^3$@nj?EXFDz2DJWmVNw-ZP z=a-6nYhyZHh;G`8YEzP4x&~h?4E_7I^r78#q*nYwM_dMmUo19Kucxq=yOEG-&PQ52 z=n7&L)q5;CgRDH2gm8e5prmR{G(EwGJ-{B`NN@bRLt}FndUE9_!HiYdwUfERzdUjb zT1k0yq#L-G@zpTa$#hoi1S;`2vfLS%o{FuVVNl$zsT#^#MNrvVH zAr<1$eCB%;fjO-u*K?4G+ks?9Vv0IUj+CLMH();NidF#@dQLye9-6N0VKt=(D=)y= z%>}MV%+vdu^sCC`+GCWMb6#>@&rvP}>pL4cwBqcA&D?1}Yoj|g;v=soFqnJjHwEZ& zRe7%YsB8^+QV!F6>-qTRUT4-#QsEwvB>za>;wl-kis0=6yejepqhK{fVf01OhMzc} zaOk?gdOz7drwISw;Cyo`;S1ezQBYxY1(<`Z3@ged2hv>+;#;mkFBr>H*bc_HB%Yu< z&xA$RKqooDl_gegqV{YjS*bInSQQ5OFR14=$UZA9Z8yF2HrH?htTh4$e<9U!I(wqI zd@m(aui;8|)24hKCm zCwMy+vjLmAQzuxv6aKIKyeCUO7QIBW|B90R$WOP);N^xRm4|otB&|4}r}K)br{KL>FIyjAEL}-Xo}zOc#e@42T7i4_U$9j05N^;9 z|62bxf3!bdU_>B$@MW+;Xlv+aD8g!KUAH`vdQ(WI?6l|DJ@EfB+mEaZ)+sB(-e6}y zoh#yghOIvJI@0}KlQM~=JeChpqpGt<&Y`fKCNXeIoq%^+olf(H-Z6^1F&~X8r}h&( zw3NM-h#GCm$2Rqx9V56Q(h9{}|ur zH0h+dOqJGWSJ&cMP3Bom=2@jsXM(OuiOtNg1en>&FTZ%X>BAdY&o&%j6*~NKdPaQ^ zV18kigS=>LZ_ndTqokAb6KAecNfm=s0@ z816``JIK8~Q~XP>_nO@O7Cc_hM)F;kKQ%58XM5AE*gi(s0NK>=6G|Uxx_p~M)kNkgwI;= zWQ*d9RbhVUfjP$X;I-q7jK(vjx^L3CH#4J_PFqB+?o85bE_JpS2yhm8{nR8{3>3aP z;xbjZ97)XbRD?rRmU6I`5EJ%!Q6xv>;Tq0C9PHF^`KQ($t0->GamU9b>l)PO8?FK( z+~B?N9@DQn!#EsH9!NufEeR?Q2gAl<4V@4{A>aowRh6IEjIOjuOk_GUv6zZyA5TfC z+=E@dXJ)n&{iiod-&*)#Tkvmv9IC^xzyu(`QT%xhJgU^xsTO2HnzL(5qRhmD0dHj; z)&T*3r2G7Yfu;e&W{`Egj_xLMu+PXj&%`~qLwlGHC=tpWN*js_eh9t_#t+TKhgGZ` z)@W-HJ2eT{nF7ahCHlozlFy6LBc9Fzy>IszX zVt8rW(C(j7f###GcLRUqroM!Of)u#=by15oybHWOS=&ONyQ8(#gL-{3b`5#jsmv2* z7GDQ^+Wk0pkA028&V)S)OA%f@yjZv%{v>Qm*e+(mN`{^Dwec18Oonb;WOx11=5l_9sn(@y#sSS=;~C84E~kQ(Zsh&MqNBt{txN8Grswr?FO#SK z>)PEwcO4FIS_68H2i~_~ob$oZ!$^g;B;E2eBtlt&djk=HYyL(43jY57J^uXciqC~ z`r`(~M|~YaJsJfAO@gbi41ReGG}M?5wicuppV_3>q&dgJ%kr_d^SV!*M@}TuNHgJm zX`J15H#;BB^)~dw%uZtb-)GJ|H#bR;_9zC|&|aH@sroWk-vwu)C!FjEsHT=`tJkzG z`ZGNbN%CjL4I|3X%yUL+v$UDfOi8w=f|<`uZ9XB}XB)}Q^yYV?li@Rdf^V8q^~y3h zF8}W{R6to+hQjcMDaYUX5B&wzEnHuZHt_|fml8I7jcPiBt^c1l8bqD%@Poq-aSZX?h?}AYS01|E#}A zpkJU%;7Xug@FLG}aY%*=TgP#)PutPp(G=)x)$ovFIsrT3|2g$KW*yh!Oy&eRv(18#X((M8U4X@{O#9N*#vsp)>;&`a>|d-(igQ4Q{MKZnD+PH62=cFGzDjP&Lt z^O{-Gx5QV?H=RV-NTz^48dpflwxnAgW$wHYy(_=oo$T@t(gl9whFQp$+V{>pY|dnE zF0C2IF!Z6?3r;&ZKpv?eIJ*v5ycsLJ1~Y-DScLR6J**?u_%p8SQSR_l2WC9Kxk_-Kr>00^a=tHub?90ETR)(9zj-%|YusUW1+~6V_;b7$}iK|;ERzLA6uanVO#GN0?Ybu#p>1i;k06RMs&sxFzjf*=t z5&l-qD-5%Fj>fVc{r@S->>Rp;i%;|kY&@OWr>@>-)F_p;5doezLBOfdK>C2_R)g#o z!iam5P&2`WnaGJRB1_rVs7}wDX8vcE_U&U1qna6P?3n!||`J`%1A@P~dela`7^!++WjCKfNlyc2TbNiHy1R)Mc4cY{^Ky)LNYVdzp- zNQQ^V*ldB7{PldZlCn5tcd)bA=gE$(B#ls-G;a-S37^kou4fTz>!lS*I&Fr11?9ah zUtOGkU74imTIS3yxp{fsDe!IPq6Wu-{m$XsixaRPkKUgGl~|&eWF?b$l2w?DRrdGn z6$M>07hHds$c*2VkAy`H5*De+vrHz(av#N@qxucCYdyZ!Fy@tdfY#EYXe=R(+ymXV z3o1?*PS|+J2_hSDJ!;@3Y{C`2%n1^KfUMDUZ`3sIJpU>@Gu>If~!>Q;mb;ybsT-3K+T~ znd+{*%RBt3L83i~YIA{9cX?jbLB&erM=^ayCG3p$;(xIdjF ztfNf4^W5%#Otb6Ga8Ab9V9nx0gSKRJP0lkoPp>>0x`!(o3_a)999Bop7W@0L)*52f zwX%~T*>CLzi!7uYMWFyEb!wpz#37-bko3rU?$T!NbtyE5Q7GE0P<$V-voBG9kAc7H zqLt?)8Tp3P@-8ZEDlG}Px()x&L{A+Eci5!;0zIFhYh}duxM^fEYqHB$nx$e zmI9AyyPI8MyA$z|LNLRd7!$gAf0K{au3f z`;>RgeeXtHQkg2fhYtDGOE1%r>D&S1y^V@EhBGzt;Yw7)Lrme#K~)McJKmcav*xz- zH;D@>yvjtDjbC-u>S_PSyNF_Pw+fY3@U;P_0J+bZaDInW?i}9FQF4uoN#l-3iCM=A zJ%FRI7<5t(6}Jm1hi!0|_c#g<^vuS5y2yQUZ99zZ#!o|%S4(8vC(*Z@EYc!)L4Goo zgW=R^nT8nx9)2h$W6^4R>(}=#zDd#62Fq{d! z-Kc3@KssZ|8+1d*$ifwW@*a>u>B9_c0N^R9({ zx{cl4+^a+v9)T8}kDpK;w3Y{@xh|)&6;Mm@Ssajk5<2-d)yBSl zOl)BfBkd;~r}>sO7B{#vPAGHvSDc<-eyL ze6l~Hl@)X*Q_;Jld$wTmCmp!*INj%f{fL><irbVIc;tj*~DvFx7RQ{B0&~o3v8#6Q6QXKtnEf}!7_ms)+z5I)m?lMw} z3EYCDPMhKa{XLyvJ^7MeI6LXwMWhWE;S9`%)kZmWIX&TqdmnuJH`kpPZz3~jvp4!# zWt@$bOd!PK$#vw;Oof%MAb*k@R&a&{{3ewniclurX&FG3CRv)>+~uyE5|N2K%~Z7i zK4@91NGfM%f5(QW$AY=HF(b_DMiJvRcz+bL3Q<~eJ+Z!6tH|EDOHax{V&tQmoP@z= zZKFP)$*oBwu;crVGu=>&`SYoqmzGdZtQA%l!+&0($!u1VDR=Pu+rXed(+!rfwo8H3 zmXje%;Z7y1Ih4LM&{^l4Ad5TSIY1I)6#O<8r!$=A-cARf)q`PI0vnyAvL;uyl0uu0 zQubHV44`8VMmcLt{;iwbfZzCyYQK%V(I(dUQc$)*r<|{TRhzN1525b7ryA5(hlrPG zluyy)8lt>yhCjblM&pRx0w-)>5~ek}Rddvbv04DrlGl}g>OeIACyLD-f8KM+3`e@p@r__X_wR=F-1|E z6QHtkj)F@k2rQh zV>-ddPKZb{c#E0OSwJOwje~y|9XS?xXdCFNx+GOJ2 zoMhgkJo8dz$i)=Z&w@v`F)r!6y%A^0sKj8hMN&(D z3lmS_1Xr`3ihGVHP@43?R8m(fsi-+gu0+80OLNj!8E-9TOucgIIty{cnz9O<(CkpX zP@zyBl4%~Z)b)eKIZY*BXnyEQD4Es3`W2c?;(H#wWHkA;!l5CgkJ^QXhc<>jg)&>M zKxkVz+hZq_i39CM%qZrDG1R1I)b^U9eC1>EH5QrrGE77Y^zYZy)dwUtic=?Qp++nv zr;(Hj{ZhYd6g20Us&BII08{0oe4~9`eZzeHd=-3#?=Z80t<17!n7JQra1)hamgdtE zsOiNBWf3ZDS#KQI^ql9P6IbFmvs6cL!?MD^6jFynV8ijLmh)Ia_t2UXaJE)cayFOP z9ep|ZYZ;#7u+$n^lhtsI)$QY)wlEaC zt<2m+lFeL=6FQfQmxpeD7!~&yNWU*$XmV8Cn>^3MoJH}1JbNoWCmec!(Vy#(f3;3_ zcuX}j=WH$ki){ppjf8vkL7SaH9Wp`9vFZ0m@p2ZC%iKq@IVE|U_hbPdf@0db$#LJ? zF<00Xp3VR=42_~V4$C>Rs*jY|bk!eB zxQ%7chLjeZu5g4Le0360e^5a)(~ZA_r6=Kq#l~5x39IYIT}=fph)H&PHQM8Jc2p0P z$Q|e#o5?pAthK)m;tEcne1$W}i9@_k%bCCxjdQA!SmMl3l3T~TZm8yym{WQ|A~P>( z!9U6s-bHNX20mLR{N1K}w}i0NFm%Oou#JS;0W$r`Ib~uL`)aNJitK-0RK6={M?w9I z{*4*LR-8hSf-}9|<0<#z1l@7;^n|2Te(8l!oKl%x%{p|6e59lsu}cr24|ZTxKBGS0 zfagplJDU-HS&Zy^WBnH_tsdS>FP?8ZVc{OdR7fpxH}~K$*T;Je;Hp)}DV*{Dvp*W) zNZ0o6krnz#AIa}*<1DZG_Ag6ee$-^mb!Ly0V4B?LrlqEZb7Eo~W-9XACtyPjEg3qF z!d{)zlq+$b@=9y4UBKB3&s#vkDkVyTi^n;WCt3>@aU7O?6vsLNojnd{&5}JxG~9yy z^+Y)yM3O0{97B$%7k4=oTwpQDqV^<+rm$|?@`N9OzM6ohZcxQhjPW|Myg; zSo8~^{rE-}J@@SpMj^q0mjc^J?* zlVW4AJo(<&p%zwG>lx2*5z`;1z;1I%79=GD{nJfCb~&T>m>f+jbcL_%gn4K{gW&Q{ zK(uF3eV1`gVs~(CES$e#53MQ9R9=J zWw1_eG>8p2fYs3)dy$DbgC6w>2l^*x(mukOIY6o-4~|t*at$fTtlLzP0Ba-}Y$!h1 ze?8tt0H&RSB-q~^U^f(;7sk;_k7g;in!!ed6mNJyoHWC6_mdpW|5Ly z@hkE^&B?lK0%NQ|Ax{o>y^cFsTV~_ke&K!EUR<1yM_vhhXb){~D9H6ZbF)2|tJ%fL zEUVCdzBuK%?>_GLelI(i?|>|Wx1I;RB^g~au6mOWq=^mas+)07{+|3jj)}(&Aika` zFsn$UH{?kj1s5yutLdBZ zl$>jSvv)fE-C8`~eBh?rWJ-FIXS#zv{GKxto{+n{PW5}n9sEQ!4kM}Gg0!86+jmy2 z0s?FaN~)*@aWZGZYTM)ag)^&D5iWC@dS_ApO5t_vMXy+Arv3l9EUW35*?eC>(w_0f zSVfL~hOrU1Y#h#KeVku057^MN={nNMybc2-{SA< zP;K9_GKbS`qW{OyRe&|!uhDgp14fPR6p&DCv0Fd8yW7w1?(SAp?81)E4n#5Nh7Gm> z8|(P5caQhs(R;mjk8b<_es7%job#f0nE^bPf!I43x}47VUyq=(3B$9U0WQdX#JRsr zs&<&^JOu{T-)_UJ-{8}X1?svR4lwIc=~u&Tj)mv<6z_C3>Y$y#I1N^#bwB2Jvw)w+ z0}~hm{Aw56%&4|f)R85?0-h%f;IP)VTiZPkK3H|AD{0lq4Yg-6Z{T3#MFW{q~MHhV-w`VexK_`KB zEP=;fA`skbcvks%k{57#PvU91*vWP?D&>Aa#pp@b!o_6_RNZ1T8z|~gcpiSYYy{@e3*5gTu#Q@g3F+vp z4&oV{1k=(4H=B9L=BvPqjRyCK40h}g@LNhzcl^aC>wpU}(f{Ni8Wq6-Z4kOqSKK6( zH39EB4OP%VWV<0aVQXw3;CU8fxngdFM@fHEZ&McL%q6Cq=Kbhw>cMOCvrR(fvjT|v zN1WFd=t-u5CCvdVs6Y5YL8#z+q60aFEH8wAPbc_IvcbpjMvd?j%;4R?*hl_#>;Vqd z9aG|!K$s5U)|A2z=`FmF%ycn)vX0Z2V4FZPJsy+ANl;kL0(OuCWnmh8lCl`>;eK`i zzF7(Mm9!f`Qvdo7zXYq^Ofg}$a}c-rA<*vjlrfksN080{W1oqtDAG0nljN}$t=VE$ znal9|3-eU`}OD;SmJ_A)dk27q>q;)IUR<*z$&OlxE54ct7m`uwt`H8{Z z*@@WYi=O`q=Bxb>XJ|MVJ1w3rFOu{(lE=WkuPBeGZ=l05FW#)!R;D@IQb8`nA{a!_4|PN zt%Dm|61byOsObk&Ilv0$qu=;VS&IDA7ksHPz;D7(ud7kLFUD?!{-{^~PGSG@Fy|s8 zdV`rBfG++fkhZpt_xOb8kORc1()OZTj({U&XLAlFg-L!<4AxYGr^%vy)r-w7ae0q@Z-Y=XjMA^vSOzR|x9@&e4ie}F^w2{WH$ z_@OLDMK%e%6D@_n{r;<;>Pr^mDV72sn1GD@mml64yg>tL5oUI3%4t-nGMX0-5hTN!;b}r`zpS}Y_L&Bg0r^({ZOQx0GI0&;=%@~_HUy% zx`rF4v?YUU{s%rrVYrW9kXgy_bP^&e_ebYk0afxPFjbGDMkE36X%EGifHW9UXAkBw zb5XIKMV@!YEQ3I9<)IgShdlP;|I@TxcvaN@J*^9IqF#fo9zlAAb8`m>%3xq=ji_z2 z5L?yABUl^xLw)8iNyu+-(03 zNOVWCGw%0l^hZAMUTJ|woI`(uDgn;ss1zQ;(aS`;PTve(OenJ-vja1O`IE7Tk&et# z0A;uf{Rqtt9@#k51SF~%cqbHcTn=1uB)o6Vpe~yMhPf{g&WTWKrh*~(4s*J6 zxZ^B@hr~H_GZIX+I^y;eqqZA`)A1kJjC@S>2LZz{LTS|z2#6MS6Ax9A8&p;~=y(dj z4POd;wh5?x2p8_M>fO>L2c$W{5E$@=EfbP78XUPyKw5E{mqFWn< zUg|0M0*UC@(t)q;$Ge$??2&>y(TMr%Va!16c&D>50ljSb3AAG@Tr7^7yPKO$d6-$X znu4KCxQ7#w0Wk^8jZ|A|eqj+Mx5- zVWK-7nwwZeuP|g$e?;K*P@nsN$#@Dqi3V&AF}n6om}j5Ew5SvK-8i9?T0c!FI zPp=sY*#9su*a&6wFrb{MEk+ab&Zt=v0TI6Cf~S+>Lm#9@s-C`x)?Y z2%sb{tlhzB>SWmhv`S^_Z(ap1;b}||@4ywLAJ~e%aCu*by05`99d#cY^Br{`swU{eOFbBpoxzmbX6b?o^$%jjHPobwF`a3TDFYW;+9Y&tLxJu0!n@ju z8d;8;avLY?E1qT^kP|yR0z)(GjlD>uDP>F`f?QC53hk zj7vJaX_g}@I#W&}V~0Q~Hys$n0z~R5Uj)6vl0CRg9<}s6iV+?|a zlnhFQ$57Oq0KPF1zm}t-Sc6P^56b3D)SUx?k3Rs8REIf52z(vCg0~$G#JUFcayRfi zB5}Sp1BZMA_t9E%8gxs`!NwLq+qnZOX$H`dASk5|1LOD)S?3ZaD1lHgb^1RG`mb_* zEWEa}(Piw00{AnKPXin!dt(2~7$6E>HX1&+9B$6b5Z~<3ja>vwe-v`>bHx9Rh%@_e z2IoNozZA9HI(+_X$hukRea-`;+d~?PclHo>MMdp-e9jRrHQi8zDa;Mgofh4;5oC_=^0N1xIkJXR5AdJHho20(FJ2KDq5 z=u|HNAxQ+5KLFDOv!e%@1`Tou7|sJw$B@91xPlDS2Pg;?Q_;Unhu+|?G>|5dze1zA z2AIwdN;>9DyTF$@0>;QpU=7#6E4&P5ftTy^H>CKJJbgctZ((zYNq}bzQ;#>j{i@0~9%vfct&6yo0lc z&Ju@wpFV>KLLxs2cRWk9Oo;|A^S5lr&FBip2) z-mFHP9}ILN8Iuqt*ki|l?L0#_?2l)B0pE8iZqHpH!ihj4+u@v)pu3K@eZ{72k*)w&_^~+&I6*$X&v3YG6vO$%57W;~{HYBlk;km1=nDsesW=|l^#;13 zQedBpa9cLvrpyD&#S<9uWBklPwRr<^Vjxhm&0wl505s9!aW^=j*T9Ni2nKwU!EE@42s7pyYmJ@2Z@CA) zng_Jpo4_yn1dW{nnmet<1db&G$kGHLDCO2@_<(ERD)9n-{oTP5ISq}>Mexk39JQpQ zdz-!7uOQV;=`NIN!^kMo8`c(RGY>nDTC&No|0Yl9Ams!U2hoiL* z%ac{eyw4OdP0T}J>U9F+ip-GEN7B#Gm{4#AV?GlN=8p(Y4*lU+S%ezE8*`OXs||C+ zb(Rpz9k^^2VxP?kxYTUL2AHXsN9W*FUc=e%g{M0YG3_e&5lX!821Lruc-~Ji*><;z zz#m%xb}96NsP5*12`~Ury9WKxbHp14sSe&hn}A8Ok<~6hk<=L#S{@L^xp=BipmMwl z-e3pZ{8gyA!@!ikjV%fm+M54&=M=afWidN4Nz5X+*S&?q5r?(`eQYHFK@rJ(VGBlhEQF-k|op~2o=PuCRn~3*%_%t`6 z`?UiL?1yRnZ0My^%4gG$XPt26y^BdZjnW^ijzA z3CN1~Q8xyX6kuq8MT{D}2eRfEOp6+EQeH!&!v*R-68+IcM6_e@$Z5b7!eM!e=Vh{t z24~C%F>V+*KSfYd^aLZ_8Jk%)qu)J;`REU9qnd{IITqWe=7D|C4bQ8}y2uudnXeI= zO#^cCX8cRbQ6;m1TU6pz{qSm&rP6xNwgyc8MBtgZP&F+?H2TYdsU-`+@LmDWn0Dw3 z&Y@bmj$bhj4^%O$&>M;H>+hD84^TO`2TO7h6biQy8{0yY@DTB#42;_g)- zwVyDDGN8`djHsA{PcA?#rD5B^0pNFOhzP5J_T&K{ID@&>MxadwOyS)eQgpEEQA^yy z4SImcxeYwnGk9K`$YD?!9!K0<2)sjx=PN_E-i*w50=$);cqUnhq@CcFmH|(<>5Quk zIxsm4GmsU-s$+g&-efLd_GXTSn^XeRoms~?jW|07)1_?26~=Mgy8-xV$4ErixSJZwm6s8APVPJLfdkT5!Qyz+74fOjGdx2)Z3xs2;(0&)r-C zzlcNN`acDmmWsK=AD8*S>8}MpF-~jofqnd z8X)UdV7CnPf^;z8egKtUVc+Y>C4DB(0ydWSfBduoo%IZ$bZqK&c#~y9j}eG(Y78bI zS`=l}-g0&og4sSG$)v_SQ@ogFI`4u2RCEyz^0Gc!locpCv+08+gm<9Hh9TDaToFEFozng|0AkvYK&fqWF zfl)PZT)CX(1*@I&l80|Oa8A#Iu2cP1?Inh zw<@g#yZr#X*!KVBv4Q*GN-Y60^AjjfJ!UZ}a8p=@I_f^U>XXR$vB0}6cpm*wQ!GNI zXty?^e%=93xhFsXt{{8f$K7jVEd(0NM}H$jP4WW#)$w4k{@t{}u$Z9boe0&2Kjz*7 zeCx|G6@Q5H(uAAx8OoADIA1w%nH!86^EvvjPjCQ;1JfW9zCC`J3FqNdwjx5$1R8Y> zKDwQdKc}ICGoY4CL-Y8gXj~aDYSTfO7Ef zeTp#JfLdsgd7wBOv2y6K*b&k+_xG!b5~R* zM}ftT03WOXe4|6Cg5BX>@C(it2XMD%qw?_sDl!2Y5-DRC_|)q#A+`epmVtxZ7A(Wp zKu+5M*Joo=e1$pyy^k2~8PDONkwp!s@S)m`CN&|3N%5&nz|F&P1K*)5-HLlvgjzWR zxZ7=LmD@PJfuofR_2nxdCef%kst|pTIY!{_+u?5&56;V3)E#pWEr&xLn}`TD7R;(_ zbe$H`ZJ-7-~;3E7$^5XAPVUw*isbg^10;yr;_CYVHJ_XB4WMK49lq zfWB4RCpngYW4R4j|2}kkV&J}W9rX?=5KRVf(HUS9GQn(dgFd_+zUu|xeAZH(Fy}8r zT|XPKb2}ztD-jWnKKX&=)z^}xf57crD1%?bEzF3|KF z=%UAACN9NE-fU-Je*Q1&*%;J`VoVY@VWQlEjR3LGb>{z{yIF?5Yc8cb`m7t&PqZgE zK|L@>SWT~l3#Sa5`lbMz6VcM?k7&oBYm29us0qMtBcYHhVuAVIik&Kt zXcg2jS`HX+xzs%HW6H64r$089u0y5pFZ7?`*r0F#?iGcoIPQ>Hl!xS-=x^%~vx;!9 zuKjf^L)7YkspeID>bp>hUk3W1$K>)Org49x$V~HP(`k4ne*%g%2)h$Fa94E)LaDU+ z*&2~c$mnQKfC(c-hMj|XYCl9Fv-P>v3D5MUO$Nkdw><*+dnjhb4D3nV4PKQLTTMgI zsUL#B>@rk%vq?jtsof83DZxI#{u)|jHgKC}ut+k1wuC$WdYuG8gVYZdi`7;ICN6=h zafEHUH5uBYPL`|SAAN?}!)h{{Zb0wx$n1t`RXnhi1Sph0qw1W9DIE!nrfO*4)1c^h zf%@?%_-XTzP4AFCk~=~}l@E`JcuG&W?EM6WyOZ*b8cmyym^>binB8fK$P_L#Y(S!M z@$DDT$molv(tm(`;EG!EK3G}n!9xB9XX~ek;h&Ld4&pg`K-Y4EoDQDxe%wYODog>` zu>*lic19O5jI;ysd@Hiva^M~w)+6Rh#s(n`*3ycgWMOmZE{`Xa>Mzo(?F*?oO^5w>p$izpu>~t@4$=gfSKky%53sT z(h{gix7sdXZg>Fkd9>MVnr3^ zwm3{|7w>LzZSj=F%BQFn>S~B?W}&6ZE@7VWz8JDGtaE5Zz$xFgKG*qcgGu~p-m%_K zyfwV{+{w;thR8A8ve7V5eNyp4Ei^=0G^jpoX1aNo)$S-KzomH6&$F#=BfU%gjR77( z!l0)?eS&rc_UE7Xzs(O0_z{p2(2Kv;m&v>7@z8Cl%QI(*^C#!tPIUGV#zxv6>KMur z(k7eI%r~VNJ&oDK8H1O8m2Q@uI1^U=!fV5G<~E%#bxNz0_;rOR3lw480}+On|ac1ybSsg&7j zZGGNql&z3)Wp`WpH7lD3w??R^5Q#P`=`ejNw;^yys3v$};2Xa{zXbt5!yS>bh~~(g zD1F4MkPChZTq?Vg7D}<$*JF}kw$Gt-fTPn-GS5EDy1{NC-J{-T`nyDXz4gBnoDf6xBStT)Irz1y3B}96M626CAGih&Zr%dM!Wq2+th#E5@kmHs0wy~#vmEo)Di)|ZaFpF@L z_+$k+hrSHS2@VOm5%8HG7uY5wE21^lIi-L4y0kSZx7utcwp!3o_EydtPPa9Cg= zf4t90Zko$=);Wrg&DXrrIEwg8{N)lk6Ne4k3<@1bE7X)}kLV8@W*g_j^ZJmvqoovn z)+Xpg|1*Cx%^{}hOSCW570P1yLs?EsVbcJKOthx4Uf^Fxt1YaaT|J;Wz2<)Hle!!A z%LR3U?!xUtcHN4?F$veT-aY|AK{bJHfrNjYuh?7Q)6M@#(1*yoiMc7VgfiL+w!!;}F*e(~N8 zueUsf_iWyD?g{5S#x=(*<6cd*;zetp)&cUqiZzNWieHMkip#Atq{5aj(%-FB%0Aiz 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%0DP^)XBl}Z4&*Z|(A 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

O<~7E7_HN&TK%%d~OYSv|Bcd!Lo*-9KXk<9W z%Gk)!vmGoQ3(FWtvf;|`Uc_yLR76MEZxi zHQI+7ry8y~sD<>&MxODw;gaF5VVM4-cBJ-@`j)&wk|+I7`B?93b-8ue_w3MsPrm!O z*<85S7-l%xNuA67#aKaHjOAl~!^({NB*cz0jmN91%C1#BuItw}qO+u%-Sb+UAo(MC zpa|E4wy!f8Ig`8-Y(J#?`ulz4r*pqitMFWGKBg00k9|x?CaorlabolY}gXcrBve8|NCjV+q+4`~x)s4-&I-`3H(*1IZ ze2Zd@wi7yHo9uaoJx2H9H3hy3S41uh8|E9$Y9wW1RiN|Kg5ODcN^HdYDS*{tcTji?0_z>?CqqvffCWuYga8oZp}<$cfz#bVj!o7Frk&twd0hY5aM5_q z^wY|Blmp{$q~n=A*H&+ZY-jC5?Cb4c?0sx!En3h*{blku53)75#Yk_Qjy#3K4JwMI zCM=779riZhCGUb)gHIKIzgH$>8)X{713zXWD3*y@+Pdn0{2N^PrK+`lUn{w5w&;*Z zDcst#U4BIO$8y}W6nBY^73QZ=f1QTX$${bKU2EYb%KVe$`HI@^7`b z3fk5Prgk>;)F`8&E3PeA13lemte?k!Q{dX5QT}`%0sB1T3-t#zj?v9LK|e~qf}e%L zxQEyVSw{k+Io6&9EX7<=`f?sLTCe21EoXL4B7g<`nCEQx+E<^cTwMNSZ(@h>*JY)4P}-3 ztq%zIwR!zy2eLX@zTR-3d0un56TA?daa1@47y=CEWjVsu)+_Z(Y9}-tZkZ=IBZzPN z+q|^tYRmF=jIc(6)tzyS!Y-y&a0c?W_`UYu7jPlqou9$i#~%u0hUSFL3OgCHEuhk8 zGAp055q}4G{k6b8^#<7okpQ#1k{p{X#l~^E5!xNvzuFf%xB+QgWehRS1;4MG+Dgqo zO^x=ley%amwA4HdtS?^}y9|-WUw~K^=&}0o+E;2{^<33SWsPzcc*BO&zc&<^b6f%V zNcMQ|2roG29diORhMCDN@*c~Du~)NlmxvshEzXWYkxYIb8VEZ?n8&bk+xc4pF9i1l{S6uuVhIt1eUIz|)PXUP(cyDK zV*(^z9F{LVmQqegCzRnunBPb|Vmd6yxzrL36&mjv{0uDPN$9@mvYBb!0eo`Vz`hg) zEa5VT+;z@F1wE7#aIt3#@DuF>?m8FfV&AhnEeFjR<_D%gQ?yxXnQXuAZ1psw7Ltp7 zu7)fQO$b&89s%F-CEr^?9U&P(F24{zBYzb)o7#*1=<0%SYPfi9$D_6`0iv^C=jM*U zjv4K_ZHoolyDEA^b&{>pKhks3a@j-086`m_RQYO-X=ZEo zY8GnO=yRY#^EvAk+h*$$^B?FNB!&(cR~v|iRNX~2RTZJyp?a${DI!(h)R(pU4Q=Mb zo;##s&S>s@_E+{N_BmEQBa1tTAI{sw9l}1tmeU6l=EKS?3$yP4^gaw&IrYn`b+rj&a3ngkNjWk6>c(%HV%s^6W!ocT85ovcxY5 zhhtR{B|*P<{W(i$N)nz3Cv3yoP-BrBJvFvfP?5e=vsIO>v@4IR9U8OloN>HK3%!8+ zOxHjgUF!b$fR0p;FX!|nJQN?Lpm#NOfA&>R4`tJ?Rj{Xq)F(xK*Px#(QY}A^lEzyJG zg5uUhH-<}tfBQvpcQam52&C`$HMn$K7xplw82Q3&wpK!)^a3qWdtEEkEjBQqL*N?s z#T;l(F!RmH=CS4s%NSdsqZc$&205SG$J@-7zGk%PlF`S2(i3%uv>lpf8nMO!$k||R zyY7KeX$f|TG50y&{quPY&UBx#{ugF$6-H=3BaL zfsXCACuW3c8MGRzGVU?I;h&Gx}lt}=u$+f zuqwFvpmsBO(^+cYhv>tgg-!@x!tdt#2d)So=lvfh2RndvkTw?e7g$uTm>MNVTgKNu z{P*zJ&<_{hBEFxkU(|k9aJjQk`O~_@`5Zj!y+jw#)^dyde@0GCyqYvMVM;=3+~H6H zx0P@jTY|fabs_x`N6`Q9e7qff47nXGiM9A+yE#y&wTcC#({ zMv>~eG)BHo{YsS}9W6De!}XukOa*vGF-Xljtw{S1urDwh*n`4hM&~knlw~8NGd$2e zRTau}WY^>w3WIDtcrKl-9Io3AnXQ*xr;%SsX5W6{CJ6d~5pmRx-+Z*lxFs_L!(tzM5xb-{xNTw}<&hPK+*% zONvhMFJ`@@e!>%6JoO6ked!g|1YI;_w&~rz2tIZ%X%l%QaW^TGebskvKzYE;AZL)0 z-@@P%kE13#Ug+nk;?&{VBJi#`K=WFATRQ{T0P?gG^c$c&>uN`qI|U)ajK=@Ku0x$b zjD_!WbL<9it}ZhM=)5%@>i>XGWv}eAyi+w&R|3tlFM%z@?xF<-*wRRuVVriKJ0 z*M*hfF7-c_7Ykwy z@cSO?mr@+FC1|{VjNfo}E;`T3*Egw-fVu8;MVrjlGeY!2@>#{utW(X`%rZ}diGh>$ zFbXV7z}qr0@`P)jm1y2#U~A{A-zrLbW4oALcSTo4-#ZIC&Wa=oj=stC*trmOk<^d< zJ91l}S4lg4$(+Kl#c?g%X8TxEnCCR?pT0;Yk+(=bw6;`nYf76DP2H8hDoffYLyN#W zwjHqVxJEBW&~E?V+)JGGyi9+I-#EW8|9d`H0N0B^T!HzRvP9u+M@14ZtywCb*V-$f_V;x|}W8R|LO%t?B^u4Ot-L#f1Z5zAv zEu88FwJ&<6+F|HBNMF=>>T3T1Vdwofa=&u;KBsvLytZ-9sM;@qE6ZP>ra`K|$u$*!M{L#9jG+v=%mp<;$? zo_v9FlG0CpMvN4NLBx%aq;O*=}zYDQ%0=K}wY0FGTcdYrP zR3YvsUn@d4@2i8i3~HNFJFI+t?GQY)t4T49pwg#;aX!0KJ+Z8Ga8R6T9za*qKEQBA) zu+aB0+OcZ$LA6o!NSP?j6l3IgZGoW=^u#jCF$>K9Y;={o=YsbfKM_kYFdQ9I4&UK! zbPlx-Fz4x{U?*&t^0Km38K>rGe`+1tN+ZKYcAj;_xDKON(L#8v*iXsQ@b%sX-n6L9 zPyvPktRF(_0@Dmhq!8abzWcwXfwi)ROD&UYx0VG|Il5PxkHZ}Z7^a)vFZfE#m0*f;4e1?9;FO+&0yWC{hv9OtbFeh`%&{ycDR z2s!#$WJjQcf6_Wl%!FzLOMZyOf%Iq*4k#7WP9aah-^bF z05;;GXc3q|DM2iNd3geyr!9Mo4lP^tOtwjSL3TsFS+0`>DpEAVjK$XV_Dv3fdo|`b zjpjEmc4G88UMc-E?|N9a*F1Zp;)t%ta#^bfyh~AUPS=GNX|qp9d&i?DMqNW|v1+rs z4EV}zxHX)+VH;xh1kd)K&duYE=WS!h(-CwV+v6AcgJwzsR^TEG-1-9PktfXGTLZzFn)$R!XQ<0lo-WZ$wcva=>qj! zW46>naYOril?_ObnFsjGa6W3GU zgOfF@zZt$j*=COYIgE#2$ZQGO8b$M~rgw9Og)aBkBL$lK+CXcukt@Ln$Mx~8Ve-QC^YdTqt-?(XhF zu^YQlN*adgIQiDQ-+YMl%pZHNy>_h6{3}UEy#zW>7O#5hCJ>n3?+c=<1TxCssqrf(P7h@YI@WFkh z2>WAU8j<7dUYuIArP!5wBFiVcWu7_b{-5kV^@zY7VP7HTr3^ zs@#Y8icvd4#|Pi_-(-BD?W6ZMGCGR+D|8h4v35hSb{P4bX&TTYMvA^4u^~FG0-Z3w z{MB;%%Z-oS9m@L4K8L(!sNYC4?2pXe=8q*|1=VxtoQL_HitCgfv9z;~cFhwWDqrX< z22}-8JSfXOzw8`-lI%HREg6*vvBNeKOPYH=8b-uUWP_ z9Q+(&kt)%@DDHkd6Fkf-Ev!z(=VcVKr?ru12U*jTRT!POsBl1D&y2Tehci}WJxSaC zrOvm$IsZBjV1n~+xma62vTUVGaiw7eA$;WFXlqE0&pEHh#(wJMnC6$y%%KzcM9Vwt zaDkV@N_%78f#yrTS#51>4vZElaNGMJ! zc=fkk_NkoK1?!8;nQPj@9gp4Dr3vH(Y79Y$Gu)#+hlKu~uNDOrF5OLB>8Jwk@71h< z*`e91vxaBS$=-^28*g$enJ$8hIqvAorxKmjPM^auLJiSWxh`iR>pMF;RzCA;0 zTH{Q0^t68yTN%4Ym?N?Rs|BQoJ&w8=cF6a=Q8eagwaj1Q60?B|A#3xUT%-BpvYHq! zhY0C>JblCH@O$NR-uFVdHSTBG*vLm^+)?MkK6}^GHPdq%w|vT(;UGN4E{9b#IZPQA zBdS$GoiAJ$#9m}5YA!yoo4D0xaUs z#eWJp^n+8GtFjNL$9${%Y19vOPTLZ5Nm{X&Z5Y+wuUkk`zEUIvIVlj61{> z*dg$t{|)0N?mjt*qM80ADb7aS%}D6~Q2c$*I(~%^FPRAennfbnn!3zPVBb=Ml;z?G zPo#4`7^$|_>!#YMTaPXYC|*{0KA+61n_D$6HUE8n1hN-ZN==rB_Me_YVx{I*U}D6N zpn*QkgM4C}M6TDB3PoanYNq(g)G~h@avf*?cFL%b>61M!qw}wrAMO4$$jdKzVft{Xz!$hBSBm>7C0c(K3AFj=*P{la)tE2NtIPfx6H4oV~A z`9z_qv{h+CPNmvno<};jfo;TS>BZz(xiv31>f0t;-jp0H5Oa6t{>G9t5F*$Z;md}rYT({1Yl*L1~dc;^4cI6)ojwKi;4@DT2vtCjnYnC*O4NaWnk zm2!q`Ib2sdnCqm!=k>?9+px~3TyVFD$dCULTL$BqCK_2j2?4W=ij;|#|h_YzK;A08kP+)=eY>e{Ab7sd`D-r z{j=qf=}l2+VQ&7BoNrnEvqH0fWM2Ejr-l7#n11h%^t(p-n=EV2vb-66MCjl|LNd1ARcrFc!gv!F1q;@`bl=l_n&-T3!P=8!D^{6?jt z?3WxV&acu&-TR=F&@;hZ!{}=3N8qu* zlHl(l!-LHJzJY^-SU;Fi2*D8`0!qF`^ud5(yMFhCg{3QvruP!*S5(VRa~W@XF>ad4tY&- zdgg4(PcDhF9<#o-+T1hQSg#=OJmXrwtKpTRAD1~F`ZHuz=<%QwUvF=d*Cj(O%`fy$ z)M96;DyzEEcZj|85zQBUJ8cj35&cL%RZ#2TyCFwHgF}s>w?a#Uo1%m1o%$X-oa&|A z6$f}?9baul&W5~---~*}$NV*E4wO@UF*BLLjN38t32_cT4zs^!yZ)j(E5Yuz3@?o+ z)tH^8&qb>XJw+PRl#&(2bxbSFC#|#X7n~ZtyF7_9sub_9p-)0}0Yd}X=n8RHLyzjy zR4+9M%4#%0e|@ z3u~a}ksH}@x+?}z>!lf?zwCqF7GKtPj$cQA)^Cw-nqL#YB4Z5ltfFSTx*ju>*a*d) zLM07Kl0)Qcl2cBBe&a;+N|aFomTV(>R8{_p{K)-g;WV5 zf)jM+Q7?AzJ9pCLi_S6xFjRMl2pNhQ$VsK?xD-8!9CJx!fvSmZY%V4hze-V=wh3@*!ZT8#tH+I7AZEa+J zZdzOtiM&Z{$&KQ1MU4vm3YTMYUTVIOJ23Zs?xDOvd4F<09BIJ!_%*rrHpx>P#WoNYbqO5@X{*6cd(rU6^L4;a@3j*HwEnr@h+ zDyLnf^`)A53LI@6hpa2W<{43K&zj>(YnE=ZG;lo==7?|kDsrZ(k9T*UZ^rRHn?vZR zy^*Sj`w@{*qocb;o(yXfGBmhLV2V$9!x&w8eYozdDvpg(_0+!CMrw*RUWTj2#l{xK z_Fg@_SEKG@u|6G@K($p<85b!l6Xf2A^AcPK91rXj90wei?dxr|tzp*9*2&iA=I^FV zOkTcJyt*hFeLD*Z`xmS(cvpC_s7KM?!U09LqWwkLg_jEx3Kthd6>Kc97v3n^Uo4jV zF(0+Bag7vuFeYOgO!n(zoEef?=3U@AU6OhwdUUH$Gx~#bVxK=1WJEzA$w&RWRKy8Car4 zAN%2wDJ7Lk@{13Y{4Vj795;O`9c39}%W>3lzvl-ld(_9hTY6Q|>AenzSi<)DbW>$> zU9^GRQ*owqmjh!@%~6=v>Q~y|#1uOUg`&Qt!L}y$@z$eO#odqE%w1-hF+sZF{tJV4 z23+y)5i};WXXuEaS^h`-Ci}hc-k@Ki?XNqb+phM82Jv&va6PSE&xNRdYUA{aH5yf} zYAAkh!7fg39{8@yqa=>2pf|SCgvM>qB9=j;IId z-Wpqbne|@!Xk)rpj$xkGrrDs|sB5ITqFSLop;@iYg}%ra^dgi((|sXSBNj82=}DA@ zYypMb!DJ-y2TEXV#E!fbeJ|ad@%F#g3DygiiRK-pfo6v}($d`GvK+Hcv4)|>urGSt zr1sPF0)8cKCPHS@LA_0P1is#x|2r&3?$1~bE%vD_`yR@DV= zHg}wB%hhC?F|kY%-H?0;g^P;NEGUC2%EQn>Y9(zHt%6x-D9E1io=%=D_bHbiJrSE7 zPRB5Z!6Di?$7g$c`y9L7Uc<52-qqgN`Ow{ouPs!ToWx14nvwS&Y+P&z@somG{^`0L zZjd@qZJ>T&7C?@tlWUr7tTn}!XX|K5GWnXWoA24~+Hct`*7J_WVj{I28qscUw$aDm z7|=N2aga~=B-Epn4cs488ff=!tedKxgFNXyRf5W;iqyQ;#_DV78|nt>0`(Pj+cY7l znr)))&Y~+1`h*`Se?kqd37xW2>MuSJ&hw0?u6u<0zN@|Sfg{Ti>f8%-(bm4xw%+=W zrM|gEsZcVlct_FVBJZLXg=Eq6qD95CO6HfeFKJK`VEU&tsq|8*x%9dDhGnJIW&7jk z;65al(9_k~TrL}+3-&$eeL($`4u_Wf9-`D!$2G|{*>TzO*u29!()QBQ$2`rl&A#6? z50!vXu4$g<@=$slbA+b3s(PpQP@jD72>zkePYUj zywV$`gMl>4T3=W%*a*iv=Vmu4l#w@4A(}b*3)+L4en#4VxbH$;N0p@Rp?S@WRQf=> zWUcVk-O>F7DraBO=b7tT$7hLyq|V|_p_yEdzRGRoYH-~&XN;up65q1^^MlhvcLnbc zI3Bn#BrE8IFXPqF%Z;4oZ)1OhS--(>&-lQo=tt{@YMX0jsOE8#xqhq{^-{S`o?#|1 z`+%J;lH(~S6g^gveG&H^lzO8V+#=3IwbNry1b@hr=YH#Y;JoDg;#}jr;y7gAWb?Mw zw1wCnSX)~wSU*^g+eF(8+d^2DTUN#T)|z7-XM1LwYfp2OxZZkh3P$2McSWlKBHU)^ z>vP_#ueK}{GwX9#$WBsoRQX);>~?CM9@kCxzpm%bwXQXuxzKh?5Pu1qrCKD%lw-Ov zd$?e2s_wesrFV`G;Uju2^J(wb*XN#Lq%Kx>7kS8*+8j-N^$~Q%_-kgVH?j5TGt_RX zE%lS!L3Bdz%4>N)kxeXAUP7JZ0XkUjLsu|Gh=+dGH}`Az7I0Zh9sQjvFnz+;o$i|M zGNKD!>mGrQ;r@2XmTFsMyKmK4?^v!_>RV4*Us;diwXU{(wBK@A9DST&E_A%`Ck2lf zEpJuaWM?j4lc#B-QMCPy(~b3Y?bIQv4E81UU0H(l&E;>ncer<bb+e zM1A61ajxV|n5nVMFg8gwM>o^!iT8c)aXzWO&HY~cH1wY2bp*C+lkuV9m|?ZCmhp^1 zHZ<^RG$gQYwD?9bA{Yd%;XrstYwy5&aSh(g{=dOef zz{>D+w4;fAk$oBXh9Vb_IxnqoR;;Y>WCrt0^<7gQ{_}^SoX>vmhlW@~f>#^w>&7;Q z4#o#wWxc|U$Blm8AH1CULTyc5O?{^Jtg07lV>YsTn5uLe>OFLrxW-LNrQU#y_R#X?H9iC=5e)c{f?D7?n-%EWKp~>ISvN zb?EUT_#l20Kc8RVdF!s>`R*Bm|9|4|3i)DtSx+>k2cus{Pz}}w8kc#G@?Pv!>ea}n zq3?6w>Apd}M&Io|p}rcwhklK);}pcmzeS;Y4SmH5ox2gu-)8@-7ss+d?a)8uWcWLv@&^vgtBR4%3SBaJ5x+)orvV zbxU*s`W(YTFSnPE_h6rAzD@lU-~E0&{ZsvW`1kX#6wo4IynmoyOWz$n8I zbJqcM)pl}hvq#u|T54OqnCF@=mo97^xO?tJB2MP`KR+?LZQGhe_gB;iMo6tB@i7Dda%& zaVjzx{^)2K2|d(SR2g`ZYFs1LTy$O>S2xsr)OcxYYRhS}G`+OF5cz%AHq)-u-qhaF zm{mi#u0Rp*C@ZO@zC-kcT>?Hi*j}on@Cm=kE>ly&9u_eHgcv9g2JWN%a_?{j3hsF4u&rQJ<`S zt!|{QrY=&~)tplgQzvM;Ywg+qT|M1W-B$f=Ly_T|p@E^Y;iKUJv}c;4gW-g}r{1pn zq8q52trgJ=J_g-(c~I9n#u>4zHJNI3BWe(Nfk;($K;89|_#Qn}bHLuLMm2F`RH!aM zztwEdXlNcLc#?rR0^Li{Wp~&~I(5zzM=QrGduMw+`vm(1*rea~OuN%w)iJ|y*s;se z!J*g#919%}9V^j=*2ulo zN%oB7fAdb{WQSs3(?kb)L+4_`&JS@7)F=gU2rO5SU=Z^7AN)W}M(>zC=SYb$D>Y1Eo{b({(wfE~lCSbykv-e6Y4cWapO^eOb^ZYCwn z$yka0{|#7=3i1?55hEoh`oa9gmjW%YILBJg75595#n}oGo6b?*-pv+njkg>!N0`r* zHY%k`9j05RsM7PL&e9I%*Qk9hv3Of)>pAN}+b`Q}yI{ZL80rK`;5y`%-1R*hJ&pKg z!dt;4Bw`J3L&rV?>W#mkO`9gUB?ii|0nlU~tyF;WF-tB74r+jDaSfo@*N^?hCURFe zTIJ?Q)j?GQb(VUaCK0{j4bd@e(f!pm*IBeh+Ra*(HcoR!orJzJT73-NYHC#~?ol_c z3)hEBVJ|WVp@8~^x`F8|yHwX)UA zT41eW^S70^T|~F&8+4I|*+o=v>zoUn<6Nb#neM&rP3}W(xBIdu7Tv`4goi>c=xEP} zT4jOQNIESYkcUFc>jF^?_IU`6{sidDZ->@zCL53G0EOHy^#9)Ha=B)znwV481rt}Q zYWitXG(EIyv=_CPF%hb@E=yNV|6aFBS4NkwJ*%A!e3ha(tXZyEqM4_8r?#uss%oir zbM?8&><6YEdbb$Xo9RSM}Od7XfMtM3*Xf} z#(fu4h5}tbofnRFm2nhcFrTS2=7-Emd% zdWP$K(NiU9@4y;6G!FDcyVV!fOK}70szX(Cxw70xbjOv)%%_UXdAb3;iK+`5a9HUG zC2||?@dNnz_Ru%pDV1Wy1EDneM#$%@qEGO>yS)2|tDdXKISchwryXTbd*0MJ!g&jR zr2_h6SD|a5nd6;(gT0ddq3wn($@atc+x8UL@~Q2UZIb<~eWt_ZSmW&LdgYq!9^tv~ z(V2Z=teJ4zC#0W z7c>SRKu^3k`Hb|V+Ea6(F*lsPLB}z7m?o^iCgIfb=>HWF#b_~=VGXyCOX7BOd!Rad zggeg-<5Jlc>|OM~{=?`Htsh`+FaooMt;;p#9Bg@R4yJneVy;6j`sW&O{ehBq(g~Ou z@Q@r%z5%MS!uPfYvs{tLMn7~Q!J`wJR<@!Gwy%^X_7Xi%OZ>*4MK}LhzR5@`bJR1#6XLmpxGmCcc4fKlxh|mpeWGih>#d7(1N68{5Xt5`ui~^1az1h7;y#-l zg^qI0G0q9-md#V5V7z@ z4X7RzPnFV(nX8Ng5yLIk%%($IbOv&jNm{6}<6w-xRRe$X`TA}$mUVS2=Apt>}1g!D)1f|$FUvIttx zEim0E9vR6c&|>_f7>RFCgYBve#%F2(3vR8lM{=?So$EXYAN} z=t>sAkFJ2;=r!Qidr(;>(XYzcQP@tEXD6H~Qta4*Y zL%k;Nlj%_5%p?vS2HNl;Y{1B&m7&yU44jdf6+Ehk2cqcQJ` z$4raSP-Oik&sKio`L7~A5@mqf?Bp_Zqo1IDQIXJtoIu~k%!x!`nQ=@ubBC=5-##5y zC!Oo68jWu2k(k3VP_<0eN_7ie>sO(KeSlklSx<|(9o%5dJv#y&$_`v2yzf$AiZwvL z%b_;8fMJ-6m@v_uYD)D4K6*iZCUePtWJhA7@(p_S!BTzkEO6pH&k1)kOeenP8Vt^> znQNu%wJQfxGyeaX2d*G@zUz)F9FqnnxvRQoxu3ajxmUTbyMsJSple?23Bv4xe*7-} zDrPU#5q1kD!a%VRc6PTIDt(0-Y(ppkFHkDN?rO;)&}kloDF8Rf3*-wt6FWREPt+&p zlS$+`JhvuPA~hT;seVhgCGIx(@zB?mz(Jwf4>hnQJVNH)jhgC2As(+~S&p;PEv^fme}U6VP> zgs^|u9o#I$V4c*r)DfBl%s+XkzNG#REO$pe8gqU&U@pdObrsEEO+8I@O_XMcX0&F8 zrm^N0=IMM_jaP-LF2h1NpaDCK^?_pcM|jX)%n!^383Ima1o;RWz%7a0h#(%xD$Iwe zsDyxFIt(s&t9%7hQ-;ZU`5h`yD@zHITdXE775oJ!ko!IU6rTmnZ%D5rg1jum;;eea zwbD_{&P4s6JOsCvgIyhtlba^K#F|tUGld(7`()fu3;z?!)JJenyWzHe<45oTKp-9Y zdHgYcE_fX$zW|spQCupPh34>Y;M@-K54k7Q(c6Q^eS|8HL?Rc{B}OWdN{KAXU6clx zc=1lwVe*B7?CKUw%}T{;?}BptJZTE{FC4djxU^k*4X^DbCqkuujqJfbIOPklE{o(T z&~5hv+B*(hP08KlW%6A)PI(3e-p26ck6@cNk`sU{zCx?DCTWJE@En|)+C&u54HYMq z@pcH+YG23#azELWJWq5Y7~-8`$1@#4aO5?zBJ|EtRg0-KGw5kh<$XaNq~=5Yye`gT z;QzI=@l_w1!$YEeLzz65@`h4+TWSimg?bMLYzMU*I^}8b93tMY!j02Y*PteRiRc5a zw-ahWssQbTDpP?%%uo!DA+8X65{tyd=^v zvAH_g2h8hJ%#qT=*J-F|*n*l=4s`1GkdL9JUJcs2hp=LIiTS9@ISggq5F(V2F{dIA z>e0DUh%_D1WQg#K|HALX8Cc9G@~=FLF@5T{$K$bkDnZRWhd(XM6>o|*oUue%k&h@^ zFgSByRcjGFh#2Vo&cURzvQXr$jTtZsQ5kH`JlKJ4SbK`lPsB|8 zWN%_TaTIZJeXwp}5$9iTkNI)}-E zM9B3p?`njyRXL}4gDIGfa~ny#R(3*dy9MF{2@{-}z-wQ_XYRmr&jaJ^PbOe)&`a$0 zd92nH?4gF3h4Xd?Yf}Nf{5F0giJvKt6VU=%*CU~vJPjwR7O?Ac<~Xw*GtAmERhcaM zD!m_HL860>rx&1?bTd`QCuq~wvt$^pRdM%;rv$_FqbdzE&|DLG!giD)MQ z9N6anEYNQtg9qTfIFS$o#7>xg6)jDY&cV_@MihNRk(7(Dhz*G>Ot31eSm0I9%SX|X zY*glBuFieVY*n?a}Zf z`A2+9@FG#LqV1*6QZrble4NKDbV{F)7eJ3bP&P|7uoHLi*-i>YtcOvaM2g_X4&WXV zR3fFC)N-rVNb3TlW^Z309(q~y(O|5eP(+wr|=Ds z={+crYo+GWd}$Os_i9l9o*juPXA}5f%uT%Fx!@`Clwvk}bG|>Y#WBpPEalb01^xh7 zC?0J0d3-!j*du7ff#OH8v2HvivQ5ItNTPk6&zAZ5S zXe9iT8PBv4d}(<^-8+;VOerc)+=fjW19m?M^P1L^$H=SjD3kEo-XdNLBWU82a$cDS zgltp-mF~(4Wt%bzNMtr{?@(oeGFe%wtixZcsCn{LmUx?M?3K2Gcj9k4`y(NVItEo`Us|seZeHB z%9zp=$*jR#&xdFBE z`}7Ywh>`GpjpzcZ5p@zs?jpevMx}=Q0v^1)v`j1z4gmfA7BU1Q(AOZ~g7WaxW98j) z6R6OCmQpZ-u9K7j9(x#WQ#Yv#aB&Bmfo;-$-0&ZgEWH8maUA%%KF-AnU`K=83zl~@ zCSre53`7cU^%!Ni(nZMtyO)cbe@TwP4%|Xi9|FHckc%-VyB@URf8xQ?Z>cfVG|c_# zNfnZF$OU8;X~k&{p|n`5by%^P)L@+9{=ncbsl}LoWr8wk`qJ4$t>kywE)u!^D5 z9Gr`2L_eK?7#zf2GLo8%IaUnr_aID7ETCH>KXad<*$eD@HkXa#!nt7X3)JZgnOaN? z=7-m(vw%}d;Hfmg3kQgS@W^3A0(?&o#8T&g6>eeP?sc#|kz@t16s=KF|412xyHgK{ zg+h$?ULlCFgpS-nmXesNL%qS2$VWJZ`>@jO@bL^PnDV2t5JBsx0ICMOT|Me8aK%b^ z^rK`${KmEbpi&9MUHhTSqFPlkGx9MB>utg*r)g?|0069hjVmM^hMU>f-ppg6aMmlaIZ!p zgK-Hb@ebnSnqal>KnLI`c)SzW~^6J#F-a~ zB;*eA@%L8C$*>IPz`E8ED~j8|Yi&R@bz82kbWk?Jv(*3^>I`4#0FM*~J?ZL*6cVw@ zM{t5GVuIUAsuyiwt}rXuAaKRIG3%_FYL)7#>V~QZW>_8JB2Z&8iHik~F_Im{&SG1! z4(2pdjiHfWh``yY$TS3Zw+V9)!!XB@!#v9+>^AlR`;JY;<0qSfcsm+%;KC4PUZ4j; zRi!^}R5|bi0c1a71@MXk=>52SRel9dXCUxe0^*e(ShXYYxLd?-h`>I>>z)7NLe`=={_X0S>{;qrjVLo3Ib{{^!7R8UFujklkCQQN_Y>b7J6w#9 zQ!r`d6Mqz(f>m6B{fj`1(Og!6Pi-x|24|Eid=o|pCfMD4p2WF0f=tzXX`c&kWq(+^Yv7wj z`?gR{?6-}c#N1{2AQQ6~OjMA{1Rg$x%i>;ghrwRn08225TY~oi=!Z{IMX26zuaHaF zfO%#sxk_N?p0oGZt>90L>^df&o{mb=EeBn@RrEnjQHf;Ym>%FxF5~VDz&-FmJbo1@DvJ5cEMXOP zEN;>e)c`PjCso~5^)UBt9PZa7?lboS+{;<+B$o;HHx^SHbEdlTSPqsKpqh__zX-=}Hs)Jdum>CXF z^chjhSD>eZs5tluHgtgK6#fzRVkJ{>Ywoz$x<9)=x!<}gdQN%D@n!iu&o4|Dw0XK> zdPsGllh9e{E;NHO!51M*mMvJTvXy&8Z}KC#h-!=&+J#+AVj|c%uxl&f z8;dc~HU;yx)4-`s!c1QWn}^RikbygjNw*g65t!TFs((~1z_ryuv@=RoPUWk*%T*h$@t>sGBp%-{l!{N3iF6fQdY^7g&~3 z{F^2lluV^D{DN6ojVa-=K&l7uGd@Z*xXfFa1AJ1stjqxp&H~GFUFKz9WfH8}UqwOw zCmUSKEUC-xc`D_Q*b^0J(OY zU>Q$5FM@ zObSH(*F`W(0kD?@qPm@kbUom(O2BSe!368&AJQ>!T~~m0`$(OUwZDja{Rg!JH>40m zO8pcPUOWfP)99~10@4>(W)=&czd>^{Uwc#gf~VDJl@!D4TME$a%NXEtJ% z?wB_Fjrc_bfHf&5vViQef!|`mob*6uJR0%td`Tr4fkv~$V&Kx=@J3DWikqX#s4tj^ z$zTp@%Xw0fQ~~j8b#VFdL z3$#=fQL&9gf;N%n-KV6qm=!+7R=1Lu@C?O;ANK5-eXb z{4Wa*Fo--xyuf$afZ3wtuZZ9$NDE+_9MTcwDpn~LOa^WMzGNYIsh9BLWpD~}F(vmR ztki3q${5^~$I3C~x^f##YM zAXF#MVr7cpUCsk3-U4gh5Wi(6R1vmBsmg|i?l&4}5A3Kc)w$>)f8P9VG8fn0+7J?FpJMh|{-F{&Eu;1!zU z`7f5cWA=F%*w^deHs(rgVS~aXgJc!$uoJfsbL4|vdnlRkzJ_#9oF?895(P6qj_<}_ zLAJ9$Jp2T(pL@mX(oboTOn`TrjBH;p>_9ohC)dCi4@NZnk~&HEVamXo&S87AsbG*^ zAoK7FIM<8X3oJYk*l!s59#!@yshiX;*qX73`$A!#Gig7j1zx=ok--yYBGZ#;gUDeG zxQIzq7I}mmO}dH3#Bsd#*2ro0mLqU#_e&4(jMsuKNJk930WolSVSNh>~u&8#5BgbKX47kPp z$s%Gossj(=tp|~T4C!=uQv*1VFR-6x>JR0iGLhTPNAzAw#ezK;id@Gc)MOl{lj#y< zv?|hbFtKerl)b{h!v`YD*^XHC7&4#(Q9rjpnFY^ilg-FGb%PhFhn25~{O(-2rRLGm*Cn4{)0-3in$i(@I*{C-eFR;i3f8q!7p_qKx zAFmTA2$x{}f5F221#$?% zD;@^s`5^HQc1@499}4clL&k!S@COzf06$+3OvT;*n4xQUe+*~&J++=1Pff&R^D&e! z6^VVzK*eG>W|cpKg}a5QT#fS|L#{&p;*!!?DFzcz5nOIA{AH?S20Qf*7{M-$ko|$C z*27l}#|-Ls;0@;BycdA$iUIc*FR8HxpYZ+vAWu;!4+Q&Ii1>XZn5*5&DMX(V`0xkH zS0x3k{}<#*m%|nluynhye|N}RsD15%oi2h;KLD@a8myZNUgI$=v>H}hA)djftif&W1Iu;_Hh&MS`%=6$QuGQhzk&Dc z4PQA1-l7ZYhANN(xQw%?6R3+!+h%a3tCZjH(0Rz`On@zaf;@aIShqB?0oj4r538Lo z??be3Tv`HLbO+dDkNguDwxco^>*}K{kYnW}Sio7RNganO#Z2UzpYV;q*sl?%NrO?j zq9F$3#NNf7PY3SyB~HQvMM87U7ka#FJS7V76BSU4a}#~q z-7wv>KhRH-d<*%de)2*@M&;nYJn%(RVV(EEgYSSxI*9)_C+ZQ!u&CvfA7J`rxfZas zFS46Uz-+|;_od@~Gx+`)@OQyj*<#q<=lGk8iPOXu!b0>!_G>fw02Zhez9}fqotA+}DiQ|b9QN@cP-Y02rhTYM3J0grjogdV z_JVu}|CEeO=~l89@X0v%%}elthw#6fKvt;M+o?|I~|5s$^V#$iIKk={^dT5y( zfOY*u(5TctL#~E*+lG@FMd?s`ycAz&@f-EXM^*zvWFr`4ESKZ2dZ7j<3cU3+WRkM6 zE+fId|KC4W#yx(A4AV$tN3P>WFyva`i$TD2i8xEiKr^irEo@R#V2vHXXt_#>;s>U_ zEZ!~yZ+ydA%mP|#gPX+>SHYT4nUOhaE`O57BNja`u0*~t4H0HN z#53JR9a!8~LW+=v8uuJjmaP!>|5uG&guMEBoINXg3xb8l!hG2A>p~1H+%fREJ7D3e zq9dT97y~O-UMwqyii^dnz&RJC<-jI!$Y|F@l}swKB)5SFMxYir49w?Ms)U*Xj{O}n zyicI3Fckd$B>E2hiavnP)nk@0H<@be6?P(*hkAn`br1Cp^$qn9^$%4y)dn!W$%u02 zq2}%x`xLW!=dcSA^FCplu@Av_bOD2r4*sJT(DwpxIvr>oxTB>1$E#4LI7=9b6IjVC z+>hbN(A))6?}k8We3nmbEze&gr9&}-GV$>8=%xtz#>^dBbm}_ zc=bJ4J0rT%ijfTq!Yen5A<%XBfAsf8>;%7`2hV(2&Vc&F74WCiamNyY(_X@-v_-bg z3dH49ynwMs!vi{jSq{RpYzK2z2I{RQ)OCy#o1nI12zJIIlnN@a9}BT(OT~`(8?6yr z>&3CcQ+@%k0*5~LM9*;4IQ~ZU*8=c^;0944RSO*PLctDxei7o=*~mFO0%AA=B+*8W zL(Z}>G7k4pA(jK1-UGMfi}DXF?@F+9yOB5D4qjm@svldRcPyN1jmoeo+!^jU_kr6F zUSSogaO_+IutK@$NSKMoYPA-sA>A~I8d_shZ&l0CPl<*0(FQIH`M8V7$rNLEL3aksAKTxW?~Su$5l#iITIM~7*wA+LnmZAII$#Tkb0w^M&M6?|6*Yg z2Z`a5C?z9;ACH^h3oQNync+vMey9ekSRQ=xV|;!;?7}1buRh|zU9iskph&VGcXk7D znJ_^^VI4V>YzB|Fm1KbTnj!1c0DbfinKV@3I-qT}ndt~V>>ZtiisMW0r^}#j@`tKV zw@2-Qo}P!QgwB)<&vg^%JO-24qGT&P-*%`A8KpDgSybWF7e67>w?G&q?BNy99ZwsN z)xFX^7TV~fXRfCJJa2RUF#nL(2|a}{p^@-fFdz%u1KiISu^e(ETcv~21u#oZkoPZ< zyTNWBQ#L6ABGmV=uX*6ahr@TPfr<#kk-LC>8sG+XW-5TU{>%;so3H@Y;uId|=v}yF zO%a!NWR^2Wky*7ck7^_BH2646y_VgCm)%P`e1mTf^8j1sz@vB;1#!+(uOT}><09sCq-Av-)A^@{OeOuwKqhX7MB9aXo( zglVX^(F;GoY`)@ep}L|B80wSApN)bgsfk>W8XWck`37$09^{d`A=`nRsn}JR&6}a( zbsd$V9gq=C;fv8fLm;1&4O|?6>~?!Rn?hwUtmY3`_W|JTx&zB^0hjX|UUwI{2-%qk zR3l7ArOFOGW+Km0k!pqAs|j08Q9pn@Rza=hG3iEC?iaEr_=o?i*R({XrV7^80Uvjh z7>yb{3!?N3lt}djmfQdvGm7p;cL#@b;u0z{^M1#Qt z)Q28aLsT2Y(n0t)oGu1F{Y}YKQ}710k=s<$-+@+Yp+>qYZJ-HyGc^SJ@c%qrANZU8 zz%|?Os!o!BQBBqbmMoeqKsAg6PkRwQFiuGZ^Y{tuO(ZIXeUZ1D3lu#Y_NqWCMqd9R z;;Ie6XmzC=L=L0=;}_4NE+kd#BYluM0L!MKsvr=Vp;**3?j)e`0jAGJmO-{FmE_^E zAHt))MLgdKG4LCl!WF;@d-0bC0<93p1P_PS&rR$^Kk%YyawuvtSY(j80~wwN8_^dW zT74-I9SXmoeA5QKdCBOkmK zE5u#Jz24mhx-kP$`#uewz@x#6Iz=6RFNLW33L?K0DfYjNxFml>Kh6XA?J>ZDTd6km zcRCRf>{(Qz9s--@VUod;Re_)IV%njvZ$5Jj(QQq(Hts+xOn2(RzCfSFB&G@aIR`S; z88zzQis>u#80`2E_^I94!HcK}sDT?YAMr?6V7gGU65_3?*q>RjD8Yzt{fHxoBKm?o zDFMqRfU{*40d?I;$e=U=hj|KXQvunbvr+`Qol21%9|x{_464^&2wsR5hyRyR{tT2^ z1QnYZuyDO`Mw?;h>mlZTjk=1pKueoZ0pmoxun3671Iw@iS<~&PdQww%s9T8i(vk9_g~Nk&)MRB;Aa z^dg*%>4;tCA!pqU6}D;62EBzW=UZe#{S^0qm4zih^f^G7xu_c+hi6|?)*&Cc5;c1l zaDG?fCJjQ3)dbQ&@*HIa#2fD_pV^(*C2 zdEF4X%N1-z_BK4{G*s~(pp4+#PobW)0!~6S;ujriqILkE7Q!2h0}t?jg(I3I$>*@! zTX8lHAhPu(!jb2akv?qE+#D4KqSa&Nr*o9n%ccXo2#agdI~==bkye`znY+w5%}cN|rnd!1uk ze_eImzugTzHPB1o!bJJmP>*!E{qS*$X8|83=n$bVgw1#x)Q?LnP7}UZg2?h3ZC!q1$5$n}(iT0-3NysuReHJ%gs{3uL|$S(+^e{J9Xhv?joy z<56jL7-%$`No1R#cSOx*ql=^(dlDJV5NHCHL)Oio;c0JZ$E5=U&!H!r%=&mx_8vjikMpVZcuz;Ahh5d|D5y6oU$v3s6{`fU2ZTs4SjKv_gc_6%kAq z?0F$}%z_v?18g_}e&8m4*Ga@5qY)$ZL5|}IvVe=h3%FqwDpJq?%d4(HeEb2K_2Eiw zM0XE>SRuTCn-DAKgWX^-fuV(17d3?Qgg_w!l}UD9FC65XLe;B36xt^89(3GY;CF+` z`iM@kx2R|e;5+l(`B~_@sf)ZpGH~(~ta%l1bW@S5sR>^63F6LSc&wJ^%FpF#cUO8PYT_VF|eML#us@eSni zw$eRt?`+Up+)j-E=g}8-%nbxE72ed1DpWfejd;LKy+(JB4Y}spv>%-b^z2h`uh6`(ngSqL_d>z`d|1b-=;Cl4pPkS%n*{BYkkE zo1$8kLQdcw7_GBZJgQ*(0CAk5!MhbUhz^ zTrGeZo**|;p4^Y~`w2{TDM8}qB_p;@2P<(3IkY{fRO$rumIw|j1+{y@;BE_*gT!X6 zjUV+2j9w-p+U8(O)`JT<5;A4c_Ggz8ZpP`#s|B7UU~@$4As& zUzgjUe!49(%l-a4TW!El#Q-Ip29sETSTsjziM9S09ViB62zvN4q;%{<3~K9pz`D1= z3iU#!q#dybo_ip2DT&faL|n}=tD}P8D|8WZg}y*gH_&S-qe^9=R29B_GwMCm@@-_V zw@DJZvU0>i(O3El{yH93Z6;1cE_!huAUYT+*+foy1@ANjRk}BXi9pI#QN?b-jXH-~ z^UA0Qoh|+&-9{zmPQ=i?P~DdVTr~`Ng>=+P?gcj0pz^sY_=3yeQwm@wOgPo|(W|lp z*`H+#YY0fM3d@iZ2-_=oGR@&wwGX zVa5AlGF%&+lNvZ*8K{ADOKVVvO2ejJRyqQg{ja0*59;}jYkpsRSmn|vy`2{p`PO)%G`RK5!Q?XBdLeCQa-Fqbwu zNs9cCJ9%y>;UqqM-KvxwivQEhIb zb8$kpH4SG>Z}w&nX5RNu{RYzATS(v7@!<-_pZElHPH$Mtl*wI^IK9V{A#Ni9lB@0| z{c%jq;fDAYb%-;LyDGy;sl{k5aPt+MOVABR$0R0_sW2n!P=fu9INWo?PIt3_r3;udU;*Wt0PV;`HfoMxF7s5cPP0=F`s{h zdpnnIRLv|N3u3**YQ_;{1zG*fLYSih+!~|x@l3&Qv_7QPgY+eOjqXjYJp=F33x9}Z ze8d!y#94ouY!ZTi?Qyo#it)U?v@|E7lUU=Uy-`H8311pC%rK ztP#u?46^CQ^m>P|Cf$#D(%)lhrKo8K0ApDJT*&ok&XKJTRh!gLL z4rx7(3rVh`>tw&xN3!vzT4E30!c@Ex1EqSn>FeTA`p+4$Gg+}1k6jjymheUhW-?ub2dO(woooliz>4lb+L%f z`~!1|i$9ZvyRp>@wC0$8_+J#JId^n~KxThC%KZZ~hMIiSs<(Q>gw6mnv{?VKj})@B zkEtmt`b#>lMPE>xOih1b+W}sa`Frk((b5j~*A>!ozw*r2(_^xD@+P-3$AQmA(ED%j zt55NZt`P>>=Av$=p*$zS@?Ug~gMUth4;ag5ek6_tuOB0KcbUn4snm{MSPjaLB=0hT za}P3^^TOyQ3b%wUhhcq*UQ*BWy%kq)F)MaDK8g&^9XQ9#dw{P(@IaBVol4P&5_}x( zaxm3mF|MBvjCQ?=OyhahzCps|sgcR|xow)}aO?5gv|zBg9lhWuoSes*oFBnOHp4Rx zq0(l zm((b$$||=8x1pr(5^x6jxaYYI;?{DLTcLl4jXkJ*#@QCBu&pUd5x&UHq@mL}+uJ6M z62BJqz+^7r?s8A-0@HjJpKSndM&jwbg8i+h?ysZgckrZckvV(9zWtE>tD#kRaVB^s z8n&G-SLO0xE$!w{4&vtIBoJb(RSDPH5BHOVw${ZAgU682(`TWGU8PgklBjD#(Ql`^ z6!ZUn2g(^j()g)sB6zCPcW?*{c>t|wzt{%jQbFF%j!Shl81O1C!eGeS$(d!5`eZo3nVe=bT2Iw|^;4wEUT6X6I?FV>CV`+EwHlbfYcRL> zwJrKc_UALBr`by9Si^VR!`nVg%^L>3+6XTH##+I9zDw2pg_8;XuvKtZY6dmwoIF7( zz~4F%E+dRLl}|n=otp)smA=Zmc!xeGz5W7U;K$t893}4rC6}`|6L3~uhdIoW+NFhH zgbdt74fw^bDRq3e625Msl+Tum!DB0M!{m{2aJZgZUd)R9;WCoJe*d6LM%lc$@3#$Y z?+`rhGO{kOd0G*AqNY2qI8(UKFb8FOCVe&pPQB6yVW!@Iu9(99E&+Q*!WSek!H-0F zv%_uJaVj~5Noae6r7?IjPvE$9CxsJ1HLirwt+yrMy~!6tz`s4{csu3!@+!F*$JB9H z+;Y-BD@ifN!r`RJE9BYyypD4Xg1`<(lrm0mm_rINLb*dFSWR|m0iNaI!YbQR*9N$V zy*N!&cI!f8CQN%2dvhAd|3_4lWWyjivkh0Zub!m+<_zHWywmC{d~DO1Hbb0+q&ZGH z9a<>}p+y_4XEFE8!-1Qw2fzr2vRl*ePJ2;d=Q2UpigxLQ7%4skZ=FY*cnKS_3?4b( oJP1qQou}fBs?+RBV!kisRE_Vcre&K{VjNiKf5km%*#H0l literal 0 HcmV?d00001 diff --git a/PythonRpcServer/server.py b/PythonRpcServer/server.py index bb880957..09d20385 100644 --- a/PythonRpcServer/server.py +++ b/PythonRpcServer/server.py @@ -41,6 +41,9 @@ def LogWorker(logId, worker): class PythonServerServicer(ct_pb2_grpc.PythonServerServicer): + # Transcribe it into a json string from the transcribe text + # Make it returns a json string + # change name to TranscribeRPC def CaptionRPC(self, request, context): #See CaptionRequest print( f"CaptionRPC({request.logId};{request.refId};{request.filePath};{request.phraseHints};{request.courseHints};{request.outputLanguages})") diff --git a/PythonRpcServer/transcribe.py b/PythonRpcServer/transcribe.py new file mode 100644 index 00000000..3a024d3f --- /dev/null +++ b/PythonRpcServer/transcribe.py @@ -0,0 +1,57 @@ +import subprocess +import os +import json +import re + +def transcribe_audio_with_whisper(audio_file_path): + if not os.path.exists(audio_file_path): + raise FileNotFoundError(f"Audio file {audio_file_path} does not exist.") + + command = [ + "whisper", + audio_file_path, + "--model", "base.en", + "--output_format", "json" + ] + + try: + result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + + print("Whisper Output:") + print(result.stdout) + + formatted_data = {"en": []} + + segments = result.stdout.strip().split('\n\n') + for segment in segments: + match = re.search(r'\[(\d+:\d+\.\d+)\s+-->\s+(\d+:\d+\.\d+)\]\s+(.*)', segment) + if match: + start_time = match.group(1) + end_time = match.group(2) + text = match.group(3).strip() + + formatted_data["en"].append({ + "starttime": start_time, + "endtime": end_time, + "caption": text + }) + + return formatted_data + + except subprocess.CalledProcessError as e: + print(f"Error during transcription: {e.stderr}") + return None + + except Exception as e: + print(f"An unexpected error occurred: {e}") + return None + +if __name__ == "__main__": + audio_file = "randomvoice_16kHz.wav" + + transcription = transcribe_audio_with_whisper(audio_file) + + if transcription: + print(json.dumps(transcription, indent=4)) + else: + print("Transcription failed.") \ No newline at end of file diff --git a/randomvoice_16kHz.json b/randomvoice_16kHz.json new file mode 100644 index 00000000..c3053a9b --- /dev/null +++ b/randomvoice_16kHz.json @@ -0,0 +1 @@ +{"text": " Hello? Hello? Hello?", "segments": [{"id": 0, "seek": 0, "start": 0.0, "end": 3.0, "text": " Hello? Hello? Hello?", "tokens": [50363, 18435, 30, 18435, 30, 18435, 30, 50513], "temperature": 0.0, "avg_logprob": -0.636968559688992, "compression_ratio": 1.1764705882352942, "no_speech_prob": 0.22877301275730133}], "language": "en"} \ No newline at end of file diff --git a/whisper.cpp b/whisper.cpp new file mode 160000 index 00000000..5236f027 --- /dev/null +++ b/whisper.cpp @@ -0,0 +1 @@ +Subproject commit 5236f0278420ab776d1787c4330678d80219b4b6 From 5460778e23ca2a2ce465b5c8c0982fd1ad17abae Mon Sep 17 00:00:00 2001 From: SaltyFish0308 <126301143+SaltyFish0308@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:14:25 -0500 Subject: [PATCH 07/23] Add json format file --- recording0.wav.json | 1077 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1077 insertions(+) create mode 100644 recording0.wav.json diff --git a/recording0.wav.json b/recording0.wav.json new file mode 100644 index 00000000..5f673c47 --- /dev/null +++ b/recording0.wav.json @@ -0,0 +1,1077 @@ +{ + "systeminfo": "AVX = 0 | AVX2 = 0 | AVX512 = 0 | FMA = 0 | NEON = 1 | ARM_FMA = 1 | METAL = 1 | F16C = 0 | FP16_VA = 1 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 0 | SSSE3 = 0 | VSX = 0 | CUDA = 0 | COREML = 0 | OPENVINO = 0 | CANN = 0", + "model": { + "type": "base", + "multilingual": false, + "vocab": 51864, + "audio": { + "ctx": 1500, + "state": 512, + "head": 8, + "layer": 6 + }, + "text": { + "ctx": 448, + "state": 512, + "head": 8, + "layer": 6 + }, + "mels": 80, + "ftype": 1 + }, + "params": { + "model": "models/ggml-base.en.bin", + "language": "en", + "translate": false + }, + "result": { + "language": "en" + }, + "transcription": [ + { + "timestamps": { + "from": "00:00:00,000", + "to": "00:00:07,320" + }, + "offsets": { + "from": 0, + "to": 7320 + }, + "text": " Reading homeworks are due early Tuesday at 9pm unless announced otherwise.", + "tokens": [ + { + "text": "[_BEG_]", + "timestamps": { + "from": "00:00:00,000", + "to": "00:00:00,000" + }, + "offsets": { + "from": 0, + "to": 0 + }, + "id": 50363, + "p": 0.848947, + "t_dtw": -1 + }, + { + "text": " Reading", + "timestamps": { + "from": "00:00:00,000", + "to": "00:00:00,750" + }, + "offsets": { + "from": 0, + "to": 750 + }, + "id": 11725, + "p": 0.407652, + "t_dtw": -1 + }, + { + "text": " hom", + "timestamps": { + "from": "00:00:00,750", + "to": "00:00:01,070" + }, + "offsets": { + "from": 750, + "to": 1070 + }, + "id": 3488, + "p": 0.254302, + "t_dtw": -1 + }, + { + "text": "eworks", + "timestamps": { + "from": "00:00:01,080", + "to": "00:00:01,710" + }, + "offsets": { + "from": 1080, + "to": 1710 + }, + "id": 19653, + "p": 0.981512, + "t_dtw": -1 + }, + { + "text": " are", + "timestamps": { + "from": "00:00:01,710", + "to": "00:00:01,970" + }, + "offsets": { + "from": 1710, + "to": 1970 + }, + "id": 389, + "p": 0.708301, + "t_dtw": -1 + }, + { + "text": " due", + "timestamps": { + "from": "00:00:02,060", + "to": "00:00:02,350" + }, + "offsets": { + "from": 2060, + "to": 2350 + }, + "id": 2233, + "p": 0.54323, + "t_dtw": -1 + }, + { + "text": " early", + "timestamps": { + "from": "00:00:02,350", + "to": "00:00:02,880" + }, + "offsets": { + "from": 2350, + "to": 2880 + }, + "id": 1903, + "p": 0.315304, + "t_dtw": -1 + }, + { + "text": " Tuesday", + "timestamps": { + "from": "00:00:02,880", + "to": "00:00:03,630" + }, + "offsets": { + "from": 2880, + "to": 3630 + }, + "id": 3431, + "p": 0.614094, + "t_dtw": -1 + }, + { + "text": " at", + "timestamps": { + "from": "00:00:03,630", + "to": "00:00:03,840" + }, + "offsets": { + "from": 3630, + "to": 3840 + }, + "id": 379, + "p": 0.960146, + "t_dtw": -1 + }, + { + "text": " 9", + "timestamps": { + "from": "00:00:03,840", + "to": "00:00:04,150" + }, + "offsets": { + "from": 3840, + "to": 4150 + }, + "id": 860, + "p": 0.923494, + "t_dtw": -1 + }, + { + "text": "pm", + "timestamps": { + "from": "00:00:04,160", + "to": "00:00:04,370" + }, + "offsets": { + "from": 4160, + "to": 4370 + }, + "id": 4426, + "p": 0.398094, + "t_dtw": -1 + }, + { + "text": " unless", + "timestamps": { + "from": "00:00:04,370", + "to": "00:00:05,010" + }, + "offsets": { + "from": 4370, + "to": 5010 + }, + "id": 4556, + "p": 0.787165, + "t_dtw": -1 + }, + { + "text": " announced", + "timestamps": { + "from": "00:00:05,010", + "to": "00:00:05,970" + }, + "offsets": { + "from": 5010, + "to": 5970 + }, + "id": 3414, + "p": 0.980173, + "t_dtw": -1 + }, + { + "text": " otherwise", + "timestamps": { + "from": "00:00:05,970", + "to": "00:00:07,000" + }, + "offsets": { + "from": 5970, + "to": 7000 + }, + "id": 4306, + "p": 0.942659, + "t_dtw": -1 + }, + { + "text": ".", + "timestamps": { + "from": "00:00:07,000", + "to": "00:00:07,320" + }, + "offsets": { + "from": 7000, + "to": 7320 + }, + "id": 13, + "p": 0.86153, + "t_dtw": -1 + }, + { + "text": "[_TT_366]", + "timestamps": { + "from": "00:00:07,320", + "to": "00:00:07,320" + }, + "offsets": { + "from": 7320, + "to": 7320 + }, + "id": 50729, + "p": 0.0230646, + "t_dtw": -1 + } + ] + }, + { + "timestamps": { + "from": "00:00:07,320", + "to": "00:00:12,600" + }, + "offsets": { + "from": 7320, + "to": 12600 + }, + "text": " We post each week's homework at least one full week before it's due date.", + "tokens": [ + { + "text": " We", + "timestamps": { + "from": "00:00:07,320", + "to": "00:00:07,490" + }, + "offsets": { + "from": 7320, + "to": 7490 + }, + "id": 775, + "p": 0.961556, + "t_dtw": -1 + }, + { + "text": " post", + "timestamps": { + "from": "00:00:07,490", + "to": "00:00:07,840" + }, + "offsets": { + "from": 7490, + "to": 7840 + }, + "id": 1281, + "p": 0.940814, + "t_dtw": -1 + }, + { + "text": " each", + "timestamps": { + "from": "00:00:07,840", + "to": "00:00:08,190" + }, + "offsets": { + "from": 7840, + "to": 8190 + }, + "id": 1123, + "p": 0.990758, + "t_dtw": -1 + }, + { + "text": " week", + "timestamps": { + "from": "00:00:08,190", + "to": "00:00:08,540" + }, + "offsets": { + "from": 8190, + "to": 8540 + }, + "id": 1285, + "p": 0.98414, + "t_dtw": -1 + }, + { + "text": "'s", + "timestamps": { + "from": "00:00:08,540", + "to": "00:00:08,710" + }, + "offsets": { + "from": 8540, + "to": 8710 + }, + "id": 338, + "p": 0.852609, + "t_dtw": -1 + }, + { + "text": " homework", + "timestamps": { + "from": "00:00:08,710", + "to": "00:00:09,420" + }, + "offsets": { + "from": 8710, + "to": 9420 + }, + "id": 26131, + "p": 0.9842, + "t_dtw": -1 + }, + { + "text": " at", + "timestamps": { + "from": "00:00:09,420", + "to": "00:00:09,590" + }, + "offsets": { + "from": 9420, + "to": 9590 + }, + "id": 379, + "p": 0.989681, + "t_dtw": -1 + }, + { + "text": " least", + "timestamps": { + "from": "00:00:09,590", + "to": "00:00:10,020" + }, + "offsets": { + "from": 9590, + "to": 10020 + }, + "id": 1551, + "p": 0.992646, + "t_dtw": -1 + }, + { + "text": " one", + "timestamps": { + "from": "00:00:10,030", + "to": "00:00:10,290" + }, + "offsets": { + "from": 10030, + "to": 10290 + }, + "id": 530, + "p": 0.768113, + "t_dtw": -1 + }, + { + "text": " full", + "timestamps": { + "from": "00:00:10,290", + "to": "00:00:10,640" + }, + "offsets": { + "from": 10290, + "to": 10640 + }, + "id": 1336, + "p": 0.589241, + "t_dtw": -1 + }, + { + "text": " week", + "timestamps": { + "from": "00:00:10,640", + "to": "00:00:10,990" + }, + "offsets": { + "from": 10640, + "to": 10990 + }, + "id": 1285, + "p": 0.992968, + "t_dtw": -1 + }, + { + "text": " before", + "timestamps": { + "from": "00:00:10,990", + "to": "00:00:11,510" + }, + "offsets": { + "from": 10990, + "to": 11510 + }, + "id": 878, + "p": 0.988001, + "t_dtw": -1 + }, + { + "text": " it", + "timestamps": { + "from": "00:00:11,530", + "to": "00:00:11,690" + }, + "offsets": { + "from": 11530, + "to": 11690 + }, + "id": 340, + "p": 0.670674, + "t_dtw": -1 + }, + { + "text": "'s", + "timestamps": { + "from": "00:00:11,690", + "to": "00:00:11,850" + }, + "offsets": { + "from": 11690, + "to": 11850 + }, + "id": 338, + "p": 0.866573, + "t_dtw": -1 + }, + { + "text": " due", + "timestamps": { + "from": "00:00:11,860", + "to": "00:00:12,120" + }, + "offsets": { + "from": 11860, + "to": 12120 + }, + "id": 2233, + "p": 0.991537, + "t_dtw": -1 + }, + { + "text": " date", + "timestamps": { + "from": "00:00:12,120", + "to": "00:00:12,600" + }, + "offsets": { + "from": 12120, + "to": 12600 + }, + "id": 3128, + "p": 0.652784, + "t_dtw": -1 + }, + { + "text": ".", + "timestamps": { + "from": "00:00:12,600", + "to": "00:00:12,600" + }, + "offsets": { + "from": 12600, + "to": 12600 + }, + "id": 13, + "p": 0.929698, + "t_dtw": -1 + }, + { + "text": "[_TT_630]", + "timestamps": { + "from": "00:00:12,600", + "to": "00:00:12,600" + }, + "offsets": { + "from": 12600, + "to": 12600 + }, + "id": 50993, + "p": 0.0345322, + "t_dtw": -1 + } + ] + }, + { + "timestamps": { + "from": "00:00:12,600", + "to": "00:00:17,280" + }, + "offsets": { + "from": 12600, + "to": 17280 + }, + "text": " We post solutions almost a day after extended due date.", + "tokens": [ + { + "text": " We", + "timestamps": { + "from": "00:00:12,600", + "to": "00:00:12,790" + }, + "offsets": { + "from": 12600, + "to": 12790 + }, + "id": 775, + "p": 0.99553, + "t_dtw": -1 + }, + { + "text": " post", + "timestamps": { + "from": "00:00:12,790", + "to": "00:00:13,180" + }, + "offsets": { + "from": 12790, + "to": 13180 + }, + "id": 1281, + "p": 0.991259, + "t_dtw": -1 + }, + { + "text": " solutions", + "timestamps": { + "from": "00:00:13,180", + "to": "00:00:14,060" + }, + "offsets": { + "from": 13180, + "to": 14060 + }, + "id": 8136, + "p": 0.965852, + "t_dtw": -1 + }, + { + "text": " almost", + "timestamps": { + "from": "00:00:14,060", + "to": "00:00:14,650" + }, + "offsets": { + "from": 14060, + "to": 14650 + }, + "id": 2048, + "p": 0.395074, + "t_dtw": -1 + }, + { + "text": " a", + "timestamps": { + "from": "00:00:14,650", + "to": "00:00:14,740" + }, + "offsets": { + "from": 14650, + "to": 14740 + }, + "id": 257, + "p": 0.984885, + "t_dtw": -1 + }, + { + "text": " day", + "timestamps": { + "from": "00:00:14,740", + "to": "00:00:15,030" + }, + "offsets": { + "from": 14740, + "to": 15030 + }, + "id": 1110, + "p": 0.997579, + "t_dtw": -1 + }, + { + "text": " after", + "timestamps": { + "from": "00:00:15,030", + "to": "00:00:15,520" + }, + "offsets": { + "from": 15030, + "to": 15520 + }, + "id": 706, + "p": 0.997904, + "t_dtw": -1 + }, + { + "text": " extended", + "timestamps": { + "from": "00:00:15,520", + "to": "00:00:16,300" + }, + "offsets": { + "from": 15520, + "to": 16300 + }, + "id": 7083, + "p": 0.59029, + "t_dtw": -1 + }, + { + "text": " due", + "timestamps": { + "from": "00:00:16,300", + "to": "00:00:16,590" + }, + "offsets": { + "from": 16300, + "to": 16590 + }, + "id": 2233, + "p": 0.986455, + "t_dtw": -1 + }, + { + "text": " date", + "timestamps": { + "from": "00:00:16,590", + "to": "00:00:17,040" + }, + "offsets": { + "from": 16590, + "to": 17040 + }, + "id": 3128, + "p": 0.994649, + "t_dtw": -1 + }, + { + "text": ".", + "timestamps": { + "from": "00:00:17,040", + "to": "00:00:17,280" + }, + "offsets": { + "from": 17040, + "to": 17280 + }, + "id": 13, + "p": 0.967296, + "t_dtw": -1 + }, + { + "text": "[_TT_864]", + "timestamps": { + "from": "00:00:17,280", + "to": "00:00:17,280" + }, + "offsets": { + "from": 17280, + "to": 17280 + }, + "id": 51227, + "p": 0.0279233, + "t_dtw": -1 + } + ] + }, + { + "timestamps": { + "from": "00:00:17,280", + "to": "00:00:21,120" + }, + "offsets": { + "from": 17280, + "to": 21120 + }, + "text": " Links to future homeworks and solutions are placeholders.", + "tokens": [ + { + "text": " Links", + "timestamps": { + "from": "00:00:17,280", + "to": "00:00:17,640" + }, + "offsets": { + "from": 17280, + "to": 17640 + }, + "id": 21691, + "p": 0.970288, + "t_dtw": -1 + }, + { + "text": " to", + "timestamps": { + "from": "00:00:17,640", + "to": "00:00:17,780" + }, + "offsets": { + "from": 17640, + "to": 17780 + }, + "id": 284, + "p": 0.997626, + "t_dtw": -1 + }, + { + "text": " future", + "timestamps": { + "from": "00:00:17,780", + "to": "00:00:18,220" + }, + "offsets": { + "from": 17780, + "to": 18220 + }, + "id": 2003, + "p": 0.99132, + "t_dtw": -1 + }, + { + "text": " hom", + "timestamps": { + "from": "00:00:18,220", + "to": "00:00:18,440" + }, + "offsets": { + "from": 18220, + "to": 18440 + }, + "id": 3488, + "p": 0.86544, + "t_dtw": -1 + }, + { + "text": "eworks", + "timestamps": { + "from": "00:00:18,440", + "to": "00:00:18,880" + }, + "offsets": { + "from": 18440, + "to": 18880 + }, + "id": 19653, + "p": 0.993508, + "t_dtw": -1 + }, + { + "text": " and", + "timestamps": { + "from": "00:00:18,880", + "to": "00:00:19,100" + }, + "offsets": { + "from": 18880, + "to": 19100 + }, + "id": 290, + "p": 0.988634, + "t_dtw": -1 + }, + { + "text": " solutions", + "timestamps": { + "from": "00:00:19,100", + "to": "00:00:19,760" + }, + "offsets": { + "from": 19100, + "to": 19760 + }, + "id": 8136, + "p": 0.978441, + "t_dtw": -1 + }, + { + "text": " are", + "timestamps": { + "from": "00:00:19,760", + "to": "00:00:19,980" + }, + "offsets": { + "from": 19760, + "to": 19980 + }, + "id": 389, + "p": 0.998055, + "t_dtw": -1 + }, + { + "text": " place", + "timestamps": { + "from": "00:00:19,980", + "to": "00:00:20,340" + }, + "offsets": { + "from": 19980, + "to": 20340 + }, + "id": 1295, + "p": 0.853392, + "t_dtw": -1 + }, + { + "text": "holders", + "timestamps": { + "from": "00:00:20,340", + "to": "00:00:20,850" + }, + "offsets": { + "from": 20340, + "to": 20850 + }, + "id": 10476, + "p": 0.982725, + "t_dtw": -1 + }, + { + "text": ".", + "timestamps": { + "from": "00:00:20,850", + "to": "00:00:21,120" + }, + "offsets": { + "from": 20850, + "to": 21120 + }, + "id": 13, + "p": 0.942825, + "t_dtw": -1 + }, + { + "text": "[_TT_1056]", + "timestamps": { + "from": "00:00:21,120", + "to": "00:00:21,120" + }, + "offsets": { + "from": 21120, + "to": 21120 + }, + "id": 51419, + "p": 0.0691018, + "t_dtw": -1 + } + ] + }, + { + "timestamps": { + "from": "00:00:21,120", + "to": "00:00:24,520" + }, + "offsets": { + "from": 21120, + "to": 24520 + }, + "text": " Tucks of future homeworks are subject to change.", + "tokens": [ + { + "text": " T", + "timestamps": { + "from": "00:00:21,120", + "to": "00:00:21,200" + }, + "offsets": { + "from": 21120, + "to": 21200 + }, + "id": 309, + "p": 0.433385, + "t_dtw": -1 + }, + { + "text": "ucks", + "timestamps": { + "from": "00:00:21,200", + "to": "00:00:21,530" + }, + "offsets": { + "from": 21200, + "to": 21530 + }, + "id": 6238, + "p": 0.424658, + "t_dtw": -1 + }, + { + "text": " of", + "timestamps": { + "from": "00:00:21,530", + "to": "00:00:21,690" + }, + "offsets": { + "from": 21530, + "to": 21690 + }, + "id": 286, + "p": 0.985274, + "t_dtw": -1 + }, + { + "text": " future", + "timestamps": { + "from": "00:00:21,690", + "to": "00:00:22,180" + }, + "offsets": { + "from": 21690, + "to": 22180 + }, + "id": 2003, + "p": 0.997737, + "t_dtw": -1 + }, + { + "text": " hom", + "timestamps": { + "from": "00:00:22,180", + "to": "00:00:22,420" + }, + "offsets": { + "from": 22180, + "to": 22420 + }, + "id": 3488, + "p": 0.975576, + "t_dtw": -1 + }, + { + "text": "eworks", + "timestamps": { + "from": "00:00:22,420", + "to": "00:00:22,910" + }, + "offsets": { + "from": 22420, + "to": 22910 + }, + "id": 19653, + "p": 0.993154, + "t_dtw": -1 + }, + { + "text": " are", + "timestamps": { + "from": "00:00:22,910", + "to": "00:00:23,150" + }, + "offsets": { + "from": 22910, + "to": 23150 + }, + "id": 389, + "p": 0.998262, + "t_dtw": -1 + }, + { + "text": " subject", + "timestamps": { + "from": "00:00:23,150", + "to": "00:00:23,730" + }, + "offsets": { + "from": 23150, + "to": 23730 + }, + "id": 2426, + "p": 0.991766, + "t_dtw": -1 + }, + { + "text": " to", + "timestamps": { + "from": "00:00:23,730", + "to": "00:00:23,890" + }, + "offsets": { + "from": 23730, + "to": 23890 + }, + "id": 284, + "p": 0.908062, + "t_dtw": -1 + }, + { + "text": " change", + "timestamps": { + "from": "00:00:23,890", + "to": "00:00:24,440" + }, + "offsets": { + "from": 23890, + "to": 24440 + }, + "id": 1487, + "p": 0.997778, + "t_dtw": -1 + }, + { + "text": ".", + "timestamps": { + "from": "00:00:24,440", + "to": "00:00:24,520" + }, + "offsets": { + "from": 24440, + "to": 24520 + }, + "id": 13, + "p": 0.981104, + "t_dtw": -1 + }, + { + "text": "[_TT_1226]", + "timestamps": { + "from": "00:00:24,520", + "to": "00:00:24,520" + }, + "offsets": { + "from": 24520, + "to": 24520 + }, + "id": 51589, + "p": 0.0327583, + "t_dtw": -1 + } + ] + } + ] +} From c95a0a4bc3413329c4c626621ca9faa434c60c2e Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 25 Sep 2024 15:16:56 -0500 Subject: [PATCH 08/23] Update with Whisper models --- pythonrpcserver.Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pythonrpcserver.Dockerfile b/pythonrpcserver.Dockerfile index 8824aee8..9b60a37a 100644 --- a/pythonrpcserver.Dockerfile +++ b/pythonrpcserver.Dockerfile @@ -8,6 +8,8 @@ WORKDIR /whisper.cpp RUN git clone https://github.com/ggerganov/whisper.cpp . && make RUN bash ./models/download-ggml-model.sh base.en + RUN bash ./models/download-ggml-model.sh tiny.en + RUN bash ./models/download-ggml-model.sh large-v3 # ------------------------------ # Stage 2: Setup Python RPC Server @@ -18,7 +20,7 @@ ENV OMP_THREAD_LIMIT=1 COPY --from=whisperbuild /whisper.cpp/main /usr/local/bin/whisper - COPY --from=whisperbuild /whisper.cpp/models/ggml-base.en.bin /usr/local/bin/models/ggml-base.en.bin + COPY --from=whisperbuild /whisper.cpp/models /PythonRpcServer/models WORKDIR /PythonRpcServer COPY ./PythonRpcServer/requirements.txt requirements.txt From 2843d78537dceb64bf0c6136b85cbb9068a49ec0 Mon Sep 17 00:00:00 2001 From: SaltyFish0308 <126301143+SaltyFish0308@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:24:54 -0500 Subject: [PATCH 09/23] Update transcribe.py --- PythonRpcServer/transcribe.py | 92 +++++++++++++++++------------------ 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/PythonRpcServer/transcribe.py b/PythonRpcServer/transcribe.py index 3a024d3f..2db1bbed 100644 --- a/PythonRpcServer/transcribe.py +++ b/PythonRpcServer/transcribe.py @@ -1,57 +1,55 @@ -import subprocess import os +import subprocess import json -import re -def transcribe_audio_with_whisper(audio_file_path): - if not os.path.exists(audio_file_path): - raise FileNotFoundError(f"Audio file {audio_file_path} does not exist.") +# Path to the Whisper executable inside the container +WHISPER_EXECUTABLE = './main' # Executable 'main' is assumed to be in the same directory as this script + +def transcribe_audio(media_filepath): + # Ensure the media file exists + if not os.path.exists(media_filepath): + raise FileNotFoundError(f"Media file not found: {media_filepath}") + + # Path to the output JSON file that Whisper will generate + json_output_path = f"{media_filepath}.json" - command = [ - "whisper", - audio_file_path, - "--model", "base.en", - "--output_format", "json" + # Command to run Whisper.cpp inside the container using the main executable + whisper_command = [ + WHISPER_EXECUTABLE, # Path to Whisper executable + '-ojf', # Output as JSON file + '-f', media_filepath # Media file path ] - try: - result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) - - print("Whisper Output:") - print(result.stdout) - - formatted_data = {"en": []} - - segments = result.stdout.strip().split('\n\n') - for segment in segments: - match = re.search(r'\[(\d+:\d+\.\d+)\s+-->\s+(\d+:\d+\.\d+)\]\s+(.*)', segment) - if match: - start_time = match.group(1) - end_time = match.group(2) - text = match.group(3).strip() - - formatted_data["en"].append({ - "starttime": start_time, - "endtime": end_time, - "caption": text - }) - - return formatted_data - - except subprocess.CalledProcessError as e: - print(f"Error during transcription: {e.stderr}") - return None + print("Running Whisper transcription inside the container...") - except Exception as e: - print(f"An unexpected error occurred: {e}") - return None + # Execute the Whisper command + result = subprocess.run(whisper_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) -if __name__ == "__main__": - audio_file = "randomvoice_16kHz.wav" + # Handle command failure + if result.returncode != 0: + raise Exception(f"Whisper failed with error:\n{result.stderr.decode('utf-8')}") - transcription = transcribe_audio_with_whisper(audio_file) + # Check if the output JSON file was generated + if not os.path.exists(json_output_path): + raise FileNotFoundError(f"Expected JSON output file not found: {json_output_path}") - if transcription: - print(json.dumps(transcription, indent=4)) - else: - print("Transcription failed.") \ No newline at end of file + # Load the JSON transcription result + with open(json_output_path, 'r') as json_file: + transcription_result = json.load(json_file) + + # Delete the JSON file after reading it + os.remove(json_output_path) + print(f"Deleted the JSON file: {json_output_path}") + + return transcription_result + +# Example usage +if __name__ == '__main__': + # Example media file path inside the container (the actual path will depend on where the file is located) + audio_filepath = 'sharedVolume/recording0.wav' # Update this path as needed + + try: + transcription_result = transcribe_audio(audio_filepath) + print("Transcription Result:", json.dumps(transcription_result, indent=4)) + except Exception as e: + print(f"Error: {str(e)}") From 3555c2907c6535416f76c060ecc13a9ecc63c7e4 Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 25 Sep 2024 15:28:14 -0500 Subject: [PATCH 10/23] Parse start end timestamp --- TaskEngine/Tasks/LocalTranscriptionTask.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/TaskEngine/Tasks/LocalTranscriptionTask.cs b/TaskEngine/Tasks/LocalTranscriptionTask.cs index 0352ce3c..9faee83c 100644 --- a/TaskEngine/Tasks/LocalTranscriptionTask.cs +++ b/TaskEngine/Tasks/LocalTranscriptionTask.cs @@ -127,12 +127,12 @@ protected async override Task OnConsume(string videoId, TaskParameters taskParam var theCaptions = new List(); int cueCount = 0; - foreach (var jsonCue in theCaptionsAsJson) { + foreach (var jsonCue in theCaptionsAsJson) { var caption = new Caption() { Index = cueCount ++, - Begin = TimeSpan.Parse(jsonCue["timestamps"]["from"].ToString(Newtonsoft.Json.Formatting.None)), - End = TimeSpan.Parse(jsonCue["timestamps"]["to"].ToString(Newtonsoft.Json.Formatting.None)) , - Text = jsonCue["text"] .ToString(Newtonsoft.Json.Formatting.None) + Begin = TimeSpan.Parse(jsonCue["timestamps"]["from"].ToString(Newtonsoft.Json.Formatting.None).replace(",",".")), + End = TimeSpan.Parse(jsonCue["timestamps"]["to"].ToString(Newtonsoft.Json.Formatting.None).replace(",",".")) , + Text = jsonCue["text"] .ToString(Newtonsoft.Json.Formatting.None).Trim() }; theCaptions.Add(caption); From 831caf7786f86a4986965410b21aa270af2e3135 Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 25 Sep 2024 15:49:46 -0500 Subject: [PATCH 11/23] Update Python transcribe --- PythonRpcServer/randomvoice_16kHz.json | 1 - PythonRpcServer/transcribe.py | 17 ++++++++++++++--- .../transcribe_example_result.json | 0 ...6kHz.wav => transcribe_hellohellohello.wav} | Bin 4 files changed, 14 insertions(+), 4 deletions(-) delete mode 100644 PythonRpcServer/randomvoice_16kHz.json rename recording0.wav.json => PythonRpcServer/transcribe_example_result.json (100%) rename PythonRpcServer/{randomvoice_16kHz.wav => transcribe_hellohellohello.wav} (100%) diff --git a/PythonRpcServer/randomvoice_16kHz.json b/PythonRpcServer/randomvoice_16kHz.json deleted file mode 100644 index c3053a9b..00000000 --- a/PythonRpcServer/randomvoice_16kHz.json +++ /dev/null @@ -1 +0,0 @@ -{"text": " Hello? Hello? Hello?", "segments": [{"id": 0, "seek": 0, "start": 0.0, "end": 3.0, "text": " Hello? Hello? Hello?", "tokens": [50363, 18435, 30, 18435, 30, 18435, 30, 50513], "temperature": 0.0, "avg_logprob": -0.636968559688992, "compression_ratio": 1.1764705882352942, "no_speech_prob": 0.22877301275730133}], "language": "en"} \ No newline at end of file diff --git a/PythonRpcServer/transcribe.py b/PythonRpcServer/transcribe.py index 2db1bbed..8f6db257 100644 --- a/PythonRpcServer/transcribe.py +++ b/PythonRpcServer/transcribe.py @@ -3,21 +3,32 @@ import json # Path to the Whisper executable inside the container -WHISPER_EXECUTABLE = './main' # Executable 'main' is assumed to be in the same directory as this script +WHISPER_EXECUTABLE = os.environ.get('WHISPER_EXE','whisper') # Executable 'main' is assumed to be in the same directory as this script +MODEL = os.environ.get('WHISPER_MODEL','models/ggml-base.en.bin') def transcribe_audio(media_filepath): + + if media_filepath == 'EXAMPLE_TRANSCRIBE_EXAMPLE_RESULT': + result_json_file = 'transcribe_example_result.json' + with open(result_json_file, 'r') as json_file: + transcription_result = json.load(json_file) + return transcription_result + # Ensure the media file exists if not os.path.exists(media_filepath): raise FileNotFoundError(f"Media file not found: {media_filepath}") # Path to the output JSON file that Whisper will generate json_output_path = f"{media_filepath}.json" - + if os.path.exists(media_filepath): + os.remove(json_output_path) + # Command to run Whisper.cpp inside the container using the main executable whisper_command = [ WHISPER_EXECUTABLE, # Path to Whisper executable '-ojf', # Output as JSON file - '-f', media_filepath # Media file path + '-f', media_filepath, # Media file path + '-m', MODEL ] print("Running Whisper transcription inside the container...") diff --git a/recording0.wav.json b/PythonRpcServer/transcribe_example_result.json similarity index 100% rename from recording0.wav.json rename to PythonRpcServer/transcribe_example_result.json diff --git a/PythonRpcServer/randomvoice_16kHz.wav b/PythonRpcServer/transcribe_hellohellohello.wav similarity index 100% rename from PythonRpcServer/randomvoice_16kHz.wav rename to PythonRpcServer/transcribe_hellohellohello.wav From f6048ac466649df82fe41f3b31abc8f01f4d6305 Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 25 Sep 2024 15:52:37 -0500 Subject: [PATCH 12/23] Update transcribe_example_result string --- PythonRpcServer/transcribe.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/PythonRpcServer/transcribe.py b/PythonRpcServer/transcribe.py index 8f6db257..bc256e6f 100644 --- a/PythonRpcServer/transcribe.py +++ b/PythonRpcServer/transcribe.py @@ -8,7 +8,7 @@ def transcribe_audio(media_filepath): - if media_filepath == 'EXAMPLE_TRANSCRIBE_EXAMPLE_RESULT': + if media_filepath == 'TEST-transcribe_example_result': result_json_file = 'transcribe_example_result.json' with open(result_json_file, 'r') as json_file: transcription_result = json.load(json_file) @@ -57,7 +57,11 @@ def transcribe_audio(media_filepath): # Example usage if __name__ == '__main__': # Example media file path inside the container (the actual path will depend on where the file is located) - audio_filepath = 'sharedVolume/recording0.wav' # Update this path as needed + import sys + if len(sys.argv) > 1: + audio_filepath = sys.argv[1] + else: + audio_filepath = 'sharedVolume/recording0.wav' # Update this path as needed try: transcription_result = transcribe_audio(audio_filepath) From cc5dd9d16affcf53c3d550ee56040f7af253b271 Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 25 Sep 2024 15:59:24 -0500 Subject: [PATCH 13/23] Fix C# compile method name typo --- TaskEngine/Tasks/LocalTranscriptionTask.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TaskEngine/Tasks/LocalTranscriptionTask.cs b/TaskEngine/Tasks/LocalTranscriptionTask.cs index 9faee83c..34eab4b8 100644 --- a/TaskEngine/Tasks/LocalTranscriptionTask.cs +++ b/TaskEngine/Tasks/LocalTranscriptionTask.cs @@ -130,8 +130,8 @@ protected async override Task OnConsume(string videoId, TaskParameters taskParam foreach (var jsonCue in theCaptionsAsJson) { var caption = new Caption() { Index = cueCount ++, - Begin = TimeSpan.Parse(jsonCue["timestamps"]["from"].ToString(Newtonsoft.Json.Formatting.None).replace(",",".")), - End = TimeSpan.Parse(jsonCue["timestamps"]["to"].ToString(Newtonsoft.Json.Formatting.None).replace(",",".")) , + Begin = TimeSpan.Parse(jsonCue["timestamps"]["from"].ToString(Newtonsoft.Json.Formatting.None).Replace(",",".")), + End = TimeSpan.Parse(jsonCue["timestamps"]["to"].ToString(Newtonsoft.Json.Formatting.None).Replace(",",".")) , Text = jsonCue["text"] .ToString(Newtonsoft.Json.Formatting.None).Trim() }; From 907c891710e6314866a284073a39ce951e8dd6f2 Mon Sep 17 00:00:00 2001 From: Tyler Liu Date: Wed, 2 Oct 2024 17:18:45 -0500 Subject: [PATCH 14/23] added transcribe functionality in server.py and Program test instance for it --- PythonRpcServer/server.py | 32 ++++++++++++++++++++++++++------ PythonRpcServer/transcribe.py | 4 ++-- TaskEngine/Program.cs | 24 ++++++++++++++++++++---- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/PythonRpcServer/server.py b/PythonRpcServer/server.py index 09d20385..44d236cd 100644 --- a/PythonRpcServer/server.py +++ b/PythonRpcServer/server.py @@ -12,6 +12,9 @@ from echo import EchoProvider from kaltura import KalturaProvider from mediaprovider import InvalidPlaylistInfoException +from transcribe import transcribe_audio + +import json import hasher import ffmpeg # import phrasehinter @@ -44,12 +47,12 @@ class PythonServerServicer(ct_pb2_grpc.PythonServerServicer): # Transcribe it into a json string from the transcribe text # Make it returns a json string # change name to TranscribeRPC - def CaptionRPC(self, request, context): - #See CaptionRequest - print( f"CaptionRPC({request.logId};{request.refId};{request.filePath};{request.phraseHints};{request.courseHints};{request.outputLanguages})") - kalturaprovider = KalturaProvider() - result = LogWorker(f"CaptionRPC({request.filePath})", lambda: kalturaprovider.getCaptions(request.refId)) - return ct_pb2.JsonString(json = result) + # def CaptionRPC(self, request, context): + # #See CaptionRequest + # print( f"CaptionRPC({request.logId};{request.refId};{request.filePath};{request.phraseHints};{request.courseHints};{request.outputLanguages})") + # kalturaprovider = KalturaProvider() + # result = LogWorker(f"CaptionRPC({request.filePath})", lambda: kalturaprovider.getCaptions(request.refId)) + # return ct_pb2.JsonString(json = result) @@ -125,6 +128,23 @@ def ComputeFileHash(self, request, context): def GetMediaInfoRPC(self, request, context): result = LogWorker(f"GetMediaInfo({request.filePath})", lambda: ffmpeg.getMediaInfo(request.filePath)) return ct_pb2.JsonString(json = result) + + + def TranscribeAudioRPC(self, request, context): + print(f"TranscribeAudioRPC({request.logId};{request.filePath})") + try: + logging.info(f"Starting transcription for file: {request.filePath}") + transcription_result = LogWorker( + f"TranscribeAudioRPC({request.filePath})", + lambda: transcribe_audio(request.filePath) + ) + logging.info(f"Transcription completed successfully for: {request.filePath}") + return ct_pb2.JsonString(json=json.dumps(transcription_result)) + + except Exception as e: + context.set_code(grpc.StatusCode.INTERNAL) + context.set_details(f"Transcription failed: {str(e)}") + return ct_pb2.JsonString(json=json.dumps({"error": str(e)})) def serve(): print("Python RPC Server Starting") diff --git a/PythonRpcServer/transcribe.py b/PythonRpcServer/transcribe.py index bc256e6f..ab5d1504 100644 --- a/PythonRpcServer/transcribe.py +++ b/PythonRpcServer/transcribe.py @@ -9,7 +9,7 @@ def transcribe_audio(media_filepath): if media_filepath == 'TEST-transcribe_example_result': - result_json_file = 'transcribe_example_result.json' + result_json_file = 'transcribe_exampleffmp_result.json' with open(result_json_file, 'r') as json_file: transcription_result = json.load(json_file) return transcription_result @@ -20,7 +20,7 @@ def transcribe_audio(media_filepath): # Path to the output JSON file that Whisper will generate json_output_path = f"{media_filepath}.json" - if os.path.exists(media_filepath): + if os.path.exists(json_output_path): os.remove(json_output_path) # Command to run Whisper.cpp inside the container using the main executable diff --git a/TaskEngine/Program.cs b/TaskEngine/Program.cs index ebe513e5..1aa1977e 100644 --- a/TaskEngine/Program.cs +++ b/TaskEngine/Program.cs @@ -133,21 +133,37 @@ static void runQueueAwakerForever() { _logger.LogInformation("Pausing {0} minutes before first periodicCheck", initialPauseInterval); // Thread.Sleep(initialPauseInterval); - Task.Delay(initialPauseInterval).Wait(); + // Task.Delay(initialPauseInterval).Wait(); // Check for new tasks every "timeInterval". // The periodic check will discover all undone tasks // TODO/REVIEW: However some tasks also publish the next items while (true) { + // try { + // _logger.LogInformation("Periodic Check"); + // queueAwakerTask.Publish(new JObject + // { + // { "Type", TaskType.PeriodicCheck.ToString() } + // }); + // } catch (Exception e) { + // _logger.LogError(e, "Error in Periodic Check"); + // } + try { - _logger.LogInformation("Periodic Check"); + var videoId = "ddceb720-a9d6-417d-b5ea-e94c6c0a86c6"; + _logger.LogInformation("Transcription Task Initiated"); queueAwakerTask.Publish(new JObject { - { "Type", TaskType.PeriodicCheck.ToString() } + { "Type", TaskType.TranscribeVideo.ToString() }, + { "videoOrMediaId", videoId } }); + + _logger.LogInformation("Transcription Task Published Successfully"); } catch (Exception e) { - _logger.LogError(e, "Error in Periodic Check"); + _logger.LogError(e, "Error in Transcription Task"); } + + // Thread.Sleep(timeInterval); Task.Delay(timeInterval).Wait(); _logger.LogInformation("Pausing {0} minutes before next periodicCheck", periodicCheck); From 1f259e2aafb9454b16da94fea4123bec0da2da0d Mon Sep 17 00:00:00 2001 From: Tyler Liu Date: Tue, 8 Oct 2024 20:25:25 -0500 Subject: [PATCH 15/23] translate mp4 into wav added --- PythonRpcServer/transcribe.py | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/PythonRpcServer/transcribe.py b/PythonRpcServer/transcribe.py index ab5d1504..546fbbb0 100644 --- a/PythonRpcServer/transcribe.py +++ b/PythonRpcServer/transcribe.py @@ -1,11 +1,43 @@ import os import subprocess import json +from time import perf_counter +from ffmpy import FFmpeg +import utils # Path to the Whisper executable inside the container WHISPER_EXECUTABLE = os.environ.get('WHISPER_EXE','whisper') # Executable 'main' is assumed to be in the same directory as this script MODEL = os.environ.get('WHISPER_MODEL','models/ggml-base.en.bin') +def convert_video_to_wav(input_filepath, offset=None): + """ + Converts a video file to WAV format using ffmpy. + """ + try: + start_time = perf_counter() + if offset is None: + offset = 0.0 + + nthreads = utils.getMaxThreads() + + print(f"Converting video '{input_filepath}' to WAV with offset {offset} using {nthreads} thread(s).") + output_filepath = utils.getTmpFile() + ext = '.wav' + + ff = FFmpeg( + global_options=f"-hide_banner -loglevel error -nostats -threads {nthreads}", + inputs={input_filepath: f'-ss {offset}'}, + outputs={output_filepath: '-c:a pcm_s16le -ac 1 -y -ar 16000 -f wav'} + ) + print(f"Starting conversion. Audio output will be saved in {output_filepath}") + ff.run() + end_time = perf_counter() + print(f"Conversion complete. Duration: {int(end_time - start_time)} seconds") + return output_filepath, ext + except Exception as e: + print("Exception during conversion:" + str(e)) + raise e + def transcribe_audio(media_filepath): if media_filepath == 'TEST-transcribe_example_result': @@ -18,6 +50,11 @@ def transcribe_audio(media_filepath): if not os.path.exists(media_filepath): raise FileNotFoundError(f"Media file not found: {media_filepath}") + # convert video to wav if needed + if not media_filepath.endswith('.wav'): + media_filepath, _ = convert_video_to_wav(media_filepath) + + # Path to the output JSON file that Whisper will generate json_output_path = f"{media_filepath}.json" if os.path.exists(json_output_path): @@ -41,6 +78,7 @@ def transcribe_audio(media_filepath): raise Exception(f"Whisper failed with error:\n{result.stderr.decode('utf-8')}") # Check if the output JSON file was generated + print(f"Checking for JSON output at: {json_output_path}") if not os.path.exists(json_output_path): raise FileNotFoundError(f"Expected JSON output file not found: {json_output_path}") From 4a985cdc61f9ee1eb85635c132902cf8a5f00f15 Mon Sep 17 00:00:00 2001 From: Tyler Liu Date: Wed, 9 Oct 2024 14:09:35 -0500 Subject: [PATCH 16/23] test print statement added --- PythonRpcServer/transcribe.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PythonRpcServer/transcribe.py b/PythonRpcServer/transcribe.py index 546fbbb0..b2185516 100644 --- a/PythonRpcServer/transcribe.py +++ b/PythonRpcServer/transcribe.py @@ -86,6 +86,10 @@ def transcribe_audio(media_filepath): with open(json_output_path, 'r') as json_file: transcription_result = json.load(json_file) + # Print the transcription result (testing purpose) + # print("Transcription result:") + # print(json.dumps(transcription_result, indent=4)) + # Delete the JSON file after reading it os.remove(json_output_path) print(f"Deleted the JSON file: {json_output_path}") From 2d734419b68a44f9a0280eef1cb3329a2600d548 Mon Sep 17 00:00:00 2001 From: Tyler Liu Date: Wed, 9 Oct 2024 14:19:11 -0500 Subject: [PATCH 17/23] update Dockerfile for pythonrpcserver to add sample json file --- pythonrpcserver.Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythonrpcserver.Dockerfile b/pythonrpcserver.Dockerfile index 9b60a37a..a2905617 100644 --- a/pythonrpcserver.Dockerfile +++ b/pythonrpcserver.Dockerfile @@ -31,6 +31,8 @@ RUN python -m grpc_tools.protoc -I . --python_out=./ --grpc_python_out=./ ct.proto COPY ./PythonRpcServer . + + RUN whisper -ojf -f transcribe_hellohellohello.wav CMD [ "nice", "-n", "18", "ionice", "-c", "2", "-n", "6", "python3", "-u", "/PythonRpcServer/server.py" ] From cafc4be0dba4532f15fbd3b15ddb6a7302c93131 Mon Sep 17 00:00:00 2001 From: Tyler Liu Date: Wed, 9 Oct 2024 15:43:39 -0500 Subject: [PATCH 18/23] test added for transcribe.py --- PythonRpcServer/server.py | 2 +- PythonRpcServer/transcribe.py | 41 ++++++++++++++-------- TaskEngine/Tasks/LocalTranscriptionTask.cs | 3 +- TaskEngine/Tasks/QueueAwakerTask.cs | 2 +- ct.proto | 1 + 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/PythonRpcServer/server.py b/PythonRpcServer/server.py index 44d236cd..9934151e 100644 --- a/PythonRpcServer/server.py +++ b/PythonRpcServer/server.py @@ -136,7 +136,7 @@ def TranscribeAudioRPC(self, request, context): logging.info(f"Starting transcription for file: {request.filePath}") transcription_result = LogWorker( f"TranscribeAudioRPC({request.filePath})", - lambda: transcribe_audio(request.filePath) + lambda: transcribe_audio(request.filePath, request.testing) ) logging.info(f"Transcription completed successfully for: {request.filePath}") return ct_pb2.JsonString(json=json.dumps(transcription_result)) diff --git a/PythonRpcServer/transcribe.py b/PythonRpcServer/transcribe.py index b2185516..c3bef52a 100644 --- a/PythonRpcServer/transcribe.py +++ b/PythonRpcServer/transcribe.py @@ -38,8 +38,18 @@ def convert_video_to_wav(input_filepath, offset=None): print("Exception during conversion:" + str(e)) raise e -def transcribe_audio(media_filepath): +def transcribe_audio(media_filepath, testing=False): + if testing: + json_output_path = f"/PythonRpcServer/transcribe_hellohellohello.wav.json" + with open(json_output_path, 'r') as json_file: + transcription_result = json.load(json_file) + + # Print the transcription result (testing purpose) + print("Transcription result:") + print(json.dumps(transcription_result, indent=4)) + return transcription_result + if media_filepath == 'TEST-transcribe_example_result': result_json_file = 'transcribe_exampleffmp_result.json' with open(result_json_file, 'r') as json_file: @@ -51,8 +61,10 @@ def transcribe_audio(media_filepath): raise FileNotFoundError(f"Media file not found: {media_filepath}") # convert video to wav if needed + wav_created = False # Track if WAV was created if not media_filepath.endswith('.wav'): media_filepath, _ = convert_video_to_wav(media_filepath) + wav_created = True # WAV file was created # Path to the output JSON file that Whisper will generate @@ -87,26 +99,27 @@ def transcribe_audio(media_filepath): transcription_result = json.load(json_file) # Print the transcription result (testing purpose) - # print("Transcription result:") - # print(json.dumps(transcription_result, indent=4)) + print("Transcription result:") + print(json.dumps(transcription_result, indent=4)) # Delete the JSON file after reading it os.remove(json_output_path) print(f"Deleted the JSON file: {json_output_path}") + if wav_created: + try: + os.remove(media_filepath) + print(f"Deleted the WAV file: {media_filepath}") + except Exception as e: + print(f"Error deleting WAV file: {str(e)}") + return transcription_result # Example usage if __name__ == '__main__': # Example media file path inside the container (the actual path will depend on where the file is located) - import sys - if len(sys.argv) > 1: - audio_filepath = sys.argv[1] - else: - audio_filepath = 'sharedVolume/recording0.wav' # Update this path as needed - - try: - transcription_result = transcribe_audio(audio_filepath) - print("Transcription Result:", json.dumps(transcription_result, indent=4)) - except Exception as e: - print(f"Error: {str(e)}") + json_output_path = f"/PythonRpcServer/transcribe_hellohellohello.wav.json" + with open(json_output_path, 'r') as json_file: + transcription_result = json.load(json_file) + + print("Transcription Result:", json.dumps(transcription_result, indent=4)) diff --git a/TaskEngine/Tasks/LocalTranscriptionTask.cs b/TaskEngine/Tasks/LocalTranscriptionTask.cs index 34eab4b8..aca11f56 100644 --- a/TaskEngine/Tasks/LocalTranscriptionTask.cs +++ b/TaskEngine/Tasks/LocalTranscriptionTask.cs @@ -98,7 +98,8 @@ protected async override Task OnConsume(string videoId, TaskParameters taskParam LogId = videoId, FilePath = video.Video1.VMPath, Model = "en", - Language = "en" + Language = "en", + Testing = true // PhraseHints = phraseHints, // CourseHints = "", // OutputLanguages = "en" diff --git a/TaskEngine/Tasks/QueueAwakerTask.cs b/TaskEngine/Tasks/QueueAwakerTask.cs index 3c4b0d2d..0fb5fe45 100644 --- a/TaskEngine/Tasks/QueueAwakerTask.cs +++ b/TaskEngine/Tasks/QueueAwakerTask.cs @@ -404,7 +404,7 @@ protected async override Task OnConsume(JObject jObject, TaskParameters taskPara else if (type == TaskType.TranscribeVideo.ToString()) { var id = jObject["videoOrMediaId"].ToString(); - + GetLogger().LogInformation($"{type}:{id}"); var video = await _context.Videos.FindAsync(id); diff --git a/ct.proto b/ct.proto index e16853c6..17958231 100644 --- a/ct.proto +++ b/ct.proto @@ -29,6 +29,7 @@ message TranscriptionRequest { string model = 2; // Whisper model to use (e.g., 'base-en', 'tiny-en') string language = 3; // Language in audio. string logId = 4; + bool testing = 5; } From 12ee8f25af421bca19c89750853294e440df39b7 Mon Sep 17 00:00:00 2001 From: Tyler Liu Date: Wed, 9 Oct 2024 17:13:30 -0500 Subject: [PATCH 19/23] caption store in db bug fixed --- PythonRpcServer/transcribe.py | 1 + .../transcribe_example_result.json | 2152 ++++++++--------- TaskEngine/Tasks/LocalTranscriptionTask.cs | 18 +- 3 files changed, 1091 insertions(+), 1080 deletions(-) diff --git a/PythonRpcServer/transcribe.py b/PythonRpcServer/transcribe.py index c3bef52a..71344698 100644 --- a/PythonRpcServer/transcribe.py +++ b/PythonRpcServer/transcribe.py @@ -123,3 +123,4 @@ def transcribe_audio(media_filepath, testing=False): transcription_result = json.load(json_file) print("Transcription Result:", json.dumps(transcription_result, indent=4)) + \ No newline at end of file diff --git a/PythonRpcServer/transcribe_example_result.json b/PythonRpcServer/transcribe_example_result.json index 5f673c47..1726e02d 100644 --- a/PythonRpcServer/transcribe_example_result.json +++ b/PythonRpcServer/transcribe_example_result.json @@ -1,1077 +1,1077 @@ { - "systeminfo": "AVX = 0 | AVX2 = 0 | AVX512 = 0 | FMA = 0 | NEON = 1 | ARM_FMA = 1 | METAL = 1 | F16C = 0 | FP16_VA = 1 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 0 | SSSE3 = 0 | VSX = 0 | CUDA = 0 | COREML = 0 | OPENVINO = 0 | CANN = 0", - "model": { - "type": "base", - "multilingual": false, - "vocab": 51864, - "audio": { - "ctx": 1500, - "state": 512, - "head": 8, - "layer": 6 - }, - "text": { - "ctx": 448, - "state": 512, - "head": 8, - "layer": 6 - }, - "mels": 80, - "ftype": 1 - }, - "params": { - "model": "models/ggml-base.en.bin", - "language": "en", - "translate": false - }, - "result": { - "language": "en" - }, - "transcription": [ - { - "timestamps": { - "from": "00:00:00,000", - "to": "00:00:07,320" - }, - "offsets": { - "from": 0, - "to": 7320 - }, - "text": " Reading homeworks are due early Tuesday at 9pm unless announced otherwise.", - "tokens": [ - { - "text": "[_BEG_]", - "timestamps": { - "from": "00:00:00,000", - "to": "00:00:00,000" - }, - "offsets": { - "from": 0, - "to": 0 - }, - "id": 50363, - "p": 0.848947, - "t_dtw": -1 - }, - { - "text": " Reading", - "timestamps": { - "from": "00:00:00,000", - "to": "00:00:00,750" - }, - "offsets": { - "from": 0, - "to": 750 - }, - "id": 11725, - "p": 0.407652, - "t_dtw": -1 - }, - { - "text": " hom", - "timestamps": { - "from": "00:00:00,750", - "to": "00:00:01,070" - }, - "offsets": { - "from": 750, - "to": 1070 - }, - "id": 3488, - "p": 0.254302, - "t_dtw": -1 - }, - { - "text": "eworks", - "timestamps": { - "from": "00:00:01,080", - "to": "00:00:01,710" - }, - "offsets": { - "from": 1080, - "to": 1710 - }, - "id": 19653, - "p": 0.981512, - "t_dtw": -1 - }, - { - "text": " are", - "timestamps": { - "from": "00:00:01,710", - "to": "00:00:01,970" - }, - "offsets": { - "from": 1710, - "to": 1970 - }, - "id": 389, - "p": 0.708301, - "t_dtw": -1 - }, - { - "text": " due", - "timestamps": { - "from": "00:00:02,060", - "to": "00:00:02,350" - }, - "offsets": { - "from": 2060, - "to": 2350 - }, - "id": 2233, - "p": 0.54323, - "t_dtw": -1 - }, - { - "text": " early", - "timestamps": { - "from": "00:00:02,350", - "to": "00:00:02,880" - }, - "offsets": { - "from": 2350, - "to": 2880 - }, - "id": 1903, - "p": 0.315304, - "t_dtw": -1 - }, - { - "text": " Tuesday", - "timestamps": { - "from": "00:00:02,880", - "to": "00:00:03,630" - }, - "offsets": { - "from": 2880, - "to": 3630 - }, - "id": 3431, - "p": 0.614094, - "t_dtw": -1 - }, - { - "text": " at", - "timestamps": { - "from": "00:00:03,630", - "to": "00:00:03,840" - }, - "offsets": { - "from": 3630, - "to": 3840 - }, - "id": 379, - "p": 0.960146, - "t_dtw": -1 - }, - { - "text": " 9", - "timestamps": { - "from": "00:00:03,840", - "to": "00:00:04,150" - }, - "offsets": { - "from": 3840, - "to": 4150 - }, - "id": 860, - "p": 0.923494, - "t_dtw": -1 - }, - { - "text": "pm", - "timestamps": { - "from": "00:00:04,160", - "to": "00:00:04,370" - }, - "offsets": { - "from": 4160, - "to": 4370 - }, - "id": 4426, - "p": 0.398094, - "t_dtw": -1 - }, - { - "text": " unless", - "timestamps": { - "from": "00:00:04,370", - "to": "00:00:05,010" - }, - "offsets": { - "from": 4370, - "to": 5010 - }, - "id": 4556, - "p": 0.787165, - "t_dtw": -1 - }, - { - "text": " announced", - "timestamps": { - "from": "00:00:05,010", - "to": "00:00:05,970" - }, - "offsets": { - "from": 5010, - "to": 5970 - }, - "id": 3414, - "p": 0.980173, - "t_dtw": -1 - }, - { - "text": " otherwise", - "timestamps": { - "from": "00:00:05,970", - "to": "00:00:07,000" - }, - "offsets": { - "from": 5970, - "to": 7000 - }, - "id": 4306, - "p": 0.942659, - "t_dtw": -1 - }, - { - "text": ".", - "timestamps": { - "from": "00:00:07,000", - "to": "00:00:07,320" - }, - "offsets": { - "from": 7000, - "to": 7320 - }, - "id": 13, - "p": 0.86153, - "t_dtw": -1 - }, - { - "text": "[_TT_366]", - "timestamps": { - "from": "00:00:07,320", - "to": "00:00:07,320" - }, - "offsets": { - "from": 7320, - "to": 7320 - }, - "id": 50729, - "p": 0.0230646, - "t_dtw": -1 - } - ] - }, - { - "timestamps": { - "from": "00:00:07,320", - "to": "00:00:12,600" - }, - "offsets": { - "from": 7320, - "to": 12600 - }, - "text": " We post each week's homework at least one full week before it's due date.", - "tokens": [ - { - "text": " We", - "timestamps": { - "from": "00:00:07,320", - "to": "00:00:07,490" - }, - "offsets": { - "from": 7320, - "to": 7490 - }, - "id": 775, - "p": 0.961556, - "t_dtw": -1 - }, - { - "text": " post", - "timestamps": { - "from": "00:00:07,490", - "to": "00:00:07,840" - }, - "offsets": { - "from": 7490, - "to": 7840 - }, - "id": 1281, - "p": 0.940814, - "t_dtw": -1 - }, - { - "text": " each", - "timestamps": { - "from": "00:00:07,840", - "to": "00:00:08,190" - }, - "offsets": { - "from": 7840, - "to": 8190 - }, - "id": 1123, - "p": 0.990758, - "t_dtw": -1 - }, - { - "text": " week", - "timestamps": { - "from": "00:00:08,190", - "to": "00:00:08,540" - }, - "offsets": { - "from": 8190, - "to": 8540 - }, - "id": 1285, - "p": 0.98414, - "t_dtw": -1 - }, - { - "text": "'s", - "timestamps": { - "from": "00:00:08,540", - "to": "00:00:08,710" - }, - "offsets": { - "from": 8540, - "to": 8710 - }, - "id": 338, - "p": 0.852609, - "t_dtw": -1 - }, - { - "text": " homework", - "timestamps": { - "from": "00:00:08,710", - "to": "00:00:09,420" - }, - "offsets": { - "from": 8710, - "to": 9420 - }, - "id": 26131, - "p": 0.9842, - "t_dtw": -1 - }, - { - "text": " at", - "timestamps": { - "from": "00:00:09,420", - "to": "00:00:09,590" - }, - "offsets": { - "from": 9420, - "to": 9590 - }, - "id": 379, - "p": 0.989681, - "t_dtw": -1 - }, - { - "text": " least", - "timestamps": { - "from": "00:00:09,590", - "to": "00:00:10,020" - }, - "offsets": { - "from": 9590, - "to": 10020 - }, - "id": 1551, - "p": 0.992646, - "t_dtw": -1 - }, - { - "text": " one", - "timestamps": { - "from": "00:00:10,030", - "to": "00:00:10,290" - }, - "offsets": { - "from": 10030, - "to": 10290 - }, - "id": 530, - "p": 0.768113, - "t_dtw": -1 - }, - { - "text": " full", - "timestamps": { - "from": "00:00:10,290", - "to": "00:00:10,640" - }, - "offsets": { - "from": 10290, - "to": 10640 - }, - "id": 1336, - "p": 0.589241, - "t_dtw": -1 - }, - { - "text": " week", - "timestamps": { - "from": "00:00:10,640", - "to": "00:00:10,990" - }, - "offsets": { - "from": 10640, - "to": 10990 - }, - "id": 1285, - "p": 0.992968, - "t_dtw": -1 - }, - { - "text": " before", - "timestamps": { - "from": "00:00:10,990", - "to": "00:00:11,510" - }, - "offsets": { - "from": 10990, - "to": 11510 - }, - "id": 878, - "p": 0.988001, - "t_dtw": -1 - }, - { - "text": " it", - "timestamps": { - "from": "00:00:11,530", - "to": "00:00:11,690" - }, - "offsets": { - "from": 11530, - "to": 11690 - }, - "id": 340, - "p": 0.670674, - "t_dtw": -1 - }, - { - "text": "'s", - "timestamps": { - "from": "00:00:11,690", - "to": "00:00:11,850" - }, - "offsets": { - "from": 11690, - "to": 11850 - }, - "id": 338, - "p": 0.866573, - "t_dtw": -1 - }, - { - "text": " due", - "timestamps": { - "from": "00:00:11,860", - "to": "00:00:12,120" - }, - "offsets": { - "from": 11860, - "to": 12120 - }, - "id": 2233, - "p": 0.991537, - "t_dtw": -1 - }, - { - "text": " date", - "timestamps": { - "from": "00:00:12,120", - "to": "00:00:12,600" - }, - "offsets": { - "from": 12120, - "to": 12600 - }, - "id": 3128, - "p": 0.652784, - "t_dtw": -1 - }, - { - "text": ".", - "timestamps": { - "from": "00:00:12,600", - "to": "00:00:12,600" - }, - "offsets": { - "from": 12600, - "to": 12600 - }, - "id": 13, - "p": 0.929698, - "t_dtw": -1 - }, - { - "text": "[_TT_630]", - "timestamps": { - "from": "00:00:12,600", - "to": "00:00:12,600" - }, - "offsets": { - "from": 12600, - "to": 12600 - }, - "id": 50993, - "p": 0.0345322, - "t_dtw": -1 - } - ] - }, - { - "timestamps": { - "from": "00:00:12,600", - "to": "00:00:17,280" - }, - "offsets": { - "from": 12600, - "to": 17280 - }, - "text": " We post solutions almost a day after extended due date.", - "tokens": [ - { - "text": " We", - "timestamps": { - "from": "00:00:12,600", - "to": "00:00:12,790" - }, - "offsets": { - "from": 12600, - "to": 12790 - }, - "id": 775, - "p": 0.99553, - "t_dtw": -1 - }, - { - "text": " post", - "timestamps": { - "from": "00:00:12,790", - "to": "00:00:13,180" - }, - "offsets": { - "from": 12790, - "to": 13180 - }, - "id": 1281, - "p": 0.991259, - "t_dtw": -1 - }, - { - "text": " solutions", - "timestamps": { - "from": "00:00:13,180", - "to": "00:00:14,060" - }, - "offsets": { - "from": 13180, - "to": 14060 - }, - "id": 8136, - "p": 0.965852, - "t_dtw": -1 - }, - { - "text": " almost", - "timestamps": { - "from": "00:00:14,060", - "to": "00:00:14,650" - }, - "offsets": { - "from": 14060, - "to": 14650 - }, - "id": 2048, - "p": 0.395074, - "t_dtw": -1 - }, - { - "text": " a", - "timestamps": { - "from": "00:00:14,650", - "to": "00:00:14,740" - }, - "offsets": { - "from": 14650, - "to": 14740 - }, - "id": 257, - "p": 0.984885, - "t_dtw": -1 - }, - { - "text": " day", - "timestamps": { - "from": "00:00:14,740", - "to": "00:00:15,030" - }, - "offsets": { - "from": 14740, - "to": 15030 - }, - "id": 1110, - "p": 0.997579, - "t_dtw": -1 - }, - { - "text": " after", - "timestamps": { - "from": "00:00:15,030", - "to": "00:00:15,520" - }, - "offsets": { - "from": 15030, - "to": 15520 - }, - "id": 706, - "p": 0.997904, - "t_dtw": -1 - }, - { - "text": " extended", - "timestamps": { - "from": "00:00:15,520", - "to": "00:00:16,300" - }, - "offsets": { - "from": 15520, - "to": 16300 - }, - "id": 7083, - "p": 0.59029, - "t_dtw": -1 - }, - { - "text": " due", - "timestamps": { - "from": "00:00:16,300", - "to": "00:00:16,590" - }, - "offsets": { - "from": 16300, - "to": 16590 - }, - "id": 2233, - "p": 0.986455, - "t_dtw": -1 - }, - { - "text": " date", - "timestamps": { - "from": "00:00:16,590", - "to": "00:00:17,040" - }, - "offsets": { - "from": 16590, - "to": 17040 - }, - "id": 3128, - "p": 0.994649, - "t_dtw": -1 - }, - { - "text": ".", - "timestamps": { - "from": "00:00:17,040", - "to": "00:00:17,280" - }, - "offsets": { - "from": 17040, - "to": 17280 - }, - "id": 13, - "p": 0.967296, - "t_dtw": -1 - }, - { - "text": "[_TT_864]", - "timestamps": { - "from": "00:00:17,280", - "to": "00:00:17,280" - }, - "offsets": { - "from": 17280, - "to": 17280 - }, - "id": 51227, - "p": 0.0279233, - "t_dtw": -1 - } - ] - }, - { - "timestamps": { - "from": "00:00:17,280", - "to": "00:00:21,120" - }, - "offsets": { - "from": 17280, - "to": 21120 - }, - "text": " Links to future homeworks and solutions are placeholders.", - "tokens": [ - { - "text": " Links", - "timestamps": { - "from": "00:00:17,280", - "to": "00:00:17,640" - }, - "offsets": { - "from": 17280, - "to": 17640 - }, - "id": 21691, - "p": 0.970288, - "t_dtw": -1 - }, - { - "text": " to", - "timestamps": { - "from": "00:00:17,640", - "to": "00:00:17,780" - }, - "offsets": { - "from": 17640, - "to": 17780 - }, - "id": 284, - "p": 0.997626, - "t_dtw": -1 - }, - { - "text": " future", - "timestamps": { - "from": "00:00:17,780", - "to": "00:00:18,220" - }, - "offsets": { - "from": 17780, - "to": 18220 - }, - "id": 2003, - "p": 0.99132, - "t_dtw": -1 - }, - { - "text": " hom", - "timestamps": { - "from": "00:00:18,220", - "to": "00:00:18,440" - }, - "offsets": { - "from": 18220, - "to": 18440 - }, - "id": 3488, - "p": 0.86544, - "t_dtw": -1 - }, - { - "text": "eworks", - "timestamps": { - "from": "00:00:18,440", - "to": "00:00:18,880" - }, - "offsets": { - "from": 18440, - "to": 18880 - }, - "id": 19653, - "p": 0.993508, - "t_dtw": -1 - }, - { - "text": " and", - "timestamps": { - "from": "00:00:18,880", - "to": "00:00:19,100" - }, - "offsets": { - "from": 18880, - "to": 19100 - }, - "id": 290, - "p": 0.988634, - "t_dtw": -1 - }, - { - "text": " solutions", - "timestamps": { - "from": "00:00:19,100", - "to": "00:00:19,760" - }, - "offsets": { - "from": 19100, - "to": 19760 - }, - "id": 8136, - "p": 0.978441, - "t_dtw": -1 - }, - { - "text": " are", - "timestamps": { - "from": "00:00:19,760", - "to": "00:00:19,980" - }, - "offsets": { - "from": 19760, - "to": 19980 - }, - "id": 389, - "p": 0.998055, - "t_dtw": -1 - }, - { - "text": " place", - "timestamps": { - "from": "00:00:19,980", - "to": "00:00:20,340" - }, - "offsets": { - "from": 19980, - "to": 20340 - }, - "id": 1295, - "p": 0.853392, - "t_dtw": -1 - }, - { - "text": "holders", - "timestamps": { - "from": "00:00:20,340", - "to": "00:00:20,850" - }, - "offsets": { - "from": 20340, - "to": 20850 - }, - "id": 10476, - "p": 0.982725, - "t_dtw": -1 - }, - { - "text": ".", - "timestamps": { - "from": "00:00:20,850", - "to": "00:00:21,120" - }, - "offsets": { - "from": 20850, - "to": 21120 - }, - "id": 13, - "p": 0.942825, - "t_dtw": -1 - }, - { - "text": "[_TT_1056]", - "timestamps": { - "from": "00:00:21,120", - "to": "00:00:21,120" - }, - "offsets": { - "from": 21120, - "to": 21120 - }, - "id": 51419, - "p": 0.0691018, - "t_dtw": -1 - } - ] - }, - { - "timestamps": { - "from": "00:00:21,120", - "to": "00:00:24,520" - }, - "offsets": { - "from": 21120, - "to": 24520 - }, - "text": " Tucks of future homeworks are subject to change.", - "tokens": [ - { - "text": " T", - "timestamps": { - "from": "00:00:21,120", - "to": "00:00:21,200" - }, - "offsets": { - "from": 21120, - "to": 21200 - }, - "id": 309, - "p": 0.433385, - "t_dtw": -1 - }, - { - "text": "ucks", - "timestamps": { - "from": "00:00:21,200", - "to": "00:00:21,530" - }, - "offsets": { - "from": 21200, - "to": 21530 - }, - "id": 6238, - "p": 0.424658, - "t_dtw": -1 - }, - { - "text": " of", - "timestamps": { - "from": "00:00:21,530", - "to": "00:00:21,690" - }, - "offsets": { - "from": 21530, - "to": 21690 - }, - "id": 286, - "p": 0.985274, - "t_dtw": -1 - }, - { - "text": " future", - "timestamps": { - "from": "00:00:21,690", - "to": "00:00:22,180" - }, - "offsets": { - "from": 21690, - "to": 22180 - }, - "id": 2003, - "p": 0.997737, - "t_dtw": -1 - }, - { - "text": " hom", - "timestamps": { - "from": "00:00:22,180", - "to": "00:00:22,420" - }, - "offsets": { - "from": 22180, - "to": 22420 - }, - "id": 3488, - "p": 0.975576, - "t_dtw": -1 - }, - { - "text": "eworks", - "timestamps": { - "from": "00:00:22,420", - "to": "00:00:22,910" - }, - "offsets": { - "from": 22420, - "to": 22910 - }, - "id": 19653, - "p": 0.993154, - "t_dtw": -1 - }, - { - "text": " are", - "timestamps": { - "from": "00:00:22,910", - "to": "00:00:23,150" - }, - "offsets": { - "from": 22910, - "to": 23150 - }, - "id": 389, - "p": 0.998262, - "t_dtw": -1 - }, - { - "text": " subject", - "timestamps": { - "from": "00:00:23,150", - "to": "00:00:23,730" - }, - "offsets": { - "from": 23150, - "to": 23730 - }, - "id": 2426, - "p": 0.991766, - "t_dtw": -1 - }, - { - "text": " to", - "timestamps": { - "from": "00:00:23,730", - "to": "00:00:23,890" - }, - "offsets": { - "from": 23730, - "to": 23890 - }, - "id": 284, - "p": 0.908062, - "t_dtw": -1 - }, - { - "text": " change", - "timestamps": { - "from": "00:00:23,890", - "to": "00:00:24,440" - }, - "offsets": { - "from": 23890, - "to": 24440 - }, - "id": 1487, - "p": 0.997778, - "t_dtw": -1 - }, - { - "text": ".", - "timestamps": { - "from": "00:00:24,440", - "to": "00:00:24,520" - }, - "offsets": { - "from": 24440, - "to": 24520 - }, - "id": 13, - "p": 0.981104, - "t_dtw": -1 - }, - { - "text": "[_TT_1226]", - "timestamps": { - "from": "00:00:24,520", - "to": "00:00:24,520" - }, - "offsets": { - "from": 24520, - "to": 24520 - }, - "id": 51589, - "p": 0.0327583, - "t_dtw": -1 - } - ] - } - ] -} + "systeminfo": "AVX = 0 | AVX2 = 0 | AVX512 = 0 | FMA = 0 | NEON = 1 | ARM_FMA = 1 | METAL = 1 | F16C = 0 | FP16_VA = 1 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 0 | SSSE3 = 0 | VSX = 0 | CUDA = 0 | COREML = 0 | OPENVINO = 0 | CANN = 0", + "model": { + "type": "base", + "multilingual": false, + "vocab": 51864, + "audio": { + "ctx": 1500, + "state": 512, + "head": 8, + "layer": 6 + }, + "text": { + "ctx": 448, + "state": 512, + "head": 8, + "layer": 6 + }, + "mels": 80, + "ftype": 1 + }, + "params": { + "model": "models/ggml-base.en.bin", + "language": "en", + "translate": false + }, + "result": { + "language": "en" + }, + "transcription": [ + { + "timestamps": { + "from": "00:00:00.000", + "to": "00:00:07.320" + }, + "offsets": { + "from": 0, + "to": 7320 + }, + "text": " Reading homeworks are due early Tuesday at 9pm unless announced otherwise.", + "tokens": [ + { + "text": "[_BEG_]", + "timestamps": { + "from": "00:00:00.000", + "to": "00:00:00.000" + }, + "offsets": { + "from": 0, + "to": 0 + }, + "id": 50363, + "p": 0.848947, + "t_dtw": -1 + }, + { + "text": " Reading", + "timestamps": { + "from": "00:00:00.000", + "to": "00:00:00.750" + }, + "offsets": { + "from": 0, + "to": 750 + }, + "id": 11725, + "p": 0.407652, + "t_dtw": -1 + }, + { + "text": " hom", + "timestamps": { + "from": "00:00:00.750", + "to": "00:00:01.070" + }, + "offsets": { + "from": 750, + "to": 1070 + }, + "id": 3488, + "p": 0.254302, + "t_dtw": -1 + }, + { + "text": "eworks", + "timestamps": { + "from": "00:00:01.080", + "to": "00:00:01.710" + }, + "offsets": { + "from": 1080, + "to": 1710 + }, + "id": 19653, + "p": 0.981512, + "t_dtw": -1 + }, + { + "text": " are", + "timestamps": { + "from": "00:00:01.710", + "to": "00:00:01.970" + }, + "offsets": { + "from": 1710, + "to": 1970 + }, + "id": 389, + "p": 0.708301, + "t_dtw": -1 + }, + { + "text": " due", + "timestamps": { + "from": "00:00:02.060", + "to": "00:00:02.350" + }, + "offsets": { + "from": 2060, + "to": 2350 + }, + "id": 2233, + "p": 0.54323, + "t_dtw": -1 + }, + { + "text": " early", + "timestamps": { + "from": "00:00:02.350", + "to": "00:00:02.880" + }, + "offsets": { + "from": 2350, + "to": 2880 + }, + "id": 1903, + "p": 0.315304, + "t_dtw": -1 + }, + { + "text": " Tuesday", + "timestamps": { + "from": "00:00:02.880", + "to": "00:00:03.630" + }, + "offsets": { + "from": 2880, + "to": 3630 + }, + "id": 3431, + "p": 0.614094, + "t_dtw": -1 + }, + { + "text": " at", + "timestamps": { + "from": "00:00:03.630", + "to": "00:00:03.840" + }, + "offsets": { + "from": 3630, + "to": 3840 + }, + "id": 379, + "p": 0.960146, + "t_dtw": -1 + }, + { + "text": " 9", + "timestamps": { + "from": "00:00:03.840", + "to": "00:00:04.150" + }, + "offsets": { + "from": 3840, + "to": 4150 + }, + "id": 860, + "p": 0.923494, + "t_dtw": -1 + }, + { + "text": "pm", + "timestamps": { + "from": "00:00:04.160", + "to": "00:00:04.370" + }, + "offsets": { + "from": 4160, + "to": 4370 + }, + "id": 4426, + "p": 0.398094, + "t_dtw": -1 + }, + { + "text": " unless", + "timestamps": { + "from": "00:00:04.370", + "to": "00:00:05.010" + }, + "offsets": { + "from": 4370, + "to": 5010 + }, + "id": 4556, + "p": 0.787165, + "t_dtw": -1 + }, + { + "text": " announced", + "timestamps": { + "from": "00:00:05.010", + "to": "00:00:05.970" + }, + "offsets": { + "from": 5010, + "to": 5970 + }, + "id": 3414, + "p": 0.980173, + "t_dtw": -1 + }, + { + "text": " otherwise", + "timestamps": { + "from": "00:00:05.970", + "to": "00:00:07.000" + }, + "offsets": { + "from": 5970, + "to": 7000 + }, + "id": 4306, + "p": 0.942659, + "t_dtw": -1 + }, + { + "text": ".", + "timestamps": { + "from": "00:00:07.000", + "to": "00:00:07.320" + }, + "offsets": { + "from": 7000, + "to": 7320 + }, + "id": 13, + "p": 0.86153, + "t_dtw": -1 + }, + { + "text": "[_TT_366]", + "timestamps": { + "from": "00:00:07.320", + "to": "00:00:07.320" + }, + "offsets": { + "from": 7320, + "to": 7320 + }, + "id": 50729, + "p": 0.0230646, + "t_dtw": -1 + } + ] + }, + { + "timestamps": { + "from": "00:00:07.320", + "to": "00:00:12.600" + }, + "offsets": { + "from": 7320, + "to": 12600 + }, + "text": " We post each week's homework at least one full week before it's due date.", + "tokens": [ + { + "text": " We", + "timestamps": { + "from": "00:00:07.320", + "to": "00:00:07.490" + }, + "offsets": { + "from": 7320, + "to": 7490 + }, + "id": 775, + "p": 0.961556, + "t_dtw": -1 + }, + { + "text": " post", + "timestamps": { + "from": "00:00:07.490", + "to": "00:00:07.840" + }, + "offsets": { + "from": 7490, + "to": 7840 + }, + "id": 1281, + "p": 0.940814, + "t_dtw": -1 + }, + { + "text": " each", + "timestamps": { + "from": "00:00:07.840", + "to": "00:00:08.190" + }, + "offsets": { + "from": 7840, + "to": 8190 + }, + "id": 1123, + "p": 0.990758, + "t_dtw": -1 + }, + { + "text": " week", + "timestamps": { + "from": "00:00:08.190", + "to": "00:00:08.540" + }, + "offsets": { + "from": 8190, + "to": 8540 + }, + "id": 1285, + "p": 0.98414, + "t_dtw": -1 + }, + { + "text": "'s", + "timestamps": { + "from": "00:00:08.540", + "to": "00:00:08.710" + }, + "offsets": { + "from": 8540, + "to": 8710 + }, + "id": 338, + "p": 0.852609, + "t_dtw": -1 + }, + { + "text": " homework", + "timestamps": { + "from": "00:00:08.710", + "to": "00:00:09.420" + }, + "offsets": { + "from": 8710, + "to": 9420 + }, + "id": 26131, + "p": 0.9842, + "t_dtw": -1 + }, + { + "text": " at", + "timestamps": { + "from": "00:00:09.420", + "to": "00:00:09.590" + }, + "offsets": { + "from": 9420, + "to": 9590 + }, + "id": 379, + "p": 0.989681, + "t_dtw": -1 + }, + { + "text": " least", + "timestamps": { + "from": "00:00:09.590", + "to": "00:00:10.020" + }, + "offsets": { + "from": 9590, + "to": 10020 + }, + "id": 1551, + "p": 0.992646, + "t_dtw": -1 + }, + { + "text": " one", + "timestamps": { + "from": "00:00:10.030", + "to": "00:00:10.290" + }, + "offsets": { + "from": 10030, + "to": 10290 + }, + "id": 530, + "p": 0.768113, + "t_dtw": -1 + }, + { + "text": " full", + "timestamps": { + "from": "00:00:10.290", + "to": "00:00:10.640" + }, + "offsets": { + "from": 10290, + "to": 10640 + }, + "id": 1336, + "p": 0.589241, + "t_dtw": -1 + }, + { + "text": " week", + "timestamps": { + "from": "00:00:10.640", + "to": "00:00:10.990" + }, + "offsets": { + "from": 10640, + "to": 10990 + }, + "id": 1285, + "p": 0.992968, + "t_dtw": -1 + }, + { + "text": " before", + "timestamps": { + "from": "00:00:10.990", + "to": "00:00:11.510" + }, + "offsets": { + "from": 10990, + "to": 11510 + }, + "id": 878, + "p": 0.988001, + "t_dtw": -1 + }, + { + "text": " it", + "timestamps": { + "from": "00:00:11.530", + "to": "00:00:11.690" + }, + "offsets": { + "from": 11530, + "to": 11690 + }, + "id": 340, + "p": 0.670674, + "t_dtw": -1 + }, + { + "text": "'s", + "timestamps": { + "from": "00:00:11.690", + "to": "00:00:11.850" + }, + "offsets": { + "from": 11690, + "to": 11850 + }, + "id": 338, + "p": 0.866573, + "t_dtw": -1 + }, + { + "text": " due", + "timestamps": { + "from": "00:00:11.860", + "to": "00:00:12.120" + }, + "offsets": { + "from": 11860, + "to": 12120 + }, + "id": 2233, + "p": 0.991537, + "t_dtw": -1 + }, + { + "text": " date", + "timestamps": { + "from": "00:00:12.120", + "to": "00:00:12.600" + }, + "offsets": { + "from": 12120, + "to": 12600 + }, + "id": 3128, + "p": 0.652784, + "t_dtw": -1 + }, + { + "text": ".", + "timestamps": { + "from": "00:00:12.600", + "to": "00:00:12.600" + }, + "offsets": { + "from": 12600, + "to": 12600 + }, + "id": 13, + "p": 0.929698, + "t_dtw": -1 + }, + { + "text": "[_TT_630]", + "timestamps": { + "from": "00:00:12.600", + "to": "00:00:12.600" + }, + "offsets": { + "from": 12600, + "to": 12600 + }, + "id": 50993, + "p": 0.0345322, + "t_dtw": -1 + } + ] + }, + { + "timestamps": { + "from": "00:00:12.600", + "to": "00:00:17.280" + }, + "offsets": { + "from": 12600, + "to": 17280 + }, + "text": " We post solutions almost a day after extended due date.", + "tokens": [ + { + "text": " We", + "timestamps": { + "from": "00:00:12.600", + "to": "00:00:12.790" + }, + "offsets": { + "from": 12600, + "to": 12790 + }, + "id": 775, + "p": 0.99553, + "t_dtw": -1 + }, + { + "text": " post", + "timestamps": { + "from": "00:00:12.790", + "to": "00:00:13.180" + }, + "offsets": { + "from": 12790, + "to": 13180 + }, + "id": 1281, + "p": 0.991259, + "t_dtw": -1 + }, + { + "text": " solutions", + "timestamps": { + "from": "00:00:13.180", + "to": "00:00:14.060" + }, + "offsets": { + "from": 13180, + "to": 14060 + }, + "id": 8136, + "p": 0.965852, + "t_dtw": -1 + }, + { + "text": " almost", + "timestamps": { + "from": "00:00:14.060", + "to": "00:00:14.650" + }, + "offsets": { + "from": 14060, + "to": 14650 + }, + "id": 2048, + "p": 0.395074, + "t_dtw": -1 + }, + { + "text": " a", + "timestamps": { + "from": "00:00:14.650", + "to": "00:00:14.740" + }, + "offsets": { + "from": 14650, + "to": 14740 + }, + "id": 257, + "p": 0.984885, + "t_dtw": -1 + }, + { + "text": " day", + "timestamps": { + "from": "00:00:14.740", + "to": "00:00:15.030" + }, + "offsets": { + "from": 14740, + "to": 15030 + }, + "id": 1110, + "p": 0.997579, + "t_dtw": -1 + }, + { + "text": " after", + "timestamps": { + "from": "00:00:15.030", + "to": "00:00:15.520" + }, + "offsets": { + "from": 15030, + "to": 15520 + }, + "id": 706, + "p": 0.997904, + "t_dtw": -1 + }, + { + "text": " extended", + "timestamps": { + "from": "00:00:15.520", + "to": "00:00:16.300" + }, + "offsets": { + "from": 15520, + "to": 16300 + }, + "id": 7083, + "p": 0.59029, + "t_dtw": -1 + }, + { + "text": " due", + "timestamps": { + "from": "00:00:16.300", + "to": "00:00:16.590" + }, + "offsets": { + "from": 16300, + "to": 16590 + }, + "id": 2233, + "p": 0.986455, + "t_dtw": -1 + }, + { + "text": " date", + "timestamps": { + "from": "00:00:16.590", + "to": "00:00:17.040" + }, + "offsets": { + "from": 16590, + "to": 17040 + }, + "id": 3128, + "p": 0.994649, + "t_dtw": -1 + }, + { + "text": ".", + "timestamps": { + "from": "00:00:17.040", + "to": "00:00:17.280" + }, + "offsets": { + "from": 17040, + "to": 17280 + }, + "id": 13, + "p": 0.967296, + "t_dtw": -1 + }, + { + "text": "[_TT_864]", + "timestamps": { + "from": "00:00:17.280", + "to": "00:00:17.280" + }, + "offsets": { + "from": 17280, + "to": 17280 + }, + "id": 51227, + "p": 0.0279233, + "t_dtw": -1 + } + ] + }, + { + "timestamps": { + "from": "00:00:17.280", + "to": "00:00:21.120" + }, + "offsets": { + "from": 17280, + "to": 21120 + }, + "text": " Links to future homeworks and solutions are placeholders.", + "tokens": [ + { + "text": " Links", + "timestamps": { + "from": "00:00:17.280", + "to": "00:00:17.640" + }, + "offsets": { + "from": 17280, + "to": 17640 + }, + "id": 21691, + "p": 0.970288, + "t_dtw": -1 + }, + { + "text": " to", + "timestamps": { + "from": "00:00:17.640", + "to": "00:00:17.780" + }, + "offsets": { + "from": 17640, + "to": 17780 + }, + "id": 284, + "p": 0.997626, + "t_dtw": -1 + }, + { + "text": " future", + "timestamps": { + "from": "00:00:17.780", + "to": "00:00:18.220" + }, + "offsets": { + "from": 17780, + "to": 18220 + }, + "id": 2003, + "p": 0.99132, + "t_dtw": -1 + }, + { + "text": " hom", + "timestamps": { + "from": "00:00:18.220", + "to": "00:00:18.440" + }, + "offsets": { + "from": 18220, + "to": 18440 + }, + "id": 3488, + "p": 0.86544, + "t_dtw": -1 + }, + { + "text": "eworks", + "timestamps": { + "from": "00:00:18.440", + "to": "00:00:18.880" + }, + "offsets": { + "from": 18440, + "to": 18880 + }, + "id": 19653, + "p": 0.993508, + "t_dtw": -1 + }, + { + "text": " and", + "timestamps": { + "from": "00:00:18.880", + "to": "00:00:19.100" + }, + "offsets": { + "from": 18880, + "to": 19100 + }, + "id": 290, + "p": 0.988634, + "t_dtw": -1 + }, + { + "text": " solutions", + "timestamps": { + "from": "00:00:19.100", + "to": "00:00:19.760" + }, + "offsets": { + "from": 19100, + "to": 19760 + }, + "id": 8136, + "p": 0.978441, + "t_dtw": -1 + }, + { + "text": " are", + "timestamps": { + "from": "00:00:19.760", + "to": "00:00:19.980" + }, + "offsets": { + "from": 19760, + "to": 19980 + }, + "id": 389, + "p": 0.998055, + "t_dtw": -1 + }, + { + "text": " place", + "timestamps": { + "from": "00:00:19.980", + "to": "00:00:20.340" + }, + "offsets": { + "from": 19980, + "to": 20340 + }, + "id": 1295, + "p": 0.853392, + "t_dtw": -1 + }, + { + "text": "holders", + "timestamps": { + "from": "00:00:20.340", + "to": "00:00:20.850" + }, + "offsets": { + "from": 20340, + "to": 20850 + }, + "id": 10476, + "p": 0.982725, + "t_dtw": -1 + }, + { + "text": ".", + "timestamps": { + "from": "00:00:20.850", + "to": "00:00:21.120" + }, + "offsets": { + "from": 20850, + "to": 21120 + }, + "id": 13, + "p": 0.942825, + "t_dtw": -1 + }, + { + "text": "[_TT_1056]", + "timestamps": { + "from": "00:00:21.120", + "to": "00:00:21.120" + }, + "offsets": { + "from": 21120, + "to": 21120 + }, + "id": 51419, + "p": 0.0691018, + "t_dtw": -1 + } + ] + }, + { + "timestamps": { + "from": "00:00:21.120", + "to": "00:00:24.520" + }, + "offsets": { + "from": 21120, + "to": 24520 + }, + "text": " Tucks of future homeworks are subject to change.", + "tokens": [ + { + "text": " T", + "timestamps": { + "from": "00:00:21.120", + "to": "00:00:21.200" + }, + "offsets": { + "from": 21120, + "to": 21200 + }, + "id": 309, + "p": 0.433385, + "t_dtw": -1 + }, + { + "text": "ucks", + "timestamps": { + "from": "00:00:21.200", + "to": "00:00:21.530" + }, + "offsets": { + "from": 21200, + "to": 21530 + }, + "id": 6238, + "p": 0.424658, + "t_dtw": -1 + }, + { + "text": " of", + "timestamps": { + "from": "00:00:21.530", + "to": "00:00:21.690" + }, + "offsets": { + "from": 21530, + "to": 21690 + }, + "id": 286, + "p": 0.985274, + "t_dtw": -1 + }, + { + "text": " future", + "timestamps": { + "from": "00:00:21.690", + "to": "00:00:22.180" + }, + "offsets": { + "from": 21690, + "to": 22180 + }, + "id": 2003, + "p": 0.997737, + "t_dtw": -1 + }, + { + "text": " hom", + "timestamps": { + "from": "00:00:22.180", + "to": "00:00:22.420" + }, + "offsets": { + "from": 22180, + "to": 22420 + }, + "id": 3488, + "p": 0.975576, + "t_dtw": -1 + }, + { + "text": "eworks", + "timestamps": { + "from": "00:00:22.420", + "to": "00:00:22.910" + }, + "offsets": { + "from": 22420, + "to": 22910 + }, + "id": 19653, + "p": 0.993154, + "t_dtw": -1 + }, + { + "text": " are", + "timestamps": { + "from": "00:00:22.910", + "to": "00:00:23.150" + }, + "offsets": { + "from": 22910, + "to": 23150 + }, + "id": 389, + "p": 0.998262, + "t_dtw": -1 + }, + { + "text": " subject", + "timestamps": { + "from": "00:00:23.150", + "to": "00:00:23.730" + }, + "offsets": { + "from": 23150, + "to": 23730 + }, + "id": 2426, + "p": 0.991766, + "t_dtw": -1 + }, + { + "text": " to", + "timestamps": { + "from": "00:00:23.730", + "to": "00:00:23.890" + }, + "offsets": { + "from": 23730, + "to": 23890 + }, + "id": 284, + "p": 0.908062, + "t_dtw": -1 + }, + { + "text": " change", + "timestamps": { + "from": "00:00:23.890", + "to": "00:00:24.440" + }, + "offsets": { + "from": 23890, + "to": 24440 + }, + "id": 1487, + "p": 0.997778, + "t_dtw": -1 + }, + { + "text": ".", + "timestamps": { + "from": "00:00:24.440", + "to": "00:00:24.520" + }, + "offsets": { + "from": 24440, + "to": 24520 + }, + "id": 13, + "p": 0.981104, + "t_dtw": -1 + }, + { + "text": "[_TT_1226]", + "timestamps": { + "from": "00:00:24.520", + "to": "00:00:24.520" + }, + "offsets": { + "from": 24520, + "to": 24520 + }, + "id": 51589, + "p": 0.0327583, + "t_dtw": -1 + } + ] + } + ] +} \ No newline at end of file diff --git a/TaskEngine/Tasks/LocalTranscriptionTask.cs b/TaskEngine/Tasks/LocalTranscriptionTask.cs index aca11f56..4e5a948b 100644 --- a/TaskEngine/Tasks/LocalTranscriptionTask.cs +++ b/TaskEngine/Tasks/LocalTranscriptionTask.cs @@ -129,11 +129,21 @@ protected async override Task OnConsume(string videoId, TaskParameters taskParam int cueCount = 0; foreach (var jsonCue in theCaptionsAsJson) { + // var caption = new Caption() { + // Index = cueCount ++, + // Begin = TimeSpan.Parse(jsonCue["timestamps"]["from"].ToString(Newtonsoft.Json.Formatting.None).Replace(",",".")), + // End = TimeSpan.Parse(jsonCue["timestamps"]["to"].ToString(Newtonsoft.Json.Formatting.None).Replace(",",".")) , + // Text = jsonCue["text"] .ToString(Newtonsoft.Json.Formatting.None).Trim() + // }; + var fromTimestamp = jsonCue["timestamps"]["from"].ToString().Replace(",", "."); + var toTimestamp = jsonCue["timestamps"]["to"].ToString().Replace(",", "."); + + // Parse the timestamps directly var caption = new Caption() { - Index = cueCount ++, - Begin = TimeSpan.Parse(jsonCue["timestamps"]["from"].ToString(Newtonsoft.Json.Formatting.None).Replace(",",".")), - End = TimeSpan.Parse(jsonCue["timestamps"]["to"].ToString(Newtonsoft.Json.Formatting.None).Replace(",",".")) , - Text = jsonCue["text"] .ToString(Newtonsoft.Json.Formatting.None).Trim() + Index = cueCount++, + Begin = TimeSpan.Parse(fromTimestamp), // Expecting "HH:mm:ss.fff" + End = TimeSpan.Parse(toTimestamp), // Expecting "HH:mm:ss.fff" + Text = jsonCue["text"].ToString().Trim() }; theCaptions.Add(caption); From 5cdc50eaed8922d98432f6558273dcbe2c57d83f Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 16 Oct 2024 16:52:35 -0500 Subject: [PATCH 20/23] Updates and Testing --- ClassTranscribeDatabase/CommonUtils.cs | 6 ++- .../Controllers/PlaylistsController.cs | 4 +- ClassTranscribeServer/Utils/WakeDownloader.cs | 2 +- PythonRpcServer/server.py | 2 +- TaskEngine/Program.cs | 37 ++++++++++--------- TaskEngine/Tasks/AzureTranscriptionTask.cs | 2 +- TaskEngine/Tasks/LocalTranscriptionTask.cs | 11 +++--- TaskEngine/Tasks/QueueAwakerTask.cs | 2 +- pythonrpcserver.Dockerfile | 2 +- 9 files changed, 36 insertions(+), 32 deletions(-) diff --git a/ClassTranscribeDatabase/CommonUtils.cs b/ClassTranscribeDatabase/CommonUtils.cs index 1e178e23..4713fd64 100644 --- a/ClassTranscribeDatabase/CommonUtils.cs +++ b/ClassTranscribeDatabase/CommonUtils.cs @@ -23,7 +23,7 @@ public enum TaskType DownloadPlaylistInfo = 3, DownloadMedia = 4, ConvertMedia = 5, - TranscribeVideo = 6, + // TranscribeVideo = 6, ProcessVideo = 7, Aggregator = 8, GenerateVTTFile = 9, @@ -39,7 +39,9 @@ public enum TaskType PythonCrawler = 19, DescribeVideo = 20, - DescribeImage = 21 + DescribeImage = 21, + AzureTranscribeVideo = 22, + LocalTranscribeVideo = 23 } diff --git a/ClassTranscribeServer/Controllers/PlaylistsController.cs b/ClassTranscribeServer/Controllers/PlaylistsController.cs index 118ad68c..e8083935 100644 --- a/ClassTranscribeServer/Controllers/PlaylistsController.cs +++ b/ClassTranscribeServer/Controllers/PlaylistsController.cs @@ -170,7 +170,7 @@ public async Task>> GetPlaylists2(string o JsonMetadata = m.JsonMetadata, CreatedAt = m.CreatedAt, SceneDetectReady = m.Video.HasSceneObjectData(), - Ready = m.Video != null && "NoError" == m.Video.TranscriptionStatus , + Ready = m.Video != null && Video.TranscriptionStatusMessages.NOERROR == m.Video.TranscriptionStatus , SourceType = m.SourceType, Duration = m.Video?.Duration, PublishStatus = m.PublishStatus, @@ -265,7 +265,7 @@ public async Task> GetPlaylist(string id) PublishStatus = m.PublishStatus, Options = m.GetOptionsAsJson(), SceneDetectReady = m.Video != null && m.Video.HasSceneObjectData(), - Ready = m.Video != null && "NoError" == m.Video.TranscriptionStatus , + Ready = m.Video != null && Video.TranscriptionStatusMessages.NOERROR == m.Video.TranscriptionStatus , Video = m.Video == null ? null : new VideoDTO { Id = m.Video.Id, diff --git a/ClassTranscribeServer/Utils/WakeDownloader.cs b/ClassTranscribeServer/Utils/WakeDownloader.cs index b2c939a7..22f6479a 100644 --- a/ClassTranscribeServer/Utils/WakeDownloader.cs +++ b/ClassTranscribeServer/Utils/WakeDownloader.cs @@ -104,7 +104,7 @@ public void TranscribeVideo(string videoOrMediaId, bool deleteExisting) { JObject msg = new JObject { - { "Type", TaskType.TranscribeVideo.ToString() }, + { "Type", TaskType.LocalTranscribeVideo.ToString() }, { "videoOrMediaId", videoOrMediaId }, { "DeleteExisting", deleteExisting } }; diff --git a/PythonRpcServer/server.py b/PythonRpcServer/server.py index 9934151e..c568d32c 100644 --- a/PythonRpcServer/server.py +++ b/PythonRpcServer/server.py @@ -152,7 +152,7 @@ def serve(): # Until we can ensure no timeouts on remote services, the default here is set to a conservative low number # This is to ensure we can still make progress even if every python tasks tries to use all cpu cores. max_workers=int(os.getenv('NUM_PYTHON_WORKERS', 3)) - print(f"max_workers={max_workers}") + print(f"max_workers={max_workers}. Starting up grpc server...") server = grpc.server(futures.ThreadPoolExecutor(max_workers=max_workers)) diff --git a/TaskEngine/Program.cs b/TaskEngine/Program.cs index 1aa1977e..d8210111 100644 --- a/TaskEngine/Program.cs +++ b/TaskEngine/Program.cs @@ -139,30 +139,31 @@ static void runQueueAwakerForever() { // TODO/REVIEW: However some tasks also publish the next items while (true) { - // try { - // _logger.LogInformation("Periodic Check"); - // queueAwakerTask.Publish(new JObject - // { - // { "Type", TaskType.PeriodicCheck.ToString() } - // }); - // } catch (Exception e) { - // _logger.LogError(e, "Error in Periodic Check"); - // } - try { - var videoId = "ddceb720-a9d6-417d-b5ea-e94c6c0a86c6"; - _logger.LogInformation("Transcription Task Initiated"); + _logger.LogInformation("Periodic Check"); queueAwakerTask.Publish(new JObject { - { "Type", TaskType.TranscribeVideo.ToString() }, - { "videoOrMediaId", videoId } + { "Type", TaskType.PeriodicCheck.ToString() } }); - - _logger.LogInformation("Transcription Task Published Successfully"); } catch (Exception e) { - _logger.LogError(e, "Error in Transcription Task"); + _logger.LogError(e, "Error in Periodic Check"); } + // Hacky testing... + // try { + // var videoId = "ddceb720-a9d6-417d-b5ea-e94c6c0a86c6"; + // _logger.LogInformation("Transcription Task Initiated"); + // queueAwakerTask.Publish(new JObject + // { + // { "Type", TaskType.LocalTranscribeVideo.ToString() }, + // { "videoOrMediaId", videoId } + // }); + + // _logger.LogInformation("Transcription Task Published Successfully"); + // } catch (Exception e) { + // _logger.LogError(e, "Error in Transcription Task"); + // } + // Thread.Sleep(timeInterval); Task.Delay(timeInterval).Wait(); @@ -208,7 +209,7 @@ static void createTaskQueues() { _serviceProvider.GetService().Consume(DISABLED_TASK); // We dont want concurrency for these tasks - _logger.LogInformation("Creating QueueAwakerTask and Box token tasks consumers."); + _logger.LogInformation("Creating QueueAwakerTask and Box token tasks consumers!"); _serviceProvider.GetService().Consume(NO_CONCURRENCY); //TODO TOREVIEW: NO_CONCURRENCY? // does nothing at the moment _serviceProvider.GetService().Consume(NO_CONCURRENCY); _serviceProvider.GetService().Consume(NO_CONCURRENCY); // calls _box.CreateAccessTokenAsync(authCode); diff --git a/TaskEngine/Tasks/AzureTranscriptionTask.cs b/TaskEngine/Tasks/AzureTranscriptionTask.cs index 6bf250ad..420cfc94 100644 --- a/TaskEngine/Tasks/AzureTranscriptionTask.cs +++ b/TaskEngine/Tasks/AzureTranscriptionTask.cs @@ -32,7 +32,7 @@ class AzureTranscriptionTask : RabbitMQTask public AzureTranscriptionTask(RabbitMQConnection rabbitMQ, MSTranscriptionService msTranscriptionService, // GenerateVTTFileTask generateVTTFileTask, ILogger logger, CaptionQueries captionQueries) - : base(rabbitMQ, TaskType.TranscribeVideo, logger) + : base(rabbitMQ, TaskType.AzureTranscribeVideo, logger) { _msTranscriptionService = msTranscriptionService; // nope _generateVTTFileTask = generateVTTFileTask; diff --git a/TaskEngine/Tasks/LocalTranscriptionTask.cs b/TaskEngine/Tasks/LocalTranscriptionTask.cs index 4e5a948b..b20ee82b 100644 --- a/TaskEngine/Tasks/LocalTranscriptionTask.cs +++ b/TaskEngine/Tasks/LocalTranscriptionTask.cs @@ -36,7 +36,7 @@ public LocalTranscriptionTask(RabbitMQConnection rabbitMQ, RpcClient rpcClient, // GenerateVTTFileTask generateVTTFileTask, ILogger logger, CaptionQueries captionQueries) - : base(rabbitMQ, TaskType.TranscribeVideo, logger) + : base(rabbitMQ, TaskType.LocalTranscribeVideo, logger) { _rpcClient = rpcClient; _captionQueries = captionQueries; @@ -90,8 +90,9 @@ protected async override Task OnConsume(string videoId, TaskParameters taskParam GetLogger().LogInformation($"{videoId}: Updated TranscribingAttempts = {video.TranscribingAttempts}"); try { + var mockWhisperResult = Globals.appSettings.MOCK_RECOGNITION == "MOCK"; - GetLogger().LogInformation($"{videoId}: Calling RecognitionWithVideoStreamAsync"); + GetLogger().LogInformation($"{videoId}: Calling RecognitionWithVideoStreamAsync( mock={mockWhisperResult})"); var request = new CTGrpc.TranscriptionRequest { @@ -99,7 +100,7 @@ protected async override Task OnConsume(string videoId, TaskParameters taskParam FilePath = video.Video1.VMPath, Model = "en", Language = "en", - Testing = true + Testing = mockWhisperResult // PhraseHints = phraseHints, // CourseHints = "", // OutputLanguages = "en" @@ -161,7 +162,7 @@ protected async override Task OnConsume(string videoId, TaskParameters taskParam { TranscriptionType = TranscriptionType.Caption, Captions = theCaptions, - Language = theLanguage, + Language = "en-US" , /* Must be en-US for FrontEnd; Cant be just "en" */ VideoId = video.Id, Label = $"{theLanguage} (ClassTranscribe)", SourceInternalRef = SOURCEINTERNALREF, // @@ -177,7 +178,7 @@ protected async override Task OnConsume(string videoId, TaskParameters taskParam } - video.TranscriptionStatus = "NoError"; + video.TranscriptionStatus = Video.TranscriptionStatusMessages.NOERROR; // video.JsonMetadata["LastSuccessfulTime"] = result.LastSuccessTime.ToString(); GetLogger().LogInformation($"{videoId}: Saving captions"); diff --git a/TaskEngine/Tasks/QueueAwakerTask.cs b/TaskEngine/Tasks/QueueAwakerTask.cs index 0fb5fe45..78c21704 100644 --- a/TaskEngine/Tasks/QueueAwakerTask.cs +++ b/TaskEngine/Tasks/QueueAwakerTask.cs @@ -401,7 +401,7 @@ protected async override Task OnConsume(JObject jObject, TaskParameters taskPara var sourceId = jObject["SourceId"].ToString(); _pythonCrawlerTask.Publish(sourceId); } - else if (type == TaskType.TranscribeVideo.ToString()) + else if (type == TaskType.LocalTranscribeVideo.ToString()) { var id = jObject["videoOrMediaId"].ToString(); diff --git a/pythonrpcserver.Dockerfile b/pythonrpcserver.Dockerfile index a2905617..dc6061fe 100644 --- a/pythonrpcserver.Dockerfile +++ b/pythonrpcserver.Dockerfile @@ -31,7 +31,7 @@ RUN python -m grpc_tools.protoc -I . --python_out=./ --grpc_python_out=./ ct.proto COPY ./PythonRpcServer . - +# The output of this file is used when we set MOCK_RECOGNITION=MOCK for quick testing RUN whisper -ojf -f transcribe_hellohellohello.wav CMD [ "nice", "-n", "18", "ionice", "-c", "2", "-n", "6", "python3", "-u", "/PythonRpcServer/server.py" ] From 46516fce8f82f06ac99a65e85b14fb195104278d Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 23 Oct 2024 16:00:00 -0500 Subject: [PATCH 21/23] Initial work on yt-dlp --- PythonRpcServer/.gitignore | 1 + PythonRpcServer/requirements.txt | 5 ++- PythonRpcServer/youtube.py | 75 ++++++++++++++++++++++---------- PythonRpcServer/youtube_test.py | 7 ++- pythonrpcserver.Dockerfile | 8 +++- 5 files changed, 67 insertions(+), 29 deletions(-) create mode 100644 PythonRpcServer/.gitignore diff --git a/PythonRpcServer/.gitignore b/PythonRpcServer/.gitignore new file mode 100644 index 00000000..f7275bbb --- /dev/null +++ b/PythonRpcServer/.gitignore @@ -0,0 +1 @@ +venv/ diff --git a/PythonRpcServer/requirements.txt b/PythonRpcServer/requirements.txt index a95812ba..8e8d7258 100644 --- a/PythonRpcServer/requirements.txt +++ b/PythonRpcServer/requirements.txt @@ -32,8 +32,9 @@ wcwidth==0.2.13 # Not versioned numpy -pytube # if not available, use the tar.gz package (see Dockerfile) - +# No longer maintained pytube # if not available, use the tar.gz package (see Dockerfile) +yt-dlp +#Always get latest # protobuf version 3.18.3 causes NotImplementedError("To be implemented") in PythonRpcServer/mediaprovider.py # Likely need to coordinate updating the C# version too diff --git a/PythonRpcServer/youtube.py b/PythonRpcServer/youtube.py index 45841871..ca69b7d1 100644 --- a/PythonRpcServer/youtube.py +++ b/PythonRpcServer/youtube.py @@ -1,12 +1,17 @@ -from pytube.extract import playlist_id +# from pytube.extract import playlist_id + +# from yt_dlp import YoutubeDL +import yt_dlp + import requests -from utils import encode, decode, getRandomString, download_file +from utils import getRandomString import os import json from time import perf_counter +import datetime #from pytube import YouTube -import pytube +# import pytube from mediaprovider import MediaProvider, InvalidPlaylistInfoException @@ -42,7 +47,10 @@ def get_youtube_channel(self, identifier): print(f'get_youtube_channel({identifier})') url = YOUTUBE_CHANNEL_BASE_URL+ identifier - channel = pytube.Channel(url) + # Use yt_dlp to create a channel, + + channel = yt_dlp.Youtube(url).get_channel() + ## channel.playlist_id = channel.playlist_id.replace('UC', 'UU') playlist_id = channel.playlist_id #according to one StackOver and one test, channels-to-playlists can also be converted with string replace UCXXXX to UUXXXX @@ -53,26 +61,33 @@ def get_youtube_playlist(self, identifier): try: start_time = perf_counter() - url= YOUTUBE_PLAYLIST_BASE_URL+ identifier + url= YOUTUBE_PLAYLIST_BASE_URL + identifier print(f"get_youtube_playlist(identifier): {url}") - playlist = pytube.Playlist(url) - + + ydl_opts = { + 'quiet': True, + 'extract_flat': 'in_playlist', # Ensure we are extracting playlist entries + 'force_generic_extractor': True, + } medias = [] - for v in playlist.videos: - - published_at = v.publish_date.strftime('%Y/%m/%d') - media = { - #"channelTitle": channelTitle, - "channelId": v.channel_id, - "playlistId": identifier, - "title": v.title, - "description": v.description, - "publishedAt": published_at, - "videoUrl": v.watch_url, - "videoId": v.video_id, - "createdAt": published_at - } - medias.append(media) + # Current time in YYYYMMDD format + now = datetime.datetime.now().strftime('%Y%m%d') + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info_dict = ydl.extract_info(url, download=False) + for entry in info_dict.get( 'entries', []): + print(entry) + published_at = entry.get('upload_date', now) + media = { + "channelId": entry['channel_id'], + "playlistId": identifier, + "title": entry['title'], + "description": entry['description'], + "publishedAt": published_at, + "videoUrl": "https://youtube.com/watch?v="+entry['id'], + "videoId": entry['id'], + "createdAt": published_at + } + medias.append(media) end_time = perf_counter() print(f'Youtube playlist {identifier}: Returning {len(medias)} items. Processing time {end_time - start_time :.2f} seconds') return medias @@ -86,7 +101,21 @@ def download_youtube_video(self, youtubeUrl): start_time = perf_counter() extension = '.mp4' filename = getRandomString(8) - filepath = pytube.YouTube(youtubeUrl).streams.filter(subtype='mp4').get_highest_resolution().download(output_path = DATA_DIRECTORY, filename = filename) + filepath =f'{DATA_DIRECTORY}/{filename}' + ydl_opts = { + 'quiet': True, + 'format': 'best[ext=mp4]', + 'outtmpl': filepath, + 'cachedir' : False, + 'progress_hooks': [], + 'call_home': False, + 'no_color': True, + 'noprogress': True, + } + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + x = ydl.download([youtubeUrl]) + print(x) + #filepath = yt_dlp.YoutubeDL(ydl_opts).streams.filter(subtype='mp4').get_highest_resolution().download(output_path = DATA_DIRECTORY, filename = filename) end_time = perf_counter() print(f"download_youtube_video({youtubeUrl}): Done. Downloaded in {end_time - start_time :.2f} seconds") return filepath, extension diff --git a/PythonRpcServer/youtube_test.py b/PythonRpcServer/youtube_test.py index a7ddf125..a838004d 100644 --- a/PythonRpcServer/youtube_test.py +++ b/PythonRpcServer/youtube_test.py @@ -5,7 +5,7 @@ import youtube -def test_youtube(): +def test_youtube1(): print("Test 1/2: Download playlist") yt=youtube.YoutubeProvider() pl=yt.get_youtube_playlist('PLBgxzZMu3GpPb35BDIU5eeopR4MhBOZw_') @@ -17,7 +17,9 @@ def test_youtube(): assert 'STAT 385' in pl[0]['title'] +def test_youtube2(): print("Test 2/2: Download video") + yt=youtube.YoutubeProvider() onevid = yt.download_youtube_video('https://youtube.com/watch?v=DqHMh8nqCPw') # 24-72 seconds typical print(onevid) assert len(onevid) == 2 @@ -34,4 +36,5 @@ def test_youtube(): print("All tests completed") if __name__ == "__main__": - test_youtube() + test_youtube1() + test_youtube2() diff --git a/pythonrpcserver.Dockerfile b/pythonrpcserver.Dockerfile index dc6061fe..c3e6498b 100644 --- a/pythonrpcserver.Dockerfile +++ b/pythonrpcserver.Dockerfile @@ -23,6 +23,11 @@ COPY --from=whisperbuild /whisper.cpp/models /PythonRpcServer/models WORKDIR /PythonRpcServer + # Don't copy any py files here, so that we don't need to re-run whisper + COPY ./PythonRpcServer/transcribe_hellohellohello.wav . + # The output of tis whisper run is used when we set MOCK_RECOGNITION=MOCK for quick testing + RUN whisper -ojf -f transcribe_hellohellohello.wav + COPY ./PythonRpcServer/requirements.txt requirements.txt RUN pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir -r requirements.txt @@ -31,8 +36,7 @@ RUN python -m grpc_tools.protoc -I . --python_out=./ --grpc_python_out=./ ct.proto COPY ./PythonRpcServer . -# The output of this file is used when we set MOCK_RECOGNITION=MOCK for quick testing - RUN whisper -ojf -f transcribe_hellohellohello.wav + CMD [ "nice", "-n", "18", "ionice", "-c", "2", "-n", "6", "python3", "-u", "/PythonRpcServer/server.py" ] From 8e18b699d4662ec026224a2cb47641b31c9e0677 Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 23 Oct 2024 16:30:40 -0500 Subject: [PATCH 22/23] Fix creation date --- PythonRpcServer/youtube.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PythonRpcServer/youtube.py b/PythonRpcServer/youtube.py index ca69b7d1..dec0fa7f 100644 --- a/PythonRpcServer/youtube.py +++ b/PythonRpcServer/youtube.py @@ -70,8 +70,8 @@ def get_youtube_playlist(self, identifier): 'force_generic_extractor': True, } medias = [] - # Current time in YYYYMMDD format - now = datetime.datetime.now().strftime('%Y%m%d') + # Current time in iso date time format + now = datetime.datetime.now().isoformat() with yt_dlp.YoutubeDL(ydl_opts) as ydl: info_dict = ydl.extract_info(url, download=False) for entry in info_dict.get( 'entries', []): From 905ab4c91d95e0edf151c1ea4783ce1bfbfa80e4 Mon Sep 17 00:00:00 2001 From: Lawrence Angrave Date: Wed, 23 Oct 2024 16:35:07 -0500 Subject: [PATCH 23/23] Remove whisper submodule --- whisper.cpp | 1 - 1 file changed, 1 deletion(-) delete mode 160000 whisper.cpp diff --git a/whisper.cpp b/whisper.cpp deleted file mode 160000 index 5236f027..00000000 --- a/whisper.cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5236f0278420ab776d1787c4330678d80219b4b6