Some checks failed
CI / Backend (push) Failing after 11s
Release / Check ui (push) Failing after 51s
Release / Docker / ui (push) Has been skipped
CI / UI (push) Failing after 55s
Release / Test backend (push) Failing after 1m9s
Release / Docker / backend (push) Has been skipped
Release / Docker / runner (push) Has been skipped
Release / Docker / caddy (push) Failing after 28s
Release / Gitea Release (push) Has been skipped
CI / UI (pull_request) Failing after 42s
CI / Backend (pull_request) Successful in 3m45s
- LibreTranslate client (chunks on blank lines, ≤4500 chars, 3-goroutine semaphore)
- Runner translation task loop (OTel, heartbeat, MinIO storage)
- PocketBase translation_jobs collection support (create/claim/finish/list)
- Per-chapter language switcher on chapter reader (EN/RU/ID/PT/FR, polls until done)
- Admin /admin/translation page: bulk enqueue form + live-polling jobs table
- New backend routes: POST /api/translation/{slug}/{n}, GET /api/translation/status,
GET /api/translation/{slug}/{n}, GET /api/admin/translation/jobs,
POST /api/admin/translation/bulk
- ListTranslationTasks added to taskqueue.Reader interface + store impl
- All builds and tests pass; svelte-check: 0 errors
98 lines
2.8 KiB
Go
98 lines
2.8 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
|
|
"github.com/libnovel/backend/internal/domain"
|
|
)
|
|
|
|
// runTranslationTask executes one machine-translation task end-to-end and
|
|
// reports the result back to PocketBase.
|
|
func (r *Runner) runTranslationTask(ctx context.Context, task domain.TranslationTask) {
|
|
ctx, span := otel.Tracer("runner").Start(ctx, "runner.translation_task")
|
|
defer span.End()
|
|
span.SetAttributes(
|
|
attribute.String("task.id", task.ID),
|
|
attribute.String("book.slug", task.Slug),
|
|
attribute.Int("chapter.number", task.Chapter),
|
|
attribute.String("translation.lang", task.Lang),
|
|
)
|
|
|
|
log := r.deps.Log.With("task_id", task.ID, "slug", task.Slug, "chapter", task.Chapter, "lang", task.Lang)
|
|
log.Info("runner: translation task starting")
|
|
|
|
// Heartbeat goroutine — keeps the task alive while translation runs.
|
|
hbCtx, hbCancel := context.WithCancel(ctx)
|
|
defer hbCancel()
|
|
go func() {
|
|
tick := time.NewTicker(r.cfg.HeartbeatInterval)
|
|
defer tick.Stop()
|
|
for {
|
|
select {
|
|
case <-hbCtx.Done():
|
|
return
|
|
case <-tick.C:
|
|
if err := r.deps.Consumer.HeartbeatTask(ctx, task.ID); err != nil {
|
|
log.Warn("runner: heartbeat failed", "err", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
fail := func(msg string) {
|
|
log.Error("runner: translation task failed", "reason", msg)
|
|
r.tasksFailed.Add(1)
|
|
span.SetStatus(codes.Error, msg)
|
|
result := domain.TranslationResult{ErrorMessage: msg}
|
|
if err := r.deps.Consumer.FinishTranslationTask(ctx, task.ID, result); err != nil {
|
|
log.Error("runner: FinishTranslationTask failed", "err", err)
|
|
}
|
|
}
|
|
|
|
// Guard: LibreTranslate must be configured.
|
|
if r.deps.LibreTranslate == nil {
|
|
fail("libretranslate client not configured (LIBRETRANSLATE_URL is empty)")
|
|
return
|
|
}
|
|
|
|
// 1. Read raw markdown chapter.
|
|
raw, err := r.deps.BookReader.ReadChapter(ctx, task.Slug, task.Chapter)
|
|
if err != nil {
|
|
fail(fmt.Sprintf("read chapter: %v", err))
|
|
return
|
|
}
|
|
if raw == "" {
|
|
fail("chapter text is empty")
|
|
return
|
|
}
|
|
|
|
// 2. Translate (chunked, concurrent).
|
|
translated, err := r.deps.LibreTranslate.Translate(ctx, raw, "en", task.Lang)
|
|
if err != nil {
|
|
fail(fmt.Sprintf("translate: %v", err))
|
|
return
|
|
}
|
|
|
|
// 3. Store translated markdown in MinIO.
|
|
key := r.deps.TranslationStore.TranslationObjectKey(task.Lang, task.Slug, task.Chapter)
|
|
if err := r.deps.TranslationStore.PutTranslation(ctx, key, []byte(translated)); err != nil {
|
|
fail(fmt.Sprintf("put translation: %v", err))
|
|
return
|
|
}
|
|
|
|
// 4. Report success.
|
|
r.tasksCompleted.Add(1)
|
|
span.SetStatus(codes.Ok, "")
|
|
result := domain.TranslationResult{ObjectKey: key}
|
|
if err := r.deps.Consumer.FinishTranslationTask(ctx, task.ID, result); err != nil {
|
|
log.Error("runner: FinishTranslationTask failed", "err", err)
|
|
}
|
|
log.Info("runner: translation task finished", "key", key)
|
|
}
|