Initial commit
This commit is contained in:
31
backend/Dockerfile
Normal file
31
backend/Dockerfile
Normal 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
66
backend/README.md
Normal 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
190
backend/api/handlers.go
Normal 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
158
backend/api/server.go
Normal 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
41
backend/api/websocket.go
Normal 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)
|
||||
}
|
||||
14
backend/database/database.go
Normal file
14
backend/database/database.go
Normal 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
166
backend/database/mongo.go
Normal 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
41
backend/go.mod
Normal 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
114
backend/go.sum
Normal 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
65
backend/main.go
Normal 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()
|
||||
}
|
||||
14
backend/models/constants.go
Normal file
14
backend/models/constants.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
const (
|
||||
TranscriptionStatusPending = 0
|
||||
TranscriptionStatusRunning = 1
|
||||
TranscriptionStatusDone = 2
|
||||
TrannscriptionStatusTranslating = 3
|
||||
TranscriptionStatusError = -1
|
||||
|
||||
SourceTypeFile = "file"
|
||||
SourceTypeURL = "url"
|
||||
|
||||
FileNameSeparator = "_WHSHPR_"
|
||||
)
|
||||
67
backend/models/transcription.go
Normal file
67
backend/models/transcription.go
Normal 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
|
||||
}
|
||||
8
backend/models/translation.go
Normal file
8
backend/models/translation.go
Normal 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
24
backend/models/whisper.go
Normal 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
126
backend/monitor/monitor.go
Normal 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
111
backend/utils/utils.go
Normal 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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user