Initial commit

This commit is contained in:
pluja
2023-08-26 10:36:05 +02:00
commit a3543ee13a
76 changed files with 6165 additions and 0 deletions

31
backend/Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM --platform=$BUILDPLATFORM golang:bookworm AS ytdlp_cache
ARG TARGETOS
ARG TARGETARCH
RUN apt update && apt install -y wget
RUN wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp
RUN chmod a+rx /usr/local/bin/yt-dlp
FROM devopsworks/golang-upx:latest as builder
ENV DEBIAN_FRONTEND noninteractive
WORKDIR /app
COPY . .
RUN go mod tidy
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o whishper . && \
upx whishper
RUN chmod a+rx whishper
FROM --platform=$BUILDPLATFORM python:3-bookworm
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY --from=builder /app/whishper ./whishper
RUN chmod a+rx ./whishper
COPY --from=ytdlp_cache /usr/local/bin/yt-dlp /bin/yt-dlp
RUN mkdir /app/uploads
EXPOSE 8080
ENTRYPOINT [ "/app/whishper", "-addr", ":8080", "-dev" ]

66
backend/README.md Normal file
View File

@@ -0,0 +1,66 @@
# Golang API for Whishper
This is the Golang API for Whishper. It allows to have an interface between the Faster-Whisper-ASR service, the database and the web interface.
## Usage
### Websocket
It exposes a `/ws/transcriptions` websocket endpoint where JSON events will be received. This endpoint only receives updates, it will not send all the transcriptions in the database to the client when it connects. The websocket will also ignore all the events from the clients.
### REST API
#### GET: `/api/transcriptions`
This endpoint will return all the transcriptions in the database.
#### POST: `/api/transcriptions`
This endpoint expects a form with the following fields:
- `file` (form file): The file to transcribe (must be present, if `sourceURL` is not present)
- `sourceURL` (string): The URL of the file to transcribe (optional, if present, `file` will be ignored)
- `modelSize` (string): The model size to use (optional, if not present, the default model size will be used). The available model sizes are: `tiny`, `base`, `small`, `medium`, `large`. All variants of the model size are also available with enlgish-only models (e.g. `tiny.en`, `base.en`, etc.)
- `language` (string): The source language for the transcription. By default it uses `auto` which will detect the language automatically. Otherwise, use a two-letter language code (e.g. `en`, `fr`, `es`, etc.)
### Flags
- `-addr`: The address to listen to (default: `:8080`). Must specify the `:` before the port number.
- `-updir`: The path to the uploads directory (default: `/app/uploads`). Must exist and be writable.
- `-asr`: The address of the ASR service (default: `whisper-api:8000`).
- `-translation`: The address of the translation service (default: `translate:5000`).
- `-dev`: Turns development mode on. This will show debug logs.
## Project structure
# `main.go`
This is the main file of the project. It contains the main function calls.
# `api/`
This folder contains all the server logic. It is split into three files:
- `server.go`: This file contains the main server logic. It creates a server struct that contains all the necessary logic to run the server.
- `handlers.go`: This file contains all the handlers for the server. It also contains the logic.
- `websocket.go`: This file contains the logic for the websocket.
# `models/`
This folder contains all the models used by the server. Each model has its own file.
# `utils/`
This folder contains all the utility functions used by the server.
# `database/`
This folder contains all the database logic. It is split into two files:
- `database.go`: This file contains the main database logic. It creates a database interface that contains all the necessary logic to interact with the database.
- `mongo.go`: This implements the database interface for MongoDB.
# `monitor/`
This folder contains the logic for the background monitor that checks the pending transcriptions, transcribes them and updates the database.

190
backend/api/handlers.go Normal file
View File

