ci / module boundaries / boundaries (push) Successful in 10s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m30s
ci / api / api (push) Successful in 58s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m1s
ci / db / db (push) Failing after 25s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m36s
ci / ts / ts (packages + web) (push) Failing after 6m3s
77 lines
2.4 KiB
Go
77 lines
2.4 KiB
Go
package uploads
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sort"
|
|
|
|
"github.com/google/uuid"
|
|
"pipu/api/internal/acoustid"
|
|
)
|
|
|
|
const maxAudioFiles = 50
|
|
|
|
func chooseAudioMatch(id uuid.UUID, matches []acoustid.Match) AudioRecognition {
|
|
// Multiple AcoustIDs can point to the same recording. Compare distinct recordings only.
|
|
best := map[string]float64{}
|
|
for _, m := range matches {
|
|
if parsed, err := uuid.Parse(m.RecordingID); err == nil && parsed != uuid.Nil && m.Score > best[parsed.String()] && m.Score <= 1 {
|
|
best[parsed.String()] = m.Score
|
|
}
|
|
}
|
|
unique := []acoustid.Match{}
|
|
for id, score := range best {
|
|
unique = append(unique, acoustid.Match{RecordingID: id, Score: score})
|
|
}
|
|
sort.Slice(unique, func(i, j int) bool { return unique[i].Score > unique[j].Score })
|
|
r := AudioRecognition{FileID: id, State: "not_found"}
|
|
if len(unique) == 0 {
|
|
return r
|
|
}
|
|
r.Score = unique[0].Score
|
|
if unique[0].Score < .9 || len(unique) > 1 && unique[0].Score-unique[1].Score < .05 {
|
|
r.State = "ambiguous"
|
|
return r
|
|
}
|
|
r.State, r.RecordingID = "matched", unique[0].RecordingID
|
|
return r
|
|
}
|
|
|
|
// identifyAudio runs only after an explicit reviewer request. It never modifies file tags.
|
|
func (s *Service) identifyAudio(ctx context.Context, files []File) ([]AudioRecognition, map[uuid.UUID]AudioRecognition, []string, error) {
|
|
results := []AudioRecognition{}
|
|
byFile := map[uuid.UUID]AudioRecognition{}
|
|
ids := []string{}
|
|
if s.identifier == nil || s.encodedFingerprint == nil {
|
|
return results, byFile, ids, errors.New("acoustid: not configured")
|
|
}
|
|
for _, f := range releaseFiles(files) {
|
|
if f.Status != StatusPendingReview && f.Status != StatusImportFailed {
|
|
continue
|
|
}
|
|
if len(results) >= maxAudioFiles {
|
|
return results, byFile, ids, errors.New("acoustid: batch too large")
|
|
}
|
|
r := AudioRecognition{FileID: f.ID, State: "unavailable"}
|
|
if f.DurationMs != nil && *f.DurationMs > 0 && f.StagingPath != "" {
|
|
fingerprint, err := s.encodedFingerprint(ctx, s.stagingAbs(f.StagingPath))
|
|
if err == nil {
|
|
matches, err := s.identifier.Lookup(ctx, fingerprint, (*f.DurationMs+500)/1000)
|
|
if err != nil {
|
|
return results, byFile, ids, err
|
|
}
|
|
r = chooseAudioMatch(f.ID, matches)
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return results, byFile, ids, err
|
|
}
|
|
results = append(results, r)
|
|
byFile[f.ID] = r
|
|
if r.State == "matched" {
|
|
ids = append(ids, r.RecordingID)
|
|
}
|
|
}
|
|
return results, byFile, ids, nil
|
|
}
|