Properly save audio and image notes when uploaded

Also fix the audio processing pipeline.
This commit is contained in:
Eli Ribble 2026-01-06 22:23:59 +00:00
parent 4d02357671
commit 39d9f6d258
11 changed files with 145 additions and 138 deletions

View file

@ -16,6 +16,8 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/platform" "github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/queue" "github.com/Gleipnir-Technology/nidus-sync/queue"
"github.com/Gleipnir-Technology/nidus-sync/userfile" "github.com/Gleipnir-Technology/nidus-sync/userfile"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/google/uuid" "github.com/google/uuid"
@ -41,7 +43,18 @@ func apiAudioPost(w http.ResponseWriter, r *http.Request, u *models.User) {
http.Error(w, "Failed to decode the payload", http.StatusBadRequest) http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return return
} }
if err := db.NoteAudioCreate(context.Background(), noteUUID, db.NoteAudio{}, u.ID); err != nil { setter := models.NoteAudioSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(u.ID),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
Duration: omit.From(payload.Duration),
Transcription: omitnull.FromPtr(payload.Transcription),
TranscriptionUserEdited: omit.From(payload.TranscriptionUserEdited),
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
if err := db.NoteAudioCreate(context.Background(), u.R.Organization, u.ID, setter); err != nil {
render.Render(w, r, errRender(err)) render.Render(w, r, errRender(err))
return return
} }
@ -92,8 +105,15 @@ func handleClientIos(w http.ResponseWriter, r *http.Request, u *models.User) {
return return
} }
var since_used time.Time
if since == nil {
since_used = time.Unix(0, 0)
} else {
since_used = *since
}
response := ResponseClientIos{ response := ResponseClientIos{
Fieldseeker: toResponseFieldseeker(csync.Fieldseeker), Fieldseeker: toResponseFieldseeker(csync.Fieldseeker),
Since: since_used,
} }
if err := render.Render(w, r, response); err != nil { if err := render.Render(w, r, response); err != nil {
render.Render(w, r, errRender(err)) render.Render(w, r, errRender(err))
@ -101,31 +121,6 @@ func handleClientIos(w http.ResponseWriter, r *http.Request, u *models.User) {
} }
} }
func apiClientIosNotePut(w http.ResponseWriter, r *http.Request, u *models.User) {
id := chi.URLParam(r, "uuid")
noteUUID, err := uuid.Parse(id)
if err != nil {
http.Error(w, "Failed to decode the uuid", http.StatusBadRequest)
return
}
var payload NidusNotePayload
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read the payload", http.StatusBadRequest)
return
}
if err := json.Unmarshal(body, &payload); err != nil {
debugSaveRequest(body, err, "Note PUT JSON decode error")
http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return
}
if err := db.NoteUpdate(context.Background(), noteUUID, db.NidusNotePayload{}); err != nil {
render.Render(w, r, errRender(err))
return
}
w.WriteHeader(http.StatusAccepted)
}
func apiImagePost(w http.ResponseWriter, r *http.Request, u *models.User) { func apiImagePost(w http.ResponseWriter, r *http.Request, u *models.User) {
id := chi.URLParam(r, "uuid") id := chi.URLParam(r, "uuid")
noteUUID, err := uuid.Parse(id) noteUUID, err := uuid.Parse(id)
@ -145,7 +140,15 @@ func apiImagePost(w http.ResponseWriter, r *http.Request, u *models.User) {
http.Error(w, "Failed to decode the payload", http.StatusBadRequest) http.Error(w, "Failed to decode the payload", http.StatusBadRequest)
return return
} }
err = db.NoteImageCreate(context.Background(), noteUUID, db.NoteImage{}, u.ID) setter := models.NoteImageSetter{
Created: omit.From(payload.Created),
CreatorID: omit.From(u.ID),
Deleted: omitnull.FromPtr(payload.Deleted),
DeletorID: omitnull.FromPtr(payload.DeletorID),
Version: omit.From(payload.Version),
UUID: omit.From(noteUUID),
}
err = db.NoteImageCreate(context.Background(), u.R.Organization, u.ID, setter)
if err != nil { if err != nil {
render.Render(w, r, errRender(err)) render.Render(w, r, errRender(err))
return return
@ -187,7 +190,7 @@ func apiMosquitoSource(w http.ResponseWriter, r *http.Request, u *models.User) {
} }
data := []render.Renderer{} data := []render.Renderer{}
for _, s := range *sources { for _, s := range sources {
data = append(data, NewResponseMosquitoSource(s)) data = append(data, NewResponseMosquitoSource(s))
} }
if err := render.RenderList(w, r, data); err != nil { if err := render.RenderList(w, r, data); err != nil {
@ -212,7 +215,7 @@ func apiTrapData(w http.ResponseWriter, r *http.Request, u *models.User) {
} }
data := []render.Renderer{} data := []render.Renderer{}
for _, td := range *trap_data { for _, td := range trap_data {
data = append(data, NewResponseTrapDatum(td)) data = append(data, NewResponseTrapDatum(td))
} }
if err := render.RenderList(w, r, data); err != nil { if err := render.RenderList(w, r, data); err != nil {
@ -236,7 +239,7 @@ func apiServiceRequest(w http.ResponseWriter, r *http.Request, u *models.User) {
} }
data := []render.Renderer{} data := []render.Renderer{}
for _, sr := range *requests { for _, sr := range requests {
data = append(data, NewResponseServiceRequest(sr)) data = append(data, NewResponseServiceRequest(sr))
} }
if err := render.RenderList(w, r, data); err != nil { if err := render.RenderList(w, r, data); err != nil {

View file

@ -14,7 +14,6 @@ func AddRoutes(r chi.Router) {
r.Method("GET", "/service-request", auth.NewEnsureAuth(apiServiceRequest)) r.Method("GET", "/service-request", auth.NewEnsureAuth(apiServiceRequest))
r.Method("GET", "/trap-data", auth.NewEnsureAuth(apiTrapData)) r.Method("GET", "/trap-data", auth.NewEnsureAuth(apiTrapData))
r.Method("GET", "/client/ios", auth.NewEnsureAuth(handleClientIos)) r.Method("GET", "/client/ios", auth.NewEnsureAuth(handleClientIos))
r.Method("PUT", "/client/ios/note/{uuid}", auth.NewEnsureAuth(apiClientIosNotePut))
r.Method("POST", "/audio/{uuid}", auth.NewEnsureAuth(apiAudioPost)) r.Method("POST", "/audio/{uuid}", auth.NewEnsureAuth(apiAudioPost))
r.Method("POST", "/audio/{uuid}/content", auth.NewEnsureAuth(apiAudioContentPost)) r.Method("POST", "/audio/{uuid}/content", auth.NewEnsureAuth(apiAudioContentPost))
r.Method("POST", "/image/{uuid}", auth.NewEnsureAuth(apiImagePost)) r.Method("POST", "/image/{uuid}", auth.NewEnsureAuth(apiImagePost))

View file

@ -32,44 +32,34 @@ func NewBounds() Bounds {
} }
} }
type LatLong interface { /* not sure if used
Latitude() float64
Longitude() float64
}
type Location struct { type Location struct {
Latitude float64 Latitude float64
Longitude float64 Longitude float64
} }
*/
type NoteImagePayload struct { type NoteImagePayload struct {
UUID string `json:"uuid"` UUID string `json:"uuid"`
Cell H3Cell `json:"cell"` Cell H3Cell `json:"cell"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
} CreatorID int `db:"creator_id"`
Deleted *time.Time `json:"deleted"`
type NoteAudio struct { DeletorID *int32 `json:"deletor_id"`
UUID string `db:"uuid"` Version int32 `json:"version"`
Breadcrumbs []NoteAudioBreadcrumbPayload
Created time.Time `db:"created"`
Creator int `db:"creator"`
Deleted *time.Time `db:"deleted"`
Duration int `db:"duration"`
IsAudioNormalized bool `db:"is_audio_normalized"`
IsTranscodedeToOgg bool `db:"is_transcoded_to_ogg"`
Transcription *string `db:"transcription"`
TranscriptionUserEdited bool `db:"transcription_user_edited"`
Version int `db:"version"`
} }
type NoteAudioPayload struct { type NoteAudioPayload struct {
UUID string `json:"uuid"` UUID string `json:"uuid"`
Breadcrumbs []NoteAudioBreadcrumbPayload `json:"breadcrumbs"` Breadcrumbs []NoteAudioBreadcrumbPayload `json:"breadcrumbs"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Duration int `json:"duration"` CreatorID int `json:"creator_id"`
Deleted *time.Time `json:"deleted"`
DeletorID *int32 `json:"deletor_id"`
Duration float32 `json:"duration"`
Transcription *string `json:"transcription"` Transcription *string `json:"transcription"`
TranscriptionUserEdited bool `json:"transcriptionUserEdited"` TranscriptionUserEdited bool `json:"transcriptionUserEdited"`
Version int `json:"version"` Version int32 `json:"version"`
} }
type ResponseMosquitoSource struct { type ResponseMosquitoSource struct {
@ -97,14 +87,6 @@ type NoteAudioBreadcrumbPayload struct {
ManuallySelected bool `json:"manuallySelected"` ManuallySelected bool `json:"manuallySelected"`
} }
type NidusNotePayload struct {
UUID string `json:"uuid"`
Timestamp time.Time `json:"timestamp"`
H3Cell int64 `json:"h3cell"`
Images []string `json:"images"`
Text string `json:"text"`
}
type ResponseFieldseeker struct { type ResponseFieldseeker struct {
MosquitoSources []ResponseMosquitoSource `json:"sources"` MosquitoSources []ResponseMosquitoSource `json:"sources"`
ServiceRequests []ResponseServiceRequest `json:"requests"` ServiceRequests []ResponseServiceRequest `json:"requests"`
@ -114,6 +96,7 @@ type ResponseFieldseeker struct {
// ResponseErr renderer type for handling all sorts of errors. // ResponseErr renderer type for handling all sorts of errors.
type ResponseClientIos struct { type ResponseClientIos struct {
Fieldseeker ResponseFieldseeker `json:"fieldseeker"` Fieldseeker ResponseFieldseeker `json:"fieldseeker"`
Since time.Time `json:"since"`
} }
func (i ResponseClientIos) Render(w http.ResponseWriter, r *http.Request) error { func (i ResponseClientIos) Render(w http.ResponseWriter, r *http.Request) error {
@ -163,9 +146,9 @@ func NewResponseMosquitoInspection(i *models.FieldseekerMosquitoinspection) Resp
SiteCondition: i.Sitecond.GetOr(""), SiteCondition: i.Sitecond.GetOr(""),
} }
} }
func NewResponseMosquitoInspections(inspections *models.FieldseekerMosquitoinspectionSlice) []ResponseMosquitoInspection { func NewResponseMosquitoInspections(inspections models.FieldseekerMosquitoinspectionSlice) []ResponseMosquitoInspection {
results := make([]ResponseMosquitoInspection, 0) results := make([]ResponseMosquitoInspection, 0)
for _, i := range *inspections { for _, i := range inspections {
results = append(results, NewResponseMosquitoInspection(i)) results = append(results, NewResponseMosquitoInspection(i))
} }
return results return results
@ -175,7 +158,7 @@ func (rtd ResponseMosquitoSource) Render(w http.ResponseWriter, r *http.Request)
return nil return nil
} }
func NewResponseMosquitoSource(ms *platform.MosquitoSource) ResponseMosquitoSource { func NewResponseMosquitoSource(ms platform.MosquitoSource) ResponseMosquitoSource {
pl := ms.PointLocation pl := ms.PointLocation
return ResponseMosquitoSource{ return ResponseMosquitoSource{
Active: toBool16(pl.Active), Active: toBool16(pl.Active),
@ -196,9 +179,9 @@ func NewResponseMosquitoSource(ms *platform.MosquitoSource) ResponseMosquitoSour
Zone: pl.Zone.GetOr(""), Zone: pl.Zone.GetOr(""),
} }
} }
func NewResponseMosquitoSources(sources *[]*platform.MosquitoSource) []ResponseMosquitoSource { func NewResponseMosquitoSources(sources []platform.MosquitoSource) []ResponseMosquitoSource {
results := make([]ResponseMosquitoSource, 0) results := make([]ResponseMosquitoSource, 0)
for _, i := range *sources { for _, i := range sources {
results = append(results, NewResponseMosquitoSource(i)) results = append(results, NewResponseMosquitoSource(i))
} }
return results return results
@ -224,24 +207,22 @@ func (rtd ResponseMosquitoTreatment) Render(w http.ResponseWriter, r *http.Reque
} }
func NewResponseMosquitoTreatment(i *models.FieldseekerTreatment) ResponseMosquitoTreatment { func NewResponseMosquitoTreatment(i *models.FieldseekerTreatment) ResponseMosquitoTreatment {
return ResponseMosquitoTreatment{ return ResponseMosquitoTreatment{
/* Comments: i.Comments.GetOr(""),
Comments: i.Comments(), Created: formatTime(i.Creationdate),
Created: i.Created().Format("2006-01-02T15:04:05.000Z"), FieldTechnician: i.Fieldtech.GetOr(""),
FieldTechnician: i.FieldTechnician(), Habitat: i.Habitat.GetOr(""),
Habitat: i.Habitat(), ID: i.Globalid.String(),
ID: i.ID(), Product: i.Product.GetOr(""),
Product: i.Product(), Quantity: i.Qty.GetOr(0),
Quantity: i.Quantity(), QuantityUnit: i.Qtyunit.GetOr(""),
QuantityUnit: i.QuantityUnit(), SiteCondition: i.Sitecond.GetOr(""),
SiteCondition: i.SiteCondition(), TreatAcres: i.Treatacres.GetOr(0.0),
TreatAcres: i.TreatAcres(), TreatHectares: i.Treathectares.GetOr(0.0),
TreatHectares: i.TreatHectares(),
*/
} }
} }
func NewResponseMosquitoTreatments(treatments *models.FieldseekerTreatmentSlice) []ResponseMosquitoTreatment { func NewResponseMosquitoTreatments(treatments models.FieldseekerTreatmentSlice) []ResponseMosquitoTreatment {
results := make([]ResponseMosquitoTreatment, 0) results := make([]ResponseMosquitoTreatment, 0)
for _, i := range *treatments { for _, i := range treatments {
results = append(results, NewResponseMosquitoTreatment(i)) results = append(results, NewResponseMosquitoTreatment(i))
} }
return results return results
@ -298,9 +279,9 @@ func NewResponseServiceRequest(sr *models.FieldseekerServicerequest) ResponseSer
Zip: sr.Reqzip.GetOr(""), Zip: sr.Reqzip.GetOr(""),
} }
} }
func NewResponseServiceRequests(requests *models.FieldseekerServicerequestSlice) []ResponseServiceRequest { func NewResponseServiceRequests(requests models.FieldseekerServicerequestSlice) []ResponseServiceRequest {
results := make([]ResponseServiceRequest, 0) results := make([]ResponseServiceRequest, 0)
for _, i := range *requests { for _, i := range requests {
results = append(results, NewResponseServiceRequest(i)) results = append(results, NewResponseServiceRequest(i))
} }
return results return results
@ -326,9 +307,9 @@ func NewResponseTrapDatum(td *models.FieldseekerTraplocation) ResponseTrapData {
Name: td.Name.GetOr(""), Name: td.Name.GetOr(""),
} }
} }
func NewResponseTrapData(data *models.FieldseekerTraplocationSlice) []ResponseTrapData { func NewResponseTrapData(data models.FieldseekerTraplocationSlice) []ResponseTrapData {
results := make([]ResponseTrapData, 0) results := make([]ResponseTrapData, 0)
for _, i := range *data { for _, i := range data {
results = append(results, NewResponseTrapDatum(i)) results = append(results, NewResponseTrapDatum(i))
} }
return results return results

View file

@ -173,6 +173,7 @@ func findUser(ctx context.Context, user_id int) (*models.User, error) {
return nil, err return nil, err
} }
} }
log.Info().Int32("user_id", user.ID).Int32("org_id", user.OrganizationID).Msg("Found user")
return user, err return user, err
} }

View file

@ -3,20 +3,25 @@ package db
import ( import (
"context" "context"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rs/zerolog/log"
) )
type NidusNotePayload struct {}
type NoteAudio struct {
Transcription string
Version int
UUID uuid.UUID
}
type NoteImage struct {}
func NoteAudioCreate(ctx context.Context, noteUUID uuid.UUID, payload NoteAudio, userID int32) error { func NoteAudioCreate(ctx context.Context, org *models.Organization, userID int32, setter models.NoteAudioSetter) error {
err := org.InsertNoteAudios(ctx, PGInstance.BobDB, &setter)
if err == nil {
return nil return nil
} }
func NoteAudioGetLatest(ctx context.Context, uuid string) (*NoteAudio, error) { // Just ignore this failure, it means we already have this content
if err.Error() == "insertOrganizationNoteAudios0: ERROR: duplicate key value violates unique constraint \"note_audio_pkey\" (SQLSTATE 23505)" {
return nil
}
log.Warn().Err(err).Msg("Unrecognized error creating note audio")
return err
}
func NoteAudioGetLatest(ctx context.Context, uuid string) (*models.NoteAudio, error) {
return nil, nil return nil, nil
} }
func NoteAudioNormalized(uuid string) error { func NoteAudioNormalized(uuid string) error {
@ -25,9 +30,19 @@ func NoteAudioNormalized(uuid string) error {
func NoteAudioTranscodedToOgg(uuid string) error { func NoteAudioTranscodedToOgg(uuid string) error {
return nil return nil
} }
func NoteImageCreate(ctx context.Context, noteUUID uuid.UUID, payload NoteImage, userID int32) error { func NoteImageCreate(ctx context.Context, org *models.Organization, userID int32, setter models.NoteImageSetter) error {
err := org.InsertNoteImages(ctx, PGInstance.BobDB, &setter)
if err == nil {
return nil return nil
} }
func NoteUpdate(ctx context.Context, noteUUID uuid.UUID, payload NidusNotePayload) error { // Just ignore this failure, it means we already have this content
if err.Error() == "insertOrganizationNoteImages0: ERROR: duplicate key value violates unique constraint \"note_image_pkey\" (SQLSTATE 23505)" {
return nil
}
log.Warn().Err(err).Msg("Unrecognized error creating note audio")
return err
}
func NoteUpdate(ctx context.Context, noteUUID uuid.UUID, setter models.NoteAudioSetter) error {
return nil return nil
} }

View file

@ -14,6 +14,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/api" "github.com/Gleipnir-Technology/nidus-sync/api"
"github.com/Gleipnir-Technology/nidus-sync/auth" "github.com/Gleipnir-Technology/nidus-sync/auth"
"github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/queue"
"github.com/Gleipnir-Technology/nidus-sync/userfile" "github.com/Gleipnir-Technology/nidus-sync/userfile"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
@ -162,6 +163,12 @@ func main() {
refreshFieldseekerData(ctx, NewOAuthTokenChannel) refreshFieldseekerData(ctx, NewOAuthTokenChannel)
}() }()
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
queue.StartAudioWorker(ctx)
}()
server := &http.Server{ server := &http.Server{
Addr: bind, Addr: bind,
Handler: r, Handler: r,

View file

@ -4,17 +4,17 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
) )
func MosquitoSourceQuery() (*[]*MosquitoSource, error) { func MosquitoSourceQuery() ([]MosquitoSource, error) {
results := make([]*MosquitoSource, 0) results := make([]MosquitoSource, 0)
return &results, nil return results, nil
} }
func ServiceRequestQuery() (*models.FieldseekerServicerequestSlice, error) { func ServiceRequestQuery() (models.FieldseekerServicerequestSlice, error) {
results := make(models.FieldseekerServicerequestSlice, 0) results := make(models.FieldseekerServicerequestSlice, 0)
return &results, nil return results, nil
} }
func TrapDataQuery() (*models.FieldseekerTraplocationSlice, error) { func TrapDataQuery() (models.FieldseekerTraplocationSlice, error) {
results := make(models.FieldseekerTraplocationSlice, 0) results := make(models.FieldseekerTraplocationSlice, 0)
return &results, nil return results, nil
} }

View file

@ -57,7 +57,7 @@ func fieldseeker(ctx context.Context, u *models.User, since *time.Time) (fsync F
ts = append(ts, t) ts = append(ts, t)
treatments_by_location[locid] = ts treatments_by_location[locid] = ts
} }
sources := make([]*MosquitoSource, 0) sources := make([]MosquitoSource, 0)
for _, p := range pl { for _, p := range pl {
inspections, ok := inspections_by_location[p.Globalid] inspections, ok := inspections_by_location[p.Globalid]
if !ok { if !ok {
@ -68,13 +68,13 @@ func fieldseeker(ctx context.Context, u *models.User, since *time.Time) (fsync F
treatments = make(models.FieldseekerTreatmentSlice, 0) treatments = make(models.FieldseekerTreatmentSlice, 0)
} }
ms := MosquitoSource{ ms := MosquitoSource{
PointLocation: p, PointLocation: *p,
Inspections: &inspections, Inspections: inspections,
Treatments: &treatments, Treatments: treatments,
} }
sources = append(sources, &ms) sources = append(sources, ms)
} }
fsync.MosquitoSources = &sources fsync.MosquitoSources = sources
return fsync, err return fsync, err
} }

View file

@ -12,13 +12,13 @@ type ClientSync struct {
} }
type FieldseekerRecordsSync struct { type FieldseekerRecordsSync struct {
MosquitoSources *[]*MosquitoSource MosquitoSources []MosquitoSource
ServiceRequests *models.FieldseekerServicerequestSlice ServiceRequests models.FieldseekerServicerequestSlice
TrapData *models.FieldseekerTraplocationSlice TrapData models.FieldseekerTraplocationSlice
} }
type MosquitoSource struct { type MosquitoSource struct {
PointLocation *models.FieldseekerPointlocation PointLocation models.FieldseekerPointlocation
Inspections *models.FieldseekerMosquitoinspectionSlice Inspections models.FieldseekerMosquitoinspectionSlice
Treatments *models.FieldseekerTreatmentSlice Treatments models.FieldseekerTreatmentSlice
} }