@@ -0,0 +1,190 @@
package api
import (
"fmt"
"os"
"time"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog/log"
"codeberg.org/pluja/whishper/models"
)
func (s *Server) handleGetAllTranscriptions(c *fiber.Ctx) error {
transcriptions := s.Db.GetAllTranscriptions()
// Convert the transcriptions to JSON.
json, err := json.Marshal(transcriptions)
if err != nil {
// 503 On vacation!
return fiber.NewError(fiber.StatusServiceUnavailable, "On vacation!")
}
// Write the JSON to the response body.
c.Set("Content-Type", "application/json")
c.Write(json)
return nil
}
func (s *Server) handleGetTranscriptionById(c *fiber.Ctx) error {
id := c.Params("id")
t := s.Db.GetTranscription(id)
if t == nil {
log.Warn().Msgf("Transcription with id %v not found", id)
return fiber.NewError(fiber.StatusNotFound, "Not found")
}
// Convert the transcription to JSON.
json, err := json.Marshal(t)
if err != nil {
// 503 On vacation!
return fiber.NewError(fiber.StatusServiceUnavailable, "On vacation!")
}
// Write the JSON to the response body.
c.Set("Content-Type", "application/json")
c.Write(json)
return nil
}
// This function receives data from a form to create a new transcription.
// If the transcription is created successfully, it returns a 201 Created status code and
// broadcasts the new transcription to all ws clients.
func (s *Server) handlePostTranscription(c *fiber.Ctx) error {
log.Debug().Msg("POST /api/transcriptions")
var transcription models.Transcription
// we get the filename from the from
var filename string
if c.FormValue("sourceUrl") == "" {
// Get the form file from the request.
file, err := c.FormFile("file")
if err != nil {
log.Error().Err(err).Msg("Error getting file field from the form")
return fiber.NewError(fiber.StatusBadRequest, "Bad request")
}
timeid := time.Now().Format("2006_01_02-150405000")
filename = timeid + models.FileNameSeparator + file.Filename
// if it's empty and there is no sourceurl we set a timestamp-based filename
if filename == timeid+models.FileNameSeparator {
filename = timeid + models.FileNameSeparator + time.Now().Format("2006_01_02-150405")
}
// Save the file to the uploads directory.
err = c.SaveFile(file, fmt.Sprintf("%v/%v", os.Getenv("UPLOAD_DIR"), filename))
if err != nil {
log.Error().Err(err).Msgf("Error saving the form file to disk into %v", os.Getenv("UPLOAD_DIR"))
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
}
}
// Parse the body into the transcription struct.
transcription.Language = c.FormValue("language")
transcription.ModelSize = c.FormValue("modelSize")
transcription.FileName = filename
transcription.Status = models.TranscriptionStatusPending
transcription.Task = "transcribe"
transcription.SourceUrl = c.FormValue("sourceUrl")
// TODO: Implement other device types
transcription.Device = "cpu"
log.Debug().Msgf("Transcription: %+v", transcription)
// Save transcription to database
res, err := s.Db.NewTranscription(&transcription)
if err != nil {
log.Error().Err(err).Msg("Error saving transcription to database")
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
}
// Write the JSON to the response body.
s.BroadcastTranscription(res)
s.NewTranscriptionCh <- true
return nil
}
func (s *Server) handleDeleteTranscription(c *fiber.Ctx) error {
// First get the transcription from the database
id := c.Params("id")
t := s.Db.GetTranscription(id)
if t == nil {
log.Warn().Msgf("Transcription with id %v not found", id)
return fiber.NewError(fiber.StatusNotFound, "Not found")
}
// Then delete the file from disk
err := os.Remove(fmt.Sprintf("%v/%v", os.Getenv("UPLOAD_DIR"), t.FileName))
if err != nil {
log.Error().Err(err).Msgf("Error deleting file %v", t.FileName)
}
// Finally delete the transcription from the database
err = s.Db.DeleteTranscription(id)
if err != nil {
log.Error().Err(err).Msgf("Error deleting transcription %v", id)
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
}
// Return status deleted
c.Status(fiber.StatusOK)
return nil
}
func (s *Server) handlePatchTranscription(c *fiber.Ctx) error {
var transcription models.Transcription
// Parse the body into the transcription struct.
err := json.Unmarshal(c.Body(), &transcription)
if err != nil {
log.Error().Err(err).Msg("Error parsing JSON body")
return fiber.NewError(fiber.StatusBadRequest, "Bad request")
}
// Update the transcription in the database
ut, err := s.Db.UpdateTranscription(&transcription)
if err != nil {
log.Error().Err(err).Msgf("Error updating transcription")
if err.Error() == "no documents were modified" {
return fiber.NewError(fiber.StatusNotModified, "Not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
}
// Write the JSON to the response body.
s.BroadcastTranscription(ut)
// Return status ok
json, err := json.Marshal(&ut)
if err != nil {
// 503 On vacation!
return fiber.NewError(fiber.StatusInternalServerError, "Error parsing json!")
}
c.Status(fiber.StatusOK)
c.Write(json)
return nil
}
func (s *Server) handleTranslate(c *fiber.Ctx) error {
id := c.Params("id")
targetLang := c.Params("target")
transcription := s.Db.GetTranscription(id)
// Set status as translating
transcription.Status = models.TrannscriptionStatusTranslating
s.Db.UpdateTranscription(transcription)
s.BroadcastTranscription(transcription)
err := transcription.Translate(targetLang)
if err != nil {
log.Debug().Err(err).Msg("Error with translation")
return err
}
// Set as done
transcription.Status = models.TranscriptionStatusDone
s.Db.UpdateTranscription(transcription)
s.BroadcastTranscription(transcription)
return nil
}

158
backend/api/server.go Normal file
View File

@@ -0,0 +1,158 @@
package api
import (
"os"
"github.com/goccy/go-json"
"github.com/gofiber/contrib/websocket"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/rs/zerolog/log"
"codeberg.org/pluja/whishper/database"
"codeberg.org/pluja/whishper/models"
)
type Server struct {
ListenAddr string
Router *fiber.App
Db database.Db
NewTranscriptionCh chan bool
clients []*websocket.Conn
}
func NewServer(listenAddr string, db database.Db) *Server {
return &Server{
ListenAddr: listenAddr,
Router: fiber.New(fiber.Config{
JSONEncoder: json.Marshal,
JSONDecoder: json.Unmarshal,
BodyLimit: 100 * 1024 * 1024, // Increase body limit to 100MB
ServerHeader: "Fiber", // Optional, for easier debugging
}),
Db: db,
clients: make([]*websocket.Conn, 0),
NewTranscriptionCh: make(chan bool, 100),
}
}
func (s *Server) Run() {
s.SetupWebsocket()
s.SetupMiddleware()
s.RegisterRoutes()
s.Router.Listen(s.ListenAddr)
}
func (s *Server) SetupWebsocket() {
s.Router.Get("/ws/transcriptions", websocket.New(func(c *websocket.Conn) {
// Add this connection to the slice of clients
s.clients = append(s.clients, c)
for {
_, msg, err := c.ReadMessage()
if err != nil {
// Check for normal close error (1000) or going away error (1001)
if err.Error() != "websocket: close 1000 (normal)" &&
err.Error() != "websocket: close 1001 (going away)" {
log.Error().Err(err).Msgf("Error reading message")
}
// Remove the client from the slice if it has disconnected
s.clients = removeWsClient(s.clients, c)
return
}
s.handleWebsocketMessage(c, msg)
}
}))
}
func (s *Server) BroadcastTranscription(t *models.Transcription) {
// Convert the transcription to JSON.
json, err := json.Marshal(&t)
if err != nil {
log.Error().Err(err).Msg("Error marshalling transcription to JSON:")
return
}
for _, client := range s.clients {
if err := client.WriteMessage(websocket.TextMessage, json); err != nil {
log.Error().Err(err).Msg("Error broadcasting message:")
}
}
}
func (s *Server) SetupMiddleware() {
s.Router.Use(cors.New())
}
func (s *Server) RegisterRoutes() {
// Static routes
s.Router.Static("/api/video", os.Getenv("UPLOAD_DIR"))
// Register HTTP route for getting initial state.
s.Router.Get("/api/transcriptions", func(c *fiber.Ctx) error {
err := s.handleGetAllTranscriptions(c)
if err != nil {
log.Error().Err(err).Msg("Error handling POST /api/transcriptions")
}
return err
})
// Register HTTP route for getting initial state.
s.Router.Get("/api/transcriptions/:id", func(c *fiber.Ctx) error {
log.Debug().Msgf("GET /api/transcriptions/%v", c.Params("id"))
err := s.handleGetTranscriptionById(c)
if err != nil {
log.Error().Err(err).Msg("Error handling GET /api/transcriptions/:id")
}
return err
})
// Register HTTP route for getting initial state.
s.Router.Get("/api/translate/:id/:target", func(c *fiber.Ctx) error {
log.Debug().Msgf("GET /api/translate/%v/%v", c.Params("id"), c.Params("target"))
err := s.handleTranslate(c)
if err != nil {
log.Error().Err(err).Msg("Error handling GET /api/translate/:id/:source")
}
return err
})
// Register HTTP route for receiving the form data and creating new transcription job.
s.Router.Post("/api/transcriptions", func(c *fiber.Ctx) error {
log.Debug().Msg("POST /api/transcriptions")
err := s.handlePostTranscription(c)
if err != nil {
log.Error().Err(err).Msg("Error handling POST /api/transcriptions")
}
return err
})
s.Router.Patch("/api/transcriptions", func(c *fiber.Ctx) error {
//log.Debug().Msgf("PATCH /api/transcriptions/%v", c.Params("id"))
err := s.handlePatchTranscription(c)
if err != nil {
log.Error().Err(err).Msg("Error handling PATCH /api/transcriptions")
}
return err
})
// Register HTTP route for receiving the form data and creating new transcription job.
s.Router.Delete("/api/transcriptions/:id", func(c *fiber.Ctx) error {
log.Debug().Msgf("DELETE /api/transcriptions/%v", c.Params("id"))
err := s.handleDeleteTranscription(c)
if err != nil {
log.Error().Err(err).Msg("Error handling DELETE /api/transcriptions")
}
return err
})
}
// Helper function to remove a WebSocket connection from the slice
func removeWsClient(s []*websocket.Conn, r *websocket.Conn) []*websocket.Conn {
for i, v := range s {
if v == r {
return append(s[:i], s[i+1:]...)
}
}
return s
}

41
backend/api/websocket.go Normal file
View File

@@ -0,0 +1,41 @@
package api
import (
"github.com/goccy/go-json"
"github.com/gofiber/contrib/websocket"
"github.com/rs/zerolog/log"
"go.mongodb.org/mongo-driver/bson/primitive"
"codeberg.org/pluja/whishper/models"
)
func (s *Server) handleWebsocketMessage(wsess *websocket.Conn, msg []byte) {
log.Info().Msgf("Received message from client: %v", wsess.RemoteAddr().String())
// Try to unmarshal message to transcription
var transcription models.Transcription
err := json.Unmarshal(msg, &transcription)
if err != nil {
log.Error().Err(err).Msg("Error unmarshalling message to transcription:")
return
}
var res *models.Transcription
// Check if the transcription has a valid ID
// If it has ID, it means it's an update
if transcription.ID != primitive.NilObjectID {
log.Printf("Updating transcription: %v", transcription.ID)
// Update transcription in database
res, err = s.Db.UpdateTranscription(&transcription)
if err != nil {
log.Error().Err(err).Msg("Error updating transcription in database:")
return
}
log.Printf("Updated transcription in database: %v", res)
} else {
log.Error().Msgf("Transcription not updated, it does not have an ID")
return
}
// broadcast with gofiber websocket
s.BroadcastTranscription(res)
}

View File

@@ -0,0 +1,14 @@
package database
import (
"codeberg.org/pluja/whishper/models"
)
type Db interface {
NewTranscription(*models.Transcription) (*models.Transcription, error)
UpdateTranscription(*models.Transcription) (*models.Transcription, error)
DeleteTranscription(string) error
GetTranscription(string) *models.Transcription
GetAllTranscriptions() []*models.Transcription
GetPendingTranscriptions() []*models.Transcription
}

166
backend/database/mongo.go Normal file
View File

@@ -0,0 +1,166 @@
package database
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/rs/zerolog/log"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"codeberg.org/pluja/whishper/models"
)
type MongoDb struct {
client *mongo.Client
}
func NewMongoDb() *MongoDb {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(fmt.Sprintf("mongodb://root:example@%v", os.Getenv("DB_ENDPOINT"))))
if err != nil {
log.Error().Err(err).Msg("Error connecting to mongodb")
}
return &MongoDb{
client: client,
}
}
func (m *MongoDb) GetTranscription(id string) *models.Transcription {
collection := m.client.Database("whishper").Collection("transcriptions")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
log.Printf("Error converting id to object id: %v", err)
return nil
}
filter := bson.D{primitive.E{Key: "_id", Value: oid}}
var result models.Transcription
err = collection.FindOne(ctx, filter).Decode(&result)
if err != nil {
log.Printf("Error getting transcription: %v", err)
return nil
}
return &result
}
func (m *MongoDb) DeleteTranscription(id string) error {
collection := m.client.Database("whishper").Collection("transcriptions")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
log.Debug().Msg("Error converting id to object id.")
return err
}
filter := bson.D{primitive.E{Key: "_id", Value: oid}}
_, err = collection.DeleteOne(ctx, filter)
if err != nil {
log.Debug().Msg("Error deleting transcription")
return err
}
return nil
}
func (m *MongoDb) NewTranscription(t *models.Transcription) (*models.Transcription, error) {
collection := m.client.Database("whishper").Collection("transcriptions")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Create a new mongodb object id
i, err := collection.InsertOne(ctx, t)
if err != nil {
log.Printf("Error creating new transcription: %v", err)
return nil, err
}
// Set the id of the transcription to the mongodb object id
t.ID = i.InsertedID.(primitive.ObjectID)
return t, nil
}
func (s *MongoDb) GetAllTranscriptions() []*models.Transcription {
collection := s.client.Database("whishper").Collection("transcriptions")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cursor, err := collection.Find(ctx, bson.D{})
if err != nil {
log.Printf("Error getting transcriptions: %v", err)
return nil
}
defer cursor.Close(ctx)
var transcriptions []*models.Transcription
for cursor.Next(ctx) {
var result models.Transcription
err := cursor.Decode(&result)
if err != nil {
log.Printf("Error decoding transcription: %v", err)
return nil
}
transcriptions = append(transcriptions, &result)
}
if err := cursor.Err(); err != nil {
log.Printf("Error getting transcriptions: %v", err)
return nil
}
return transcriptions
}
func (s *MongoDb) GetPendingTranscriptions() []*models.Transcription {
collection := s.client.Database("whishper").Collection("transcriptions")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
filter := bson.D{primitive.E{Key: "status", Value: models.TranscriptionStatusPending}}
cursor, err := collection.Find(ctx, filter)
if err != nil {
log.Printf("Error getting transcriptions: %v", err)
return nil
}
defer cursor.Close(ctx)
var transcriptions []*models.Transcription
for cursor.Next(ctx) {
var result models.Transcription
err := cursor.Decode(&result)
if err != nil {
log.Printf("Error decoding transcription: %v", err)
return nil
}
transcriptions = append(transcriptions, &result)
}
return transcriptions
}
func (m *MongoDb) UpdateTranscription(t *models.Transcription) (*models.Transcription, error) {
collection := m.client.Database("whishper").Collection("transcriptions")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
filter := bson.D{primitive.E{Key: "_id", Value: t.ID}}
updateQuery := bson.D{primitive.E{Key: "$set", Value: t}}
updateResult, err := collection.UpdateOne(ctx, filter, updateQuery)
if err != nil {
return nil, err
}
if updateResult.MatchedCount == 0 {
return nil, errors.New("no documents matched the filter")
}
if updateResult.ModifiedCount == 0 {
return nil, errors.New("no documents were modified")
}
return t, nil
}

41
backend/go.mod Normal file
View File

@@ -0,0 +1,41 @@
module codeberg.org/pluja/whishper
go 1.20
require (
github.com/goccy/go-json v0.10.2
github.com/gofiber/contrib/websocket v1.2.0
github.com/gofiber/fiber/v2 v2.48.0
github.com/rs/zerolog v1.30.0
go.mongodb.org/mongo-driver v1.12.1
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/fasthttp/websocket v1.5.4 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/snakesel/libretranslate v0.0.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.48.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/wader/goutubedl v0.0.0-20230817095831-89e825670ccd // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)

114
backend/go.sum Normal file
View File

@@ -0,0 +1,114 @@
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fasthttp/websocket v1.5.4 h1:Bq8HIcoiffh3pmwSKB8FqaNooluStLQQxnzQspMatgI=
github.com/fasthttp/websocket v1.5.4/go.mod h1:R2VXd4A6KBspb5mTrsWnZwn6ULkX56/Ktk8/0UNSJao=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/contrib/websocket v1.2.0 h1:E+GNxglSApjJCPwH1y3wLz69c1PuSvADwhMBeDc8Xxc=
github.com/gofiber/contrib/websocket v1.2.0/go.mod h1:Sf8RYFluiIKxONa/Kq0jk05EOUtqrb81pJopTxzcsX4=
github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0=
github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/snakesel/libretranslate v0.0.2 h1:6LG/UMMpGtoj3NXvlzsxZgQEH0Qsi62jCDd5Yq5ALL8=
github.com/snakesel/libretranslate v0.0.2/go.mod h1:B8F8Dda8RlkHRMzs/aw8DWj9HfyHSXpaJTFD391hEUI=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/wader/goutubedl v0.0.0-20230817095831-89e825670ccd h1:XPyQDsvxm+Kawdk41tpvODAEcm24h9vVR/aZTI0tGAk=
github.com/wader/goutubedl v0.0.0-20230817095831-89e825670ccd/go.mod h1:5KXd5tImdbmz4JoVhePtbIokCwAfEhUVVx3WLHmjYuw=
github.com/wader/osleaktest v0.0.0-20191111175233-f643b0fed071/go.mod h1:XD6emOFPHVzb0+qQpiNOdPL2XZ0SRUM0N5JHuq6OmXo=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE=
go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

65
backend/main.go Normal file
View File

@@ -0,0 +1,65 @@
package main
import (
"flag"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"codeberg.org/pluja/whishper/api"
"codeberg.org/pluja/whishper/database"
"codeberg.org/pluja/whishper/monitor"
)
func main() {
// Logging
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
zerolog.SetGlobalLevel(zerolog.InfoLevel)
listenAddr := flag.String("addr", ":8080", "server listen address")
uploadDir := flag.String("updir", "/app/uploads", "upload directory")
asrEndpoint := flag.String("asr", "transcription-api:8000", "asr endpoint, i.e. localhost:9888")
dbHost := flag.String("db", "mongo:27017", "database endpoint host, i.e. localhost:27017")
translationEndpoint := flag.String("translation", "translate:5000", "translation endpoint, i.e. localhost:5000")
dev := flag.Bool("dev", false, "development mode")
flag.Parse()
// Set environment variables
if os.Getenv("UPLOAD_DIR") == "" {
os.Setenv("UPLOAD_DIR", *uploadDir)
}
if os.Getenv("ASR_ENDPOINT") == "" {
os.Setenv("ASR_ENDPOINT", *asrEndpoint)
}
if os.Getenv("TRANSLATION_ENDPOINT") == "" {
os.Setenv("TRANSLATION_ENDPOINT", *translationEndpoint)
}
if os.Getenv("DB_ENDPOINT") == "" {
os.Setenv("DB_ENDPOINT", *dbHost)
}
// Configure dev mode
if *dev {
log.Logger = log.Output(
zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: "15:04:05",
},
).With().Caller().Logger()
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
log.Debug().Msg("DEV MODE IS ON")
log.Debug().Msgf("ListenAddr: %v", *listenAddr)
log.Debug().Msgf("UploadDir: %v", *uploadDir)
log.Debug().Msgf("AsrEndpoint: %v", *asrEndpoint)
log.Debug().Msgf("TranslationEndpoint: %v", *translationEndpoint)
log.Debug().Msgf("DbHost: %v", *dbHost)
dabs := database.NewMongoDb()
server := api.NewServer(*listenAddr, dabs)
go monitor.StartMonitor(server)
server.NewTranscriptionCh <- true
server.Run()
}

View File

@@ -0,0 +1,14 @@
package models
const (
TranscriptionStatusPending = 0
TranscriptionStatusRunning = 1
TranscriptionStatusDone = 2
TrannscriptionStatusTranslating = 3
TranscriptionStatusError = -1
SourceTypeFile = "file"
SourceTypeURL = "url"
FileNameSeparator = "_WHSHPR_"
)

View File

@@ -0,0 +1,67 @@
package models
import (
"fmt"
"os"
"github.com/rs/zerolog/log"
ltr "github.com/snakesel/libretranslate"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Transcription struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
Status int `bson:"status" json:"status"`
Language string `bson:"language" json:"language"`
ModelSize string `bson:"modelSize" json:"modelSize"`
Task string `bson:"task" json:"task"`
Device string `bson:"device" json:"device"`
FileName string `bson:"fileName" json:"fileName"`
SourceUrl string `bson:"sourceUrl" json:"sourceUrl"`
Result WhisperResult `bson:"result" json:"result"`
Translations []Translation `bson:"translations" json:"translations"`
}
func (t *Transcription) Translate(target string) error {
for _, translation := range t.Translations {
if translation.TargetLanguage == target {
log.Debug().Msgf("Translation for %v already exists!", target)
return fmt.Errorf("translation for %v already exists", target)
}
}
translate := ltr.New(ltr.Config{
Url: fmt.Sprintf("http://%v", os.Getenv("TRANSLATION_ENDPOINT")),
})
var translation Translation
translation.SourceLanguage = t.Language
translation.TargetLanguage = target
trtext, err := translate.Translate(t.Result.Text, translation.SourceLanguage, translation.TargetLanguage)
if err != nil {
log.Debug().Err(err).Msgf("Error translating text...")
return err
}
translatedText := trtext
translatedSegments := make([]Segment, len(t.Result.Segments))
copy(translatedSegments, t.Result.Segments)
for i, seg := range t.Result.Segments {
trtext, err := translate.Translate(seg.Text, translation.SourceLanguage, translation.TargetLanguage)
if err != nil {
log.Debug().Err(err).Msgf("Error translating segment text...")
return err
}
translatedSegments[i].Text = trtext
// Word-level data is lost, since we can't make sure that words will be in the same order and number as the final translation.
// For example, if we translate "The big home" to Spanish, we could get "La casa grande", thus words changed order.
translatedSegments[i].Words = []Word{}
}
translation.Result.Text = translatedText
translation.Result.Segments = translatedSegments
translation.Result.Language = translation.TargetLanguage
t.Translations = append(t.Translations, translation)
return nil
}

View File

@@ -0,0 +1,8 @@
package models
type Translation struct {
SourceLanguage string `json:"sourceLanguage"`
TargetLanguage string `json:"targetLanguage"`
Status int `json:"translationStatus"`
Result WhisperResult `json:"result"`
}

24
backend/models/whisper.go Normal file
View File

@@ -0,0 +1,24 @@
package models
type WhisperResult struct {
Language string `json:"language"`
Duration float64 `json:"duration"`
Segments []Segment `json:"segments"`
Text string `json:"text"`
}
type Segment struct {
End float64 `json:"end"`
ID string `json:"id"`
Start float64 `json:"start"`
Score float64 `json:"score"`
Text string `json:"text"`
Words []Word `json:"words"`
}
type Word struct {
End float64 `json:"end"`
Start float64 `json:"start"`
Word string `json:"word"`
Score float64 `json:"score"`
}

126
backend/monitor/monitor.go Normal file
View File

@@ -0,0 +1,126 @@
package monitor
import (
"bytes"
"io"
"mime/multipart"
"os"
"path/filepath"
"github.com/rs/zerolog/log"
"codeberg.org/pluja/whishper/api"
"codeberg.org/pluja/whishper/models"
"codeberg.org/pluja/whishper/utils"
)
func StartMonitor(s *api.Server) {
log.Info().Msg("Starting monitor!")
go func() {
for {
// Wait for new transcription to be added to the database
// notification will be received through the NewTranscriptionCh channel
<-s.NewTranscriptionCh
pendingTranscriptions := s.Db.GetPendingTranscriptions()
log.Debug().Msgf("Pending transcriptions: %v", len(pendingTranscriptions))
for _, pt := range pendingTranscriptions {
log.Debug().Msgf("Taking pending transcription %v", pt.ID)
if pt.Status == models.TranscriptionStatusPending {
err := transcribe(s, pt)
if err != nil {
log.Error().Err(err).Msg("Error transcribing")
pt.Status = models.TranscriptionStatusError
ut, err := s.Db.UpdateTranscription(pt)
if err != nil {
log.Error().Err(err).Msg("Error updating transcription")
}
s.BroadcastTranscription(ut)
continue
}
}
}
}
}()
}
func transcribe(s *api.Server, t *models.Transcription) error {
// Update transcription status
t.Status = models.TranscriptionStatusRunning
log.Debug().Msgf("Updating transcription %v", t)
_, err := s.Db.UpdateTranscription(t)
if err != nil {
log.Error().Err(err).Msg("Error updating transcription")
return err
}
s.BroadcastTranscription(t)
if t.SourceUrl != "" {
// Download media
fn, err := utils.DownloadMedia(t)
if err != nil {
log.Error().Err(err).Msg("Error downloading media")
return err
}
t.FileName = fn
s.BroadcastTranscription(t)
}
// Prepare multipart form data
body, writer, err := prepareMultipartFormData(t)
if err != nil {
log.Error().Err(err).Msg("Error preparing multipart form data")
return err
}
// Send transcription request to transcription service
res, err := utils.SendTranscriptionRequest(t, body, writer)
if err != nil {
log.Error().Err(err).Msg("Error sending transcription request")
return err
}
t.Result = *res
t.Translations = []models.Translation{}
t.Status = models.TranscriptionStatusDone
_, err = s.Db.UpdateTranscription(t)
if err != nil {
log.Error().Err(err).Msg("Error updating transcription")
return err
}
s.BroadcastTranscription(t)
return nil
}
func prepareMultipartFormData(t *models.Transcription) (*bytes.Buffer, *multipart.Writer, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", t.FileName)
if err != nil {
log.Error().Err(err).Msg("Error creating form file")
return nil, nil, err
}
// Read file from disk
filePath := filepath.Join(os.Getenv("UPLOAD_DIR"), t.FileName)
file, err := os.Open(filePath)
if err != nil {
log.Error().Err(err).Msg("Error opening file")
return nil, nil, err
}
defer file.Close()
_, err = io.Copy(part, file)
if err != nil {
log.Error().Err(err).Msg("Error copying file")
return nil, nil, err
}
err = writer.Close()
if err != nil {
log.Error().Err(err).Msg("Error closing writer")
return nil, nil, err
}
return body, writer, nil
}

111
backend/utils/utils.go Normal file
View File

@@ -0,0 +1,111 @@
package utils
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/rs/zerolog/log"
"github.com/wader/goutubedl"
"go.mongodb.org/mongo-driver/bson/primitive"
"codeberg.org/pluja/whishper/models"
)
func SanitizeFilename(filename string) string {
// First remove trailing spaces
filename = strings.TrimSpace(filename)
// Then remove quotes and dots
filename = strings.Trim(filename, `"'.`)
reg, _ := regexp.Compile("[^a-zA-Z0-9]+")
filename = reg.ReplaceAllString(filename, "_")
return filename
}
func DownloadMedia(t *models.Transcription) (string, error) {
if t.SourceUrl == "" {
log.Debug().Msg("Source URL is empty")
return "", fmt.Errorf("source URL is empty")
}
if t.ID == primitive.NilObjectID {
log.Debug().Msg("Transcription ID is empty")
return "", fmt.Errorf("transcription ID is empty")
}
goutubedl.Path = "yt-dlp"
result, err := goutubedl.New(context.Background(), t.SourceUrl, goutubedl.Options{})
if err != nil {
log.Debug().Err(err).Msg("Error creating goutubedl")
return "", err
}
downloadResult, err := result.Download(context.Background(), "best")
if err != nil {
log.Debug().Err(err).Msg("Error downloading media")
return "", err
}
filename := fmt.Sprintf("%v%v%v", t.ID.Hex(), models.FileNameSeparator, result.Info.Title)
filename = SanitizeFilename(filename)
defer downloadResult.Close()
f, err := os.Create(filepath.Join(os.Getenv("UPLOAD_DIR"), filename))
if err != nil {
log.Debug().Err(err).Msg("Error creating file")
return "", err
}
defer f.Close()
io.Copy(f, downloadResult)
return filename, nil
}
func SendTranscriptionRequest(t *models.Transcription, body *bytes.Buffer, writer *multipart.Writer) (*models.WhisperResult, error) {
url := fmt.Sprintf("http://%v/transcribe?model_size=%v&task=%v&language=%v&device=%v", os.Getenv("ASR_ENDPOINT"), t.ModelSize, t.Task, t.Language, t.Device)
// Send transcription request to transcription service
req, err := http.NewRequest("POST", url, body)
if err != nil {
log.Debug().Err(err).Msg("Error creating request to transcription service")
return nil, err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Debug().Err(err).Msg("Error sending request")
return nil, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
log.Debug().Err(err).Msg("Error reading response body")
return nil, err
}
if resp.StatusCode != http.StatusOK {
log.Debug().Msgf("Response from %v: %v", url, string(b))
log.Debug().Err(err).Msgf("Invalid response status %v:", resp.StatusCode)
return nil, errors.New("invalid status")
}
var asrResponse *models.WhisperResult
if err := json.Unmarshal(b, &asrResponse); err != nil {
log.Debug().Err(err).Msg("Error decoding response")
return nil, err
}
return asrResponse, nil
}