View file

@ -4,13 +4,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log"
"os" "os"
"os/exec" "os/exec"
"github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/userfile" "github.com/Gleipnir-Technology/nidus-sync/userfile"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/rs/zerolog/log"
) )
// AudioJob represents a job to process an audio file. // AudioJob represents a job to process an audio file.
@ -25,18 +25,18 @@ var audioJobChannel chan AudioJob
func StartAudioWorker(ctx context.Context) { func StartAudioWorker(ctx context.Context) {
buffer := 100 buffer := 100
audioJobChannel = make(chan AudioJob, buffer) // Buffered channel to prevent blocking audioJobChannel = make(chan AudioJob, buffer) // Buffered channel to prevent blocking
log.Printf("Started audio worker with buffer depth %d", buffer) log.Info().Int("buffer depth", buffer).Msg("Started audio worker")
go func() { go func() {
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Println("Audio worker shutting down.") log.Info().Msg("Audio worker shutting down.")
return return
case job := <-audioJobChannel: case job := <-audioJobChannel:
log.Printf("Processing audio job for UUID: %s", job.AudioUUID) log.Info().Str("uuid", job.AudioUUID.String()).Msg("Processing audio job")
err := processAudioFile(job.AudioUUID) err := processAudioFile(job.AudioUUID)
if err != nil { if err != nil {
log.Printf("Error processing audio file %s: %v", job.AudioUUID, err) log.Error().Err(err).Str("uuid", job.AudioUUID.String()).Msg("Error processing audio file")
} }
} }
} }
@ -47,9 +47,9 @@ func StartAudioWorker(ctx context.Context) {
func EnqueueAudioJob(job AudioJob) { func EnqueueAudioJob(job AudioJob) {
select { select {
case audioJobChannel <- job: case audioJobChannel <- job:
log.Printf("Enqueued audio job for UUID: %s", job.AudioUUID) log.Info().Str("uuid", job.AudioUUID.String()).Msg("Enqueued audio job")
default: default:
log.Printf("Audio job channel is full, dropping job for UUID: %s", job.AudioUUID) log.Warn().Str("uuid", job.AudioUUID.String()).Msg("Audio job channel is full, dropping job")
} }
} }
@ -76,10 +76,10 @@ func normalizeAudio(audioUUID uuid.UUID) error {
source := userfile.AudioFileContentPathRaw(audioUUID.String()) source := userfile.AudioFileContentPathRaw(audioUUID.String())
_, err := os.Stat(source) _, err := os.Stat(source)
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
log.Printf("%s doesn't exist, skipping normalization", source) log.Warn().Str("source", source).Msg("file doesn't exist, skipping normalization")
return nil return nil
} }
log.Printf("Normalizing %s", source) log.Info().Str("sourcce", source).Msg("Normalizing")
destination := userfile.AudioFileContentPathNormalized(audioUUID.String()) destination := userfile.AudioFileContentPathNormalized(audioUUID.String())
// Use "ffmpeg" directly, assuming it's in the system PATH // Use "ffmpeg" directly, assuming it's in the system PATH
cmd := exec.Command("ffmpeg", "-i", source, "-filter:a", "loudnorm", destination) cmd := exec.Command("ffmpeg", "-i", source, "-filter:a", "loudnorm", destination)
@ -92,7 +92,7 @@ func normalizeAudio(audioUUID uuid.UUID) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to update database for normalized audio %s: %v", audioUUID, err) return fmt.Errorf("failed to update database for normalized audio %s: %v", audioUUID, err)
} }
log.Printf("Normalized audio to %s", destination) log.Info().Str("destination", destination).Msg("Normalized audio")
return nil return nil
} }
@ -100,22 +100,22 @@ func transcodeToOgg(audioUUID uuid.UUID) error {
source := userfile.AudioFileContentPathNormalized(audioUUID.String()) source := userfile.AudioFileContentPathNormalized(audioUUID.String())
_, err := os.Stat(source) _, err := os.Stat(source)
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
log.Printf("%s doesn't exist, skipping OGG transcoding", source) log.Warn().Str("source", source).Msg("file doesn't exist, skipping OGG transcoding")
return nil return nil
} }
log.Printf("Transcoding %s to ogg", source) log.Info().Str("source", source).Msg("Transcoding to ogg")
destination := userfile.AudioFileContentPathOgg(audioUUID.String()) destination := userfile.AudioFileContentPathOgg(audioUUID.String())
// Use "ffmpeg" directly, assuming it's in the system PATH // Use "ffmpeg" directly, assuming it's in the system PATH
cmd := exec.Command("ffmpeg", "-i", source, "-vn", "-acodec", "libvorbis", destination) cmd := exec.Command("ffmpeg", "-i", source, "-vn", "-acodec", "libvorbis", destination)
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
log.Printf("FFmpeg output for OGG transcoding: %s", out) log.Error().Err(err).Bytes("out", out).Msg("FFmpeg output for OGG transcoding")
return fmt.Errorf("ffmpeg OGG transcoding failed: %v", err) return fmt.Errorf("ffmpeg OGG transcoding failed: %v", err)
} }
err = db.NoteAudioTranscodedToOgg(audioUUID.String()) err = db.NoteAudioTranscodedToOgg(audioUUID.String())
if err != nil { if err != nil {
return fmt.Errorf("failed to update database for OGG transcoded audio %s: %v", audioUUID, err) return fmt.Errorf("failed to update database for OGG transcoded audio %s: %v", audioUUID, err)
} }
log.Printf("Transcoded audio to %s", destination) log.Info().Str("destination", destination).Msg("Transcoded audio")
return nil return nil
} }

View file

@ -9,6 +9,7 @@ import (
"os" "os"
"github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/label-studio" "github.com/Gleipnir-Technology/nidus-sync/label-studio"
"github.com/Gleipnir-Technology/nidus-sync/minio" "github.com/Gleipnir-Technology/nidus-sync/minio"
"github.com/Gleipnir-Technology/nidus-sync/userfile" "github.com/Gleipnir-Technology/nidus-sync/userfile"
@ -127,7 +128,7 @@ func processLabelTask(ctx context.Context, minioClient *minio.Client, minioBucke
return nil return nil
} }
func createTask(client *labelstudio.Client, project *labelstudio.Project, minioClient *minio.Client, bucket string, customer string, note *db.NoteAudio) error { func createTask(client *labelstudio.Client, project *labelstudio.Project, minioClient *minio.Client, bucket string, customer string, note *models.NoteAudio) error {
audioRef := fmt.Sprintf("s3://%s/%s-normalized.m4a", bucket, note.UUID) audioRef := fmt.Sprintf("s3://%s/%s-normalized.m4a", bucket, note.UUID)
audioFile := fmt.Sprintf("%s/%s-normalized.m4a", userfile.UserFilesDirectory, note.UUID) audioFile := fmt.Sprintf("%s/%s-normalized.m4a", userfile.UserFilesDirectory, note.UUID)
uploadPath := fmt.Sprintf("%s-normalized.m4a", note.UUID) uploadPath := fmt.Sprintf("%s-normalized.m4a", note.UUID)
@ -142,7 +143,7 @@ func createTask(client *labelstudio.Client, project *labelstudio.Project, minioC
//if note.Transcription.IsValue() { //if note.Transcription.IsValue() {
//transcription = note.Transcription.MustGet() //transcription = note.Transcription.MustGet()
//} //}
transcription = note.Transcription transcription = note.Transcription.GetOr("")
simpleTasks := []map[string]interface{}{ simpleTasks := []map[string]interface{}{
{ {
"data": map[string]string{ "data": map[string]string{
@ -184,7 +185,7 @@ func findLabelStudioProject(client *labelstudio.Client, title string) (*labelstu
return nil, fmt.Errorf("No such project '%s'", title) return nil, fmt.Errorf("No such project '%s'", title)
} }
func findMatchingTask(client *labelstudio.Client, project *labelstudio.Project, customer string, note *db.NoteAudio) (*labelstudio.Task, error) { func findMatchingTask(client *labelstudio.Client, project *labelstudio.Project, customer string, note *models.NoteAudio) (*labelstudio.Task, error) {
/*meta := map[string]string{ /*meta := map[string]string{
"customer": customer, "customer": customer,
"note_uuid": note.UUID, "note_uuid": note.UUID